From 986c63f3446dae84802729996fc0e8ef006f18d5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 11 Feb 2015 15:37:05 +0100 Subject: [PATCH] HTTP api start iou process Now we need to start ioucon --- gns3server/handlers/__init__.py | 3 +- gns3server/handlers/iou_handler.py | 180 +++++++++++++++++++++++++++++ gns3server/modules/iou/iou_vm.py | 54 ++++----- gns3server/schemas/iou.py | 127 ++++++++++++++++++++ tests/api/test_iou.py | 97 ++++++++++++++++ 5 files changed, 433 insertions(+), 28 deletions(-) create mode 100644 gns3server/handlers/iou_handler.py create mode 100644 gns3server/schemas/iou.py create mode 100644 tests/api/test_iou.py diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 727b4053..78bb3ca6 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -3,4 +3,5 @@ __all__ = ["version_handler", "vpcs_handler", "project_handler", "virtualbox_handler", - "dynamips_handler"] + "dynamips_handler", + "iou_handler"] diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py new file mode 100644 index 00000000..70ccc6b6 --- /dev/null +++ b/gns3server/handlers/iou_handler.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# 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 . + +from ..web.route import Route +from ..schemas.iou import IOU_CREATE_SCHEMA +from ..schemas.iou import IOU_UPDATE_SCHEMA +from ..schemas.iou import IOU_OBJECT_SCHEMA +from ..modules.iou import IOU + + +class IOUHandler: + + """ + API entry points for IOU. + """ + + @classmethod + @Route.post( + r"/projects/{project_id}/iou/vms", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + description="Create a new IOU instance", + input=IOU_CREATE_SCHEMA, + output=IOU_OBJECT_SCHEMA) + def create(request, response): + + iou = IOU.instance() + vm = yield from iou.create_vm(request.json["name"], + request.match_info["project_id"], + request.json.get("vm_id"), + console=request.json.get("console"), + ) + vm.iou_path = request.json.get("iou_path", vm.iou_path) + vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) + response.set_status(201) + response.json(vm) + + @classmethod + @Route.get( + r"/projects/{project_id}/iou/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Success", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Get a IOU instance", + output=IOU_OBJECT_SCHEMA) + def show(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + response.json(vm) + + @classmethod + @Route.put( + r"/projects/{project_id}/iou/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Instance updated", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Conflict" + }, + description="Update a IOU instance", + input=IOU_UPDATE_SCHEMA, + output=IOU_OBJECT_SCHEMA) + def update(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + vm.name = request.json.get("name", vm.name) + vm.console = request.json.get("console", vm.console) + vm.iou_path = request.json.get("iou_path", vm.iou_path) + vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) + response.json(vm) + + @classmethod + @Route.delete( + r"/projects/{project_id}/iou/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Delete a IOU instance") + def delete(request, response): + + yield from IOU.instance().delete_vm(request.match_info["vm_id"]) + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/start", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a IOU instance") + def start(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.start() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/stop", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a IOU instance") + def stop(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.stop() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/reload", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + }, + status_codes={ + 204: "Instance reloaded", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Reload a IOU instance") + def reload(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.reload() + response.set_status(204) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 5abf0b44..9651e210 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -63,7 +63,7 @@ class IOUVM(BaseVM): self._iou_stdout_file = "" self._started = False self._iou_path = None - self._iourc = None + self._iourc_path = None self._ioucon_thread = None # IOU settings @@ -124,13 +124,24 @@ class IOUVM(BaseVM): raise IOUError("IOU image '{}' is not executable".format(self._iou_path)) @property - def iourc(self): + def iourc_path(self): """ Returns the path to the iourc file. :returns: path to the iourc file """ - return self._iourc + return self._iourc_path + + @iourc_path.setter + def iourc_path(self, path): + """ + Set path to IOURC file + """ + + self._iourc_path = path + log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name, + id=self._id, + path=self._iourc_path)) @property def use_default_iou_values(self): @@ -154,18 +165,6 @@ class IOUVM(BaseVM): else: log.info("IOU {name} [id={id}]: does not use the default IOU image values".format(name=self._name, id=self._id)) - @iourc.setter - def iourc(self, iourc): - """ - Sets the path to the iourc file. - :param iourc: path to the iourc file. - """ - - self._iourc = iourc - log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name, - id=self._id, - path=self._iourc)) - def _check_requirements(self): """ Check if IOUYAP is available @@ -186,6 +185,8 @@ class IOUVM(BaseVM): "vm_id": self.id, "console": self._console, "project_id": self.project.id, + "iourc_path": self._iourc_path, + "iou_path": self.iou_path } @property @@ -229,7 +230,7 @@ class IOUVM(BaseVM): def application_id(self): return self._manager.get_application_id(self.id) - #TODO: ASYNCIO + # TODO: ASYNCIO def _library_check(self): """ Checks for missing shared library dependencies in the IOU image. @@ -257,9 +258,9 @@ class IOUVM(BaseVM): if not self.is_running(): # TODO: ASYNC - #self._library_check() + # self._library_check() - if not self._iourc or not os.path.isfile(self._iourc): + if not self._iourc_path or not os.path.isfile(self._iourc_path): raise IOUError("A valid iourc file is necessary to start IOU") iouyap_path = self.iouyap_path @@ -269,18 +270,18 @@ class IOUVM(BaseVM): self._create_netmap_config() # created a environment variable pointing to the iourc file. env = os.environ.copy() - env["IOURC"] = self._iourc + env["IOURC"] = self._iourc_path self._command = self._build_command() try: log.info("Starting IOU: {}".format(self._command)) self._iou_stdout_file = os.path.join(self.working_dir, "iou.log") log.info("Logging to {}".format(self._iou_stdout_file)) with open(self._iou_stdout_file, "w") as fd: - self._iou_process = yield from asyncio.create_subprocess_exec(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self.working_dir, - env=env) + self._iou_process = yield from asyncio.create_subprocess_exec(*self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self.working_dir, + env=env) log.info("IOU instance {} started PID={}".format(self._id, self._iou_process.pid)) self._started = True except FileNotFoundError as e: @@ -291,10 +292,9 @@ class IOUVM(BaseVM): raise IOUError("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) # start console support - #self._start_ioucon() + # self._start_ioucon() # connections support - #self._start_iouyap() - + # self._start_iouyap() @asyncio.coroutine def stop(self): diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py new file mode 100644 index 00000000..562ac0f7 --- /dev/null +++ b/gns3server/schemas/iou.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# +# 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 . + + +IOU_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new IOU instance", + "type": "object", + "properties": { + "name": { + "description": "IOU VM name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "IOU VM identifier", + "oneOf": [ + {"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": "integer"} # for legacy projects + ] + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": ["integer", "null"] + }, + "iou_path": { + "description": "Path of iou binary", + "type": "string" + }, + "iourc_path": { + "description": "Path of iourc", + "type": "string" + }, + }, + "additionalProperties": False, + "required": ["name", "iou_path", "iourc_path"] +} + +IOU_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a IOU instance", + "type": "object", + "properties": { + "name": { + "description": "IOU VM name", + "type": ["string", "null"], + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": ["integer", "null"] + }, + "iou_path": { + "description": "Path of iou binary", + "type": "string" + }, + "iourc_path": { + "description": "Path of iourc", + "type": "string" + }, + }, + "additionalProperties": False, +} + +IOU_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "IOU instance", + "type": "object", + "properties": { + "name": { + "description": "IOU VM name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "IOU VM 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}$" + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "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}$" + }, + "iou_path": { + "description": "Path of iou binary", + "type": "string" + }, + "iourc_path": { + "description": "Path of iourc", + "type": "string" + }, + }, + "additionalProperties": False, + "required": ["name", "vm_id", "console", "project_id", "iou_path", "iourc_path"] +} diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py new file mode 100644 index 00000000..5dd3268c --- /dev/null +++ b/tests/api/test_iou.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# +# 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 . + +import pytest +import os +import stat +from tests.utils import asyncio_patch +from unittest.mock import patch + + +@pytest.fixture +def fake_iou_bin(tmpdir): + """Create a fake IOU image on disk""" + + path = str(tmpdir / "iou.bin") + with open(path, "w+") as f: + f.write('\x7fELF\x01\x01\x01') + os.chmod(path, stat.S_IREAD | stat.S_IEXEC) + return path + +@pytest.fixture +def base_params(tmpdir, fake_iou_bin): + """Return standard parameters""" + return {"name": "PC TEST 1", "iou_path": fake_iou_bin, "iourc_path": str(tmpdir / "iourc")} + + +@pytest.fixture +def vm(server, project, base_params): + response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), base_params) + assert response.status == 201 + return response.json + + +def test_iou_create(server, project, base_params): + response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), base_params, example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/iou/vms" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + + +def test_iou_get(server, project, vm): + response = server.get("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + assert response.status == 200 + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + + +def test_iou_start(server, vm): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.start", return_value=True) as mock: + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_iou_stop(server, vm): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.stop", return_value=True) as mock: + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_iou_reload(server, vm): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.reload", return_value=True) as mock: + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_iou_delete(server, vm): + with asyncio_patch("gns3server.modules.iou.IOU.delete_vm", return_value=True) as mock: + response = server.delete("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_iou_update(server, vm, tmpdir, free_console_port): + response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", + "console": free_console_port}) + assert response.status == 200 + assert response.json["name"] == "test" + assert response.json["console"] == free_console_port