From e6e83ffa8a0555288caf1e6d11b973fbddbc2e00 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Wed, 14 Jan 2015 16:48:32 -0700 Subject: [PATCH 01/12] Update the VM name in VirtualBox for linked clones. --- gns3server/modules/virtualbox/virtualbox_vm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index e50ab9cb..22294edf 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -454,6 +454,8 @@ class VirtualBoxVM(object): """ log.info("VirtualBox VM {name} [id={id}] has set the VM name to {vmname}".format(name=self._name, id=self._id, vmname=vmname)) + if self._linked_clone: + self._modify_vm('--name "{}"'.format(vmname)) self._vmname = vmname @property From 701a1626f7b3e60aae0812f747d00128aa2a5c83 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 15 Jan 2015 17:44:09 -0700 Subject: [PATCH 02/12] Bump version to 1.2.2 --- gns3server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/version.py b/gns3server/version.py index 8b884619..ee8422d4 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,5 +23,5 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.2.2.dev2" +__version__ = "1.2.2" __version_info__ = (1, 2, 2, 0) From aff834f565d6524cad0f1d9091d159adf66e40ca Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 10:18:02 +0100 Subject: [PATCH 03/12] Oops bad merge --- gns3server/version.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/gns3server/version.py b/gns3server/version.py index 80e9e442..ed60edd9 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -15,10 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -<<<<<<< HEAD -__version__ = "1.3.dev1" -__version_info__ = (1, 3, 0, 0) -======= # __version__ is a human-readable version number. # __version_info__ is a four-tuple for programmatic comparison. The first @@ -27,6 +23,6 @@ __version_info__ = (1, 3, 0, 0) # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "1.2.2" -__version_info__ = (1, 2, 2, 0) ->>>>>>> origin/master +__version__ = "1.3.dev1" +__version_info__ = (1, 3, 0, 0) + From 0cdc1c3042b0b9bc52378fafbd1145fe56a5df5b Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 16:20:10 +0100 Subject: [PATCH 04/12] VCPS create NIO work and tested --- .../post_vpcsvpcsidportsportidnio.txt | 22 +++ gns3server/handlers/vpcs_handler.py | 35 ++-- gns3server/modules/base_manager.py | 6 +- gns3server/modules/vpcs/vpcs_device.py | 33 +++- gns3server/schemas/vpcs.py | 155 +----------------- tests/api/test_vpcs.py | 10 +- tests/modules/vpcs/test_vpcs_device.py | 16 ++ 7 files changed, 90 insertions(+), 187 deletions(-) create mode 100644 docs/api/examples/post_vpcsvpcsidportsportidnio.txt diff --git a/docs/api/examples/post_vpcsvpcsidportsportidnio.txt b/docs/api/examples/post_vpcsvpcsidportsportidnio.txt new file mode 100644 index 00000000..cdcb7a4e --- /dev/null +++ b/docs/api/examples/post_vpcsvpcsidportsportidnio.txt @@ -0,0 +1,22 @@ +curl -i -xPOST 'http://localhost:8000/vpcs/{vpcs_id}/ports/{port_id}/nio' -d '{"local_file": "/tmp/test", "remote_file": "/tmp/remote", "type": "nio_unix"}' + +POST /vpcs/{vpcs_id}/ports/{port_id}/nio HTTP/1.1 +{ + "local_file": "/tmp/test", + "remote_file": "/tmp/remote", + "type": "nio_unix" +} + + +HTTP/1.1 404 +CONNECTION: close +CONTENT-LENGTH: 59 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /vpcs/{vpcs_id}/ports/{port_id}/nio + +{ + "message": "ID 42 doesn't exist", + "status": 404 +} diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 5213c27e..6fc3495e 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . + from ..web.route import Route from ..schemas.vpcs import VPCS_CREATE_SCHEMA from ..schemas.vpcs import VPCS_OBJECT_SCHEMA @@ -72,32 +73,9 @@ class VPCSHandler(object): vm = yield from vpcs_manager.stop_vm(int(request.match_info['vpcs_id'])) response.json({}) - @classmethod - @Route.get( - r"/vpcs/{vpcs_id}", - parameters={ - "vpcs_id": "Id of VPCS instance" - }, - description="Get information about a VPCS", - output=VPCS_OBJECT_SCHEMA) - def show(request, response): - response.json({'name': "PC 1", "vpcs_id": 42, "console": 4242}) - - @classmethod - @Route.put( - r"/vpcs/{vpcs_id}", - parameters={ - "vpcs_id": "Id of VPCS instance" - }, - description="Update VPCS information", - input=VPCS_OBJECT_SCHEMA, - output=VPCS_OBJECT_SCHEMA) - def update(request, response): - response.json({'name': "PC 1", "vpcs_id": 42, "console": 4242}) - @classmethod @Route.post( - r"/vpcs/{vpcs_id}/nio", + r"/vpcs/{vpcs_id}/ports/{port_id}/nio", parameters={ "vpcs_id": "Id of VPCS instance" }, @@ -108,5 +86,12 @@ class VPCSHandler(object): description="ADD NIO to a VPCS", input=VPCS_ADD_NIO_SCHEMA) def create_nio(request, response): - # TODO: raise 404 if VPCS not found + # TODO: raise 404 if VPCS not found GET VM can raise an exeption + # TODO: response with nio + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(int(request.match_info['vpcs_id'])) + vm.port_add_nio_binding(int(request.match_info['port_id']), request.json) + response.json({'name': "PC 2", "vpcs_id": 42, "console": 4242}) + + diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 923576b5..ab07f427 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -48,7 +48,7 @@ class BaseManager: def destroy(cls): cls._instance = None - def _get_vm_instance(self, vm_id): + def get_vm(self, vm_id): """ Returns a VM instance. @@ -80,10 +80,10 @@ class BaseManager: @asyncio.coroutine def start_vm(self, vm_id): - vm = self._get_vm_instance(vm_id) + vm = self.get_vm(vm_id) yield from vm.start() @asyncio.coroutine def stop_vm(self, vm_id): - vm = self._get_vm_instance(vm_id) + vm = self.get_vm(vm_id) yield from vm.stop() diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 61c896a0..5384a985 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -27,12 +27,14 @@ import signal import shutil import re import asyncio +import socket from pkg_resources import parse_version from .vpcs_error import VPCSError from .adapters.ethernet_adapter import EthernetAdapter from .nios.nio_udp import NIO_UDP from .nios.nio_tap import NIO_TAP +from ..attic import has_privileged_access from ..base_vm import BaseVM @@ -168,8 +170,8 @@ class VPCSDevice(BaseVM): """ if not self.is_running(): - # if not self._ethernet_adapter.get_nio(0): - # raise VPCSError("This VPCS instance must be connected in order to start") + if not self._ethernet_adapter.get_nio(0): + raise VPCSError("This VPCS instance must be connected in order to start") self._command = self._build_command() try: @@ -237,7 +239,7 @@ class VPCSDevice(BaseVM): return True return False - def port_add_nio_binding(self, port_id, nio): + def port_add_nio_binding(self, port_id, nio_settings): """ Adds a port NIO binding. @@ -249,11 +251,34 @@ class VPCSDevice(BaseVM): raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, port_id=port_id)) + nio = None + if nio_settings["type"] == "nio_udp": + lport = nio_settings["lport"] + rhost = nio_settings["rhost"] + rport = nio_settings["rport"] + try: + #TODO: handle IPv6 + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.connect((rhost, rport)) + except OSError as e: + raise VPCSError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) + nio = NIO_UDP(lport, rhost, rport) + elif nio_settings["type"] == "nio_tap": + tap_device = nio_settings["tap_device"] + print(has_privileged_access) + if not has_privileged_access(self._path): + raise VPCSError("{} has no privileged access to {}.".format(self._path, tap_device)) + nio = NIO_TAP(tap_device) + if not nio: + raise VPCSError("Requested NIO does not exist or is not supported: {}".format(nio_settings["type"])) + + self._ethernet_adapter.add_nio(port_id, nio) log.info("VPCS {name} [id={id}]: {nio} added to port {port_id}".format(name=self._name, id=self._id, nio=nio, port_id=port_id)) + return nio def port_remove_nio_binding(self, port_id): """ @@ -317,6 +342,8 @@ class VPCSDevice(BaseVM): nio = self._ethernet_adapter.get_nio(0) if nio: + print(nio) + print(isinstance(nio, NIO_UDP)) if isinstance(nio, NIO_UDP): # UDP tunnel command.extend(["-s", str(nio.lport)]) # source UDP port diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index 7d205391..dc5ca6dd 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -75,36 +75,6 @@ VPCS_ADD_NIO_SCHEMA = { "required": ["type", "lport", "rhost", "rport"], "additionalProperties": False }, - "Ethernet": { - "description": "Generic Ethernet Network Input/Output", - "properties": { - "type": { - "enum": ["nio_generic_ethernet"] - }, - "ethernet_device": { - "description": "Ethernet device name e.g. eth0", - "type": "string", - "minLength": 1 - }, - }, - "required": ["type", "ethernet_device"], - "additionalProperties": False - }, - "LinuxEthernet": { - "description": "Linux Ethernet Network Input/Output", - "properties": { - "type": { - "enum": ["nio_linux_ethernet"] - }, - "ethernet_device": { - "description": "Ethernet device name e.g. eth0", - "type": "string", - "minLength": 1 - }, - }, - "required": ["type", "ethernet_device"], - "additionalProperties": False - }, "TAP": { "description": "TAP Network Input/Output", "properties": { @@ -120,89 +90,14 @@ VPCS_ADD_NIO_SCHEMA = { "required": ["type", "tap_device"], "additionalProperties": False }, - "UNIX": { - "description": "UNIX Network Input/Output", - "properties": { - "type": { - "enum": ["nio_unix"] - }, - "local_file": { - "description": "path to the UNIX socket file (local)", - "type": "string", - "minLength": 1 - }, - "remote_file": { - "description": "path to the UNIX socket file (remote)", - "type": "string", - "minLength": 1 - }, - }, - "required": ["type", "local_file", "remote_file"], - "additionalProperties": False - }, - "VDE": { - "description": "VDE Network Input/Output", - "properties": { - "type": { - "enum": ["nio_vde"] - }, - "control_file": { - "description": "path to the VDE control file", - "type": "string", - "minLength": 1 - }, - "local_file": { - "description": "path to the VDE control file", - "type": "string", - "minLength": 1 - }, - }, - "required": ["type", "control_file", "local_file"], - "additionalProperties": False - }, - "NULL": { - "description": "NULL Network Input/Output", - "properties": { - "type": { - "enum": ["nio_null"] - }, - }, - "required": ["type"], - "additionalProperties": False - }, }, - "properties": { - "id": { - "description": "VPCS device instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VPCS instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 0 - }, - "nio": { - "type": "object", - "description": "Network Input/Output", - "oneOf": [ - {"$ref": "#/definitions/UDP"}, - {"$ref": "#/definitions/Ethernet"}, - {"$ref": "#/definitions/LinuxEthernet"}, - {"$ref": "#/definitions/TAP"}, - {"$ref": "#/definitions/UNIX"}, - {"$ref": "#/definitions/VDE"}, - {"$ref": "#/definitions/NULL"}, - ] - }, - }, - "additionalProperties": False, - "required": ["id", "port_id", "port", "nio"] + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/TAP"}, + ], + "additionalProperties": True, + "required": ['type'] } VPCS_OBJECT_SCHEMA = { @@ -230,41 +125,3 @@ VPCS_OBJECT_SCHEMA = { "required": ["name", "vpcs_id", "console"] } -VBOX_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new VirtualBox VM instance", - "type": "object", - "properties": { - "name": { - "description": "VirtualBox VM instance name", - "type": "string", - "minLength": 1, - }, - "vbox_id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["name"], -} - - -VBOX_OBJECT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "VirtualBox instance", - "type": "object", - "properties": { - "name": { - "description": "VirtualBox VM name", - "type": "string", - "minLength": 1, - }, - "vbox_id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["name", "vbox_id"] -} diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index ccecd96f..eb65610f 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from tests.api.base import server, loop +from tests.api.base import server, loop, port_manager from tests.utils import asyncio_patch from gns3server import modules @@ -30,16 +30,12 @@ def test_vpcs_create(server): def test_vpcs_nio_create(server): - response = server.post('/vpcs/42/nio', { - 'id': 42, - 'nio': { + response = server.post('/vpcs/42/ports/0/nio', { 'type': 'nio_unix', 'local_file': '/tmp/test', 'remote_file': '/tmp/remote' }, - 'port': 0, - 'port_id': 0}, example=True) assert response.status == 200 - assert response.route == '/vpcs/{vpcs_id}/nio' + assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' assert response.json['name'] == 'PC 2' diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index 7eeb8e82..b56ace3f 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -61,3 +61,19 @@ def test_stop(tmpdir, loop, port_manager): assert vm.is_running() == False process.terminate.assert_called_with() +def test_add_nio_binding_udp(port_manager, tmpdir): + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + assert nio.lport == 4242 + +def test_add_nio_binding_tap(port_manager, tmpdir): + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True): + nio = vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) + assert nio.tap_device == "test" + +def test_add_nio_binding_tap_no_privileged_access(port_manager, tmpdir): + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=False): + with pytest.raises(VPCSError): + vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) From bf6f62e6290582237044703acbc92ed4de2bf5ce Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 17:09:45 +0100 Subject: [PATCH 05/12] Serialize NIO --- .../post_vpcsvpcsidportsportidnio.txt | 19 ++++++++------ gns3server/handlers/vpcs_handler.py | 18 +++++++------ gns3server/modules/base_manager.py | 2 +- gns3server/modules/vpcs/nios/nio_tap.py | 3 +++ gns3server/modules/vpcs/nios/nio_udp.py | 3 +++ gns3server/modules/vpcs/vpcs_device.py | 1 - gns3server/schemas/vpcs.py | 2 +- gns3server/web/response.py | 11 ++++++++ gns3server/web/route.py | 4 +++ tests/api/test_version.py | 2 +- tests/api/test_vpcs.py | 26 ++++++++++++++----- 11 files changed, 65 insertions(+), 26 deletions(-) diff --git a/docs/api/examples/post_vpcsvpcsidportsportidnio.txt b/docs/api/examples/post_vpcsvpcsidportsportidnio.txt index cdcb7a4e..06fbc0fb 100644 --- a/docs/api/examples/post_vpcsvpcsidportsportidnio.txt +++ b/docs/api/examples/post_vpcsvpcsidportsportidnio.txt @@ -1,22 +1,25 @@ -curl -i -xPOST 'http://localhost:8000/vpcs/{vpcs_id}/ports/{port_id}/nio' -d '{"local_file": "/tmp/test", "remote_file": "/tmp/remote", "type": "nio_unix"}' +curl -i -xPOST 'http://localhost:8000/vpcs/{vpcs_id}/ports/{port_id}/nio' -d '{"lport": 4242, "rhost": "127.0.0.1", "rport": 4343, "type": "nio_udp"}' POST /vpcs/{vpcs_id}/ports/{port_id}/nio HTTP/1.1 { - "local_file": "/tmp/test", - "remote_file": "/tmp/remote", - "type": "nio_unix" + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" } -HTTP/1.1 404 +HTTP/1.1 200 CONNECTION: close -CONTENT-LENGTH: 59 +CONTENT-LENGTH: 89 CONTENT-TYPE: application/json DATE: Thu, 08 Jan 2015 16:09:15 GMT SERVER: Python/3.4 aiohttp/0.13.1 X-ROUTE: /vpcs/{vpcs_id}/ports/{port_id}/nio { - "message": "ID 42 doesn't exist", - "status": 404 + "lport": 4242, + "rhost": "127.0.0.1", + "rport": 4343, + "type": "nio_udp" } diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 6fc3495e..b12383b5 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -19,7 +19,7 @@ from ..web.route import Route from ..schemas.vpcs import VPCS_CREATE_SCHEMA from ..schemas.vpcs import VPCS_OBJECT_SCHEMA -from ..schemas.vpcs import VPCS_ADD_NIO_SCHEMA +from ..schemas.vpcs import VPCS_NIO_SCHEMA from ..modules.vpcs import VPCS @@ -48,7 +48,8 @@ class VPCSHandler(object): "vpcs_id": "Id of VPCS instance" }, status_codes={ - 201: "Success of creation of VPCS", + 200: "Success of starting VPCS", + 404: "If VPCS doesn't exist" }, description="Start VPCS", ) @@ -64,7 +65,8 @@ class VPCSHandler(object): "vpcs_id": "Id of VPCS instance" }, status_codes={ - 201: "Success of stopping VPCS", + 200: "Success of stopping VPCS", + 404: "If VPCS doesn't exist" }, description="Stop VPCS", ) @@ -81,17 +83,17 @@ class VPCSHandler(object): }, status_codes={ 201: "Success of creation of NIO", - 409: "Conflict" + 404: "If VPCS doesn't exist" }, description="ADD NIO to a VPCS", - input=VPCS_ADD_NIO_SCHEMA) + input=VPCS_NIO_SCHEMA, + output=VPCS_NIO_SCHEMA) def create_nio(request, response): - # TODO: raise 404 if VPCS not found GET VM can raise an exeption # TODO: response with nio vpcs_manager = VPCS.instance() vm = vpcs_manager.get_vm(int(request.match_info['vpcs_id'])) - vm.port_add_nio_binding(int(request.match_info['port_id']), request.json) + nio = vm.port_add_nio_binding(int(request.match_info['port_id']), request.json) - response.json({'name': "PC 2", "vpcs_id": 42, "console": 4242}) + response.json(nio) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index ab07f427..36d031a6 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -39,7 +39,7 @@ class BaseManager: :returns: instance of Manager """ - if not hasattr(cls, "_instance"): + if not hasattr(cls, "_instance") or cls._instance is None: cls._instance = cls() return cls._instance diff --git a/gns3server/modules/vpcs/nios/nio_tap.py b/gns3server/modules/vpcs/nios/nio_tap.py index 4c3ed6b2..39923a01 100644 --- a/gns3server/modules/vpcs/nios/nio_tap.py +++ b/gns3server/modules/vpcs/nios/nio_tap.py @@ -44,3 +44,6 @@ class NIO_TAP(object): def __str__(self): return "NIO TAP" + + def __json__(self): + return {"type": "nio_tap", "tap_device": self._tap_device} diff --git a/gns3server/modules/vpcs/nios/nio_udp.py b/gns3server/modules/vpcs/nios/nio_udp.py index 0527f675..cca313e7 100644 --- a/gns3server/modules/vpcs/nios/nio_udp.py +++ b/gns3server/modules/vpcs/nios/nio_udp.py @@ -70,3 +70,6 @@ class NIO_UDP(object): def __str__(self): return "NIO UDP" + + def __json__(self): + return {"type": "nio_udp", "lport": self._lport, "rport": self._rport, "rhost": self._rhost} diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 5384a985..fe079c25 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -265,7 +265,6 @@ class VPCSDevice(BaseVM): nio = NIO_UDP(lport, rhost, rport) elif nio_settings["type"] == "nio_tap": tap_device = nio_settings["tap_device"] - print(has_privileged_access) if not has_privileged_access(self._path): raise VPCSError("{} has no privileged access to {}.".format(self._path, tap_device)) nio = NIO_TAP(tap_device) diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index dc5ca6dd..27a5bcd3 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -42,7 +42,7 @@ VPCS_CREATE_SCHEMA = { } -VPCS_ADD_NIO_SCHEMA = { +VPCS_NIO_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Request validation to add a NIO for a VPCS instance", "type": "object", diff --git a/gns3server/web/response.py b/gns3server/web/response.py index 325455f4..d829e725 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -18,7 +18,9 @@ import json import jsonschema import aiohttp.web +import logging +log = logging.getLogger(__name__) class Response(aiohttp.web.Response): @@ -29,13 +31,22 @@ class Response(aiohttp.web.Response): headers['X-Route'] = self._route super().__init__(headers=headers, **kwargs) + """ + Set the response content type to application/json and serialize + the content. + + :param anwser The response as a Python object + """ def json(self, answer): """Pass a Python object and return a JSON as answer""" self.content_type = "application/json" + if hasattr(answer, '__json__'): + answer = answer.__json__() if self._output_schema is not None: try: jsonschema.validate(answer, self._output_schema) except jsonschema.ValidationError as e: + log.error("Invalid output schema") raise aiohttp.web.HTTPBadRequest(text="{}".format(e)) self.body = json.dumps(answer, indent=4, sort_keys=True).encode('utf-8') diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 1687cde9..e55edf06 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -19,6 +19,9 @@ import json import jsonschema import asyncio import aiohttp +import logging + +log = logging.getLogger(__name__) from ..modules.vm_error import VMError from .response import Response @@ -37,6 +40,7 @@ def parse_request(request, input_schema): try: jsonschema.validate(request.json, input_schema) except jsonschema.ValidationError as e: + log.error("Invalid input schema") raise aiohttp.web.HTTPBadRequest(text="Request is not {} '{}' in schema: {}".format( e.validator, e.validator_value, diff --git a/tests/api/test_version.py b/tests/api/test_version.py index a052bb43..8fb46174 100644 --- a/tests/api/test_version.py +++ b/tests/api/test_version.py @@ -21,7 +21,7 @@ It's also used for unittest the HTTP implementation. """ from tests.utils import asyncio_patch -from tests.api.base import server, loop +from tests.api.base import server, loop, port_manager from gns3server.version import __version__ diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index eb65610f..623e1b65 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from unittest.mock import patch from tests.api.base import server, loop, port_manager from tests.utils import asyncio_patch from gns3server import modules @@ -29,13 +30,26 @@ def test_vpcs_create(server): assert response.json['vpcs_id'] == 84 -def test_vpcs_nio_create(server): - response = server.post('/vpcs/42/ports/0/nio', { - 'type': 'nio_unix', - 'local_file': '/tmp/test', - 'remote_file': '/tmp/remote' +def test_vpcs_nio_create_udp(server): + vm = server.post('/vpcs', {'name': 'PC TEST 1'}) + response = server.post('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"]), { + 'type': 'nio_udp', + 'lport': 4242, + 'rport': 4343, + 'rhost': '127.0.0.1' }, example=True) assert response.status == 200 assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' - assert response.json['name'] == 'PC 2' + assert response.json['type'] == 'nio_udp' + +@patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True) +def test_vpcs_nio_create_tap(mock, server): + vm = server.post('/vpcs', {'name': 'PC TEST 1'}) + response = server.post('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"]), { + 'type': 'nio_tap', + 'tap_device': 'test', + }) + assert response.status == 200 + assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' + assert response.json['type'] == 'nio_tap' From 8e307c8cbb69528776efd54728e6b318660f77d8 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 20:23:43 +0100 Subject: [PATCH 06/12] Use PATH environnement variable for searching binary --- gns3server/modules/base_vm.py | 50 ++------------------------ gns3server/modules/vpcs/vpcs_device.py | 26 ++++++++++---- tests/modules/vpcs/test_vpcs_device.py | 21 ++++++----- 3 files changed, 35 insertions(+), 62 deletions(-) diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 19571831..5fe85b64 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -19,74 +19,28 @@ import asyncio from .vm_error import VMError from .attic import find_unused_port +from ..config import Config import logging log = logging.getLogger(__name__) class BaseVM: - _allocated_console_ports = [] - def __init__(self, name, identifier, port_manager): self._loop = asyncio.get_event_loop() - self._allocate_console() self._queue = asyncio.Queue() self._name = name self._id = identifier self._created = asyncio.Future() self._worker = asyncio.async(self._run()) self._port_manager = port_manager + self._config = Config.instance() log.info("{type} device {name} [id={id}] has been created".format( type=self.__class__.__name__, name=self._name, id=self._id)) - def _allocate_console(self): - - if not self._console: - # allocate a console port - try: - self._console = find_unused_port(self._console_start_port_range, - self._console_end_port_range, - self._console_host, - ignore_ports=self._allocated_console_ports) - except Exception as e: - raise VMError(e) - - if self._console in self._allocated_console_ports: - raise VMError("Console port {} is already used by another VM".format(self._console)) - self._allocated_console_ports.append(self._console) - - - @property - def console(self): - """ - Returns the TCP console port. - - :returns: console port (integer) - """ - - return self._console - - @console.setter - def console(self, console): - """ - Sets the TCP console port. - - :param console: console port (integer) - """ - - if console in self._allocated_console_ports: - raise VMError("Console port {} is already used by another VM".format(console)) - - self._allocated_console_ports.remove(self._console) - self._console = console - self._allocated_console_ports.append(self._console) - log.info("{type} {name} [id={id}]: console port set to {port}".format(type=self.__class__.__name__, - name=self._name, - id=self._id, - port=console)) @property def id(self): """ diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index fe079c25..d1531eb2 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -28,6 +28,7 @@ import shutil import re import asyncio import socket +import shutil from pkg_resources import parse_version from .vpcs_error import VPCSError @@ -52,17 +53,17 @@ class VPCSDevice(BaseVM): :param console: TCP console port """ def __init__(self, name, vpcs_id, port_manager, - path = None, - working_dir = None, - console=None): + working_dir = None, console = None): + super().__init__(name, vpcs_id, port_manager) #self._path = path #self._working_dir = working_dir # TODO: Hardcodded for testing - self._path = "/usr/local/bin/vpcs" - self._working_dir = "/tmp" + self._path = self._config.get_section_config("VPCS").get("path", "vpcs") + self._working_dir = "/tmp" self._console = console + self._command = [] self._process = None self._vpcs_stdout_file = "" @@ -89,12 +90,15 @@ class VPCSDevice(BaseVM): raise VPCSError(e) self._check_requirements() - super().__init__(name, vpcs_id, port_manager) def _check_requirements(self): """ Check if VPCS is available with the correct version """ + if self._path == "vpcs": + self._path = shutil.which("vpcs") + + if not self._path: raise VPCSError("No path to a VPCS executable has been set") @@ -106,6 +110,16 @@ class VPCSDevice(BaseVM): self._check_vpcs_version() + @property + def console(self): + """ + Returns the console port of this VPCS device. + + :returns: console port + """ + + return self._console + @property def name(self): """ diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index b56ace3f..f5d71268 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -28,33 +28,38 @@ from gns3server.modules.vpcs.vpcs_error import VPCSError @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8")) def test_vm(tmpdir, port_manager): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) assert vm.name == "test" assert vm.id == 42 @patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8")) def test_vm_invalid_vpcs_version(tmpdir, port_manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) assert vm.name == "test" assert vm.id == 42 +@patch("gns3server.config.Config.get_section_config", return_value = {"path": "/bin/test_fake"}) def test_vm_invalid_vpcs_path(tmpdir, port_manager): with pytest.raises(VPCSError): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test_fake") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) assert vm.name == "test" assert vm.id == 42 def test_start(tmpdir, loop, port_manager): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) + nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() == True def test_stop(tmpdir, loop, port_manager): process = MagicMock() with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) + nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() == True loop.run_until_complete(asyncio.async(vm.stop())) @@ -62,18 +67,18 @@ def test_stop(tmpdir, loop, port_manager): process.terminate.assert_called_with() def test_add_nio_binding_udp(port_manager, tmpdir): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) assert nio.lport == 4242 def test_add_nio_binding_tap(port_manager, tmpdir): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True): nio = vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) assert nio.tap_device == "test" def test_add_nio_binding_tap_no_privileged_access(port_manager, tmpdir): - vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir), path="/bin/test") + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=False): with pytest.raises(VPCSError): vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) From 77db08c39e035ec618c8ca402036f68ad51db4df Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 21:39:58 +0100 Subject: [PATCH 07/12] Remove NIO from VPCS --- gns3server/handlers/vpcs_handler.py | 25 ++++++++++++++++++++++--- gns3server/web/route.py | 4 ++++ tests/api/base.py | 3 +++ tests/api/test_vpcs.py | 15 +++++++++++++++ tests/modules/vpcs/test_vpcs_device.py | 7 +++++++ 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index b12383b5..e34dbfe8 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -79,21 +79,40 @@ class VPCSHandler(object): @Route.post( r"/vpcs/{vpcs_id}/ports/{port_id}/nio", parameters={ - "vpcs_id": "Id of VPCS instance" + "vpcs_id": "Id of VPCS instance", + "port_id": "Id of the port where the nio should be add" }, status_codes={ - 201: "Success of creation of NIO", + 200: "Success of creation of NIO", 404: "If VPCS doesn't exist" }, description="ADD NIO to a VPCS", input=VPCS_NIO_SCHEMA, output=VPCS_NIO_SCHEMA) def create_nio(request, response): - # TODO: response with nio vpcs_manager = VPCS.instance() vm = vpcs_manager.get_vm(int(request.match_info['vpcs_id'])) nio = vm.port_add_nio_binding(int(request.match_info['port_id']), request.json) response.json(nio) + @classmethod + @Route.delete( + r"/vpcs/{vpcs_id}/ports/{port_id}/nio", + parameters={ + "vpcs_id": "Id of VPCS instance", + "port_id": "Id of the port where the nio should be remove" + }, + status_codes={ + 200: "Success of deletin of NIO", + 404: "If VPCS doesn't exist" + }, + description="Remove NIO from a VPCS") + def delete_nio(request, response): + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_vm(int(request.match_info['vpcs_id'])) + nio = vm.port_remove_nio_binding(int(request.match_info['port_id'])) + response.json({}) + + diff --git a/gns3server/web/route.py b/gns3server/web/route.py index e55edf06..086a6b50 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -70,6 +70,10 @@ class Route(object): def put(cls, path, *args, **kw): return cls._route('PUT', path, *args, **kw) + @classmethod + def delete(cls, path, *args, **kw): + return cls._route('DELETE', path, *args, **kw) + @classmethod def _route(cls, method, path, *args, **kw): # This block is executed only the first time diff --git a/tests/api/base.py b/tests/api/base.py index a95d36c2..b8f5f395 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -44,6 +44,9 @@ class Query: def get(self, path, **kwargs): return self._fetch("GET", path, **kwargs) + def delete(self, path, **kwargs): + return self._fetch("DELETE", path, **kwargs) + def _get_url(self, path): return "http://{}:{}{}".format(self._host, self._port, path) diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 623e1b65..d3ea87a1 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -53,3 +53,18 @@ def test_vpcs_nio_create_tap(mock, server): assert response.status == 200 assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' assert response.json['type'] == 'nio_tap' + +def test_vpcs_delete_nio(server): + vm = server.post('/vpcs', {'name': 'PC TEST 1'}) + response = server.post('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"]), { + 'type': 'nio_udp', + 'lport': 4242, + 'rport': 4343, + 'rhost': '127.0.0.1' + }, + ) + response = server.delete('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"])) + assert response.status == 200 + assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' + + diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py index f5d71268..4844e820 100644 --- a/tests/modules/vpcs/test_vpcs_device.py +++ b/tests/modules/vpcs/test_vpcs_device.py @@ -82,3 +82,10 @@ def test_add_nio_binding_tap_no_privileged_access(port_manager, tmpdir): with patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=False): with pytest.raises(VPCSError): vm.port_add_nio_binding(0, {"type": "nio_tap", "tap_device": "test"}) + assert vm._ethernet_adapter.ports[0] is not None + +def test_port_remove_nio_binding(port_manager, tmpdir): + vm = VPCSDevice("test", 42, port_manager, working_dir=str(tmpdir)) + nio = vm.port_add_nio_binding(0, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + vm.port_remove_nio_binding(0) + assert vm._ethernet_adapter.ports[0] == None From 42920e505944e0aa06eda7ae28e3a98240a38df7 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 21:44:56 +0100 Subject: [PATCH 08/12] Haiku theme --- .../examples/delete_vpcsvpcsidportsportidnio.txt | 15 +++++++++++++++ docs/conf.py | 3 ++- tests/api/test_vpcs.py | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 docs/api/examples/delete_vpcsvpcsidportsportidnio.txt diff --git a/docs/api/examples/delete_vpcsvpcsidportsportidnio.txt b/docs/api/examples/delete_vpcsvpcsidportsportidnio.txt new file mode 100644 index 00000000..37bc3fda --- /dev/null +++ b/docs/api/examples/delete_vpcsvpcsidportsportidnio.txt @@ -0,0 +1,15 @@ +curl -i -xDELETE 'http://localhost:8000/vpcs/{vpcs_id}/ports/{port_id}/nio' + +DELETE /vpcs/{vpcs_id}/ports/{port_id}/nio HTTP/1.1 + + + +HTTP/1.1 200 +CONNECTION: close +CONTENT-LENGTH: 2 +CONTENT-TYPE: application/json +DATE: Thu, 08 Jan 2015 16:09:15 GMT +SERVER: Python/3.4 aiohttp/0.13.1 +X-ROUTE: /vpcs/{vpcs_id}/ports/{port_id}/nio + +{} diff --git a/docs/conf.py b/docs/conf.py index 346e586a..563979c4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -101,7 +101,8 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +#html_theme = 'default' +html_theme = 'haiku' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index d3ea87a1..db56a014 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -63,7 +63,7 @@ def test_vpcs_delete_nio(server): 'rhost': '127.0.0.1' }, ) - response = server.delete('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"])) + response = server.delete('/vpcs/{}/ports/0/nio'.format(vm.json["vpcs_id"]), example=True) assert response.status == 200 assert response.route == '/vpcs/{vpcs_id}/ports/{port_id}/nio' From 878532325a0a326ea2b9fce88ff45fed7c5782bf Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 21:48:03 +0100 Subject: [PATCH 09/12] Nature --- docs/api/sleep.rst | 13 ------------- docs/api/stream.rst | 13 ------------- docs/conf.py | 2 +- 3 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 docs/api/sleep.rst delete mode 100644 docs/api/stream.rst diff --git a/docs/api/sleep.rst b/docs/api/sleep.rst deleted file mode 100644 index fbf7845d..00000000 --- a/docs/api/sleep.rst +++ /dev/null @@ -1,13 +0,0 @@ -/sleep ------------------------------- - -.. contents:: - -GET /sleep -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - -Response status codes -************************** -- **200**: OK - diff --git a/docs/api/stream.rst b/docs/api/stream.rst deleted file mode 100644 index 00180c62..00000000 --- a/docs/api/stream.rst +++ /dev/null @@ -1,13 +0,0 @@ -/stream ------------------------------- - -.. contents:: - -GET /stream -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - -Response status codes -************************** -- **200**: OK - diff --git a/docs/conf.py b/docs/conf.py index 563979c4..27de1def 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -102,7 +102,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. #html_theme = 'default' -html_theme = 'haiku' +html_theme = 'nature' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 2c9a802ccab28da0f70e3ed1f7d30620ed08f475 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 21:53:04 +0100 Subject: [PATCH 10/12] Default documentation theme --- docs/conf.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 27de1def..684c7297 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -90,6 +90,7 @@ exclude_patterns = ['_build'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' + # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -101,8 +102,9 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -#html_theme = 'default' -html_theme = 'nature' +html_theme = 'default' +#html_theme = 'nature' +using_rtd_theme=False # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 9f82f3826b9472b6cb0d590e01aacfb05c334e05 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 21:58:02 +0100 Subject: [PATCH 11/12] Default doc style --- docs/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 684c7297..6b87c66e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -104,7 +104,9 @@ pygments_style = 'sphinx' # a list of builtin themes. html_theme = 'default' #html_theme = 'nature' -using_rtd_theme=False + +#If uncommented it's turn off the default read the doc style +html_style = "/default.css" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 869ad026ffaa85b7a3d77cdd3153fe05c15519f8 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Fri, 16 Jan 2015 21:59:51 +0100 Subject: [PATCH 12/12] Do not add a show source in documenation --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 6b87c66e..73bef3c9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -167,7 +167,7 @@ html_static_path = ['_static'] # html_split_index = False # If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True +html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True