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