diff --git a/gns3server/controller/link.py b/gns3server/controller/link.py
new file mode 100644
index 00000000..37ea0ad9
--- /dev/null
+++ b/gns3server/controller/link.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 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 uuid
+import asyncio
+
+
+class Link:
+ def __init__(self):
+ self._id = str(uuid.uuid4())
+ self._vms = []
+
+ @asyncio.coroutine
+ def addVM(self, vm, adapter_number, port_number):
+ """
+ Add a VM to the link
+ """
+ self._vms.append({
+ "vm": vm,
+ "adapter_number": adapter_number,
+ "port_number": port_number
+ })
+
+ @property
+ def id(self):
+ return self._id
+
+ def __json__(self):
+ res = []
+ for side in self._vms:
+ res.append({
+ "vm_id": side["vm"].id,
+ "adapter_number": side["adapter_number"],
+ "port_number": side["port_number"]
+ })
+ return {"vms": res, "link_id": self._id}
+
diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py
index 49832be1..71f74e72 100644
--- a/gns3server/controller/project.py
+++ b/gns3server/controller/project.py
@@ -16,9 +16,11 @@
# along with this program. If not, see .
import asyncio
+import aiohttp
from uuid import UUID, uuid4
from .vm import VM
+from .link import Link
class Project:
@@ -45,6 +47,7 @@ class Project:
self._temporary = temporary
self._hypervisors = set()
self._vms = {}
+ self._links = {}
@property
def name(self):
@@ -81,6 +84,33 @@ class Project:
return vm
return self._vms[vm_id]
+ def getVM(self, vm_id):
+ """
+ Return the VM or raise a 404 if the VM is unknown
+ """
+ try:
+ return self._vms[vm_id]
+ except KeyError:
+ raise aiohttp.web.HTTPNotFound(text="VM ID {} doesn't exist".format(vm_id))
+
+ @asyncio.coroutine
+ def addLink(self):
+ """
+ Create a link. By default the link is empty
+ """
+ link = Link()
+ self._links[link.id] = link
+ return link
+
+ def getLink(self, link_id):
+ """
+ Return the Link or raise a 404 if the VM is unknown
+ """
+ try:
+ return self._links[link_id]
+ except KeyError:
+ raise aiohttp.web.HTTPNotFound(text="Link ID {} doesn't exist".format(link_id))
+
@asyncio.coroutine
def close(self):
for hypervisor in self._hypervisors:
diff --git a/gns3server/handlers/api/controller/__init__.py b/gns3server/handlers/api/controller/__init__.py
index 5eac84b5..9ddd4f7b 100644
--- a/gns3server/handlers/api/controller/__init__.py
+++ b/gns3server/handlers/api/controller/__init__.py
@@ -19,3 +19,4 @@ from .hypervisor_handler import HypervisorHandler
from .project_handler import ProjectHandler
from .version_handler import VersionHandler
from .vm_handler import VMHandler
+from .link_handler import LinkHandler
diff --git a/gns3server/handlers/api/controller/link_handler.py b/gns3server/handlers/api/controller/link_handler.py
new file mode 100644
index 00000000..e22ad677
--- /dev/null
+++ b/gns3server/handlers/api/controller/link_handler.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 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.link import LINK_OBJECT_SCHEMA
+from ....controller.project import Project
+from ....controller import Controller
+
+
+class LinkHandler:
+ """
+ API entry point for Link
+ """
+
+ @classmethod
+ @Route.post(
+ r"/projects/{project_id}/links",
+ parameters={
+ "project_id": "UUID for the project"
+ },
+ status_codes={
+ 201: "Link created",
+ 400: "Invalid request"
+ },
+ description="Create a new link instance",
+ input=LINK_OBJECT_SCHEMA,
+ output=LINK_OBJECT_SCHEMA)
+ def create(request, response):
+
+ controller = Controller.instance()
+ project = controller.getProject(request.match_info["project_id"])
+ link = yield from project.addLink()
+ for vm in request.json["vms"]:
+ yield from link.addVM(project.getVM(vm["vm_id"]),
+ vm["adapter_number"],
+ vm["port_number"])
+ response.set_status(201)
+ response.json(link)
diff --git a/gns3server/schemas/link.py b/gns3server/schemas/link.py
new file mode 100644
index 00000000..cd71fb4a
--- /dev/null
+++ b/gns3server/schemas/link.py
@@ -0,0 +1,60 @@
+#!/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 .
+
+
+LINK_OBJECT_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "A link object",
+ "type": "object",
+ "properties": {
+ "link_id": {
+ "description": "Link identifier",
+ "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}$"
+ },
+ "vms": {
+ "description": "List of the VMS",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "vm_id": {
+ "description": "VM identifier",
+ "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}$"
+ },
+ "adapter_number": {
+ "description": "Adapter number",
+ "type": "integer"
+ },
+ "port_number": {
+ "description": "Port number",
+ "type": "integer"
+ }
+ },
+ "required": ["vm_id", "adapter_number", "port_number"],
+ "additionalProperties": False
+ }
+ }
+ },
+ "required": ["vms"],
+ "additionalProperties": False
+}
diff --git a/tests/controller/test_link.py b/tests/controller/test_link.py
new file mode 100644
index 00000000..9cdcfc75
--- /dev/null
+++ b/tests/controller/test_link.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2016 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
+
+from gns3server.controller.link import Link
+from gns3server.controller.vm import VM
+from gns3server.controller.hypervisor import Hypervisor
+from gns3server.controller.project import Project
+
+
+@pytest.fixture
+def project():
+ return Project()
+
+
+@pytest.fixture
+def hypervisor():
+ return Hypervisor("example.com")
+
+
+def test_addVM(async_run, project, hypervisor):
+ vm1 = VM(project, hypervisor)
+
+ link = Link()
+ async_run(link.addVM(vm1, 0, 4))
+ assert link._vms == [
+ {
+ "vm": vm1,
+ "adapter_number": 0,
+ "port_number": 4
+ }
+ ]
+
+
+def test_json(async_run, project, hypervisor):
+ vm1 = VM(project, hypervisor)
+ vm2 = VM(project, hypervisor)
+
+ link = Link()
+ async_run(link.addVM(vm1, 0, 4))
+ async_run(link.addVM(vm2, 1, 3))
+ assert link.__json__() == {
+ "link_id": link.id,
+ "vms": [
+ {
+ "vm_id": vm1.id,
+ "adapter_number": 0,
+ "port_number": 4
+ },
+ {
+ "vm_id": vm2.id,
+ "adapter_number": 1,
+ "port_number": 3
+ }
+ ]
+ }
+
diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py
index 5d602f92..10adb62e 100644
--- a/tests/controller/test_project.py
+++ b/tests/controller/test_project.py
@@ -16,6 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+import pytest
+import aiohttp
from unittest.mock import MagicMock
@@ -45,3 +47,34 @@ def test_addVM(async_run):
'console_type': 'telnet',
'startup_config': 'test.cfg',
'name': 'test'})
+
+
+def test_getVM(async_run):
+ hypervisor = MagicMock()
+ project = Project()
+ vm = async_run(project.addVM(hypervisor, None, name="test", vm_type="vpcs", properties={"startup_config": "test.cfg"}))
+ assert project.getVM(vm.id) == vm
+
+ with pytest.raises(aiohttp.web_exceptions.HTTPNotFound):
+ project.getVM("test")
+
+
+def test_addLink(async_run):
+ hypervisor = MagicMock()
+ project = Project()
+ vm1 = async_run(project.addVM(hypervisor, None, name="test1", vm_type="vpcs", properties={"startup_config": "test.cfg"}))
+ vm2 = async_run(project.addVM(hypervisor, None, name="test2", vm_type="vpcs", properties={"startup_config": "test.cfg"}))
+ link = async_run(project.addLink())
+ async_run(link.addVM(vm1, 3, 1))
+ async_run(link.addVM(vm2, 4, 2))
+ assert len(link._vms) == 2
+
+
+def test_getLink(async_run):
+ hypervisor = MagicMock()
+ project = Project()
+ link = async_run(project.addLink())
+ assert project.getLink(link.id) == link
+
+ with pytest.raises(aiohttp.web_exceptions.HTTPNotFound):
+ project.getLink("test")
diff --git a/tests/handlers/api/controller/test_link.py b/tests/handlers/api/controller/test_link.py
new file mode 100644
index 00000000..343a32de
--- /dev/null
+++ b/tests/handlers/api/controller/test_link.py
@@ -0,0 +1,70 @@
+# -*- 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 .
+
+"""
+This test suite check /project endpoint
+"""
+
+import uuid
+import os
+import asyncio
+import aiohttp
+import pytest
+
+
+from unittest.mock import patch, MagicMock, PropertyMock
+from tests.utils import asyncio_patch
+
+from gns3server.handlers.api.controller.project_handler import ProjectHandler
+from gns3server.controller import Controller
+from gns3server.controller.vm import VM
+
+
+@pytest.fixture
+def hypervisor(http_controller, async_run):
+ hypervisor = MagicMock()
+ hypervisor.id = "example.com"
+ Controller.instance()._hypervisors = {"example.com": hypervisor}
+ return hypervisor
+
+
+@pytest.fixture
+def project(http_controller, async_run):
+ return async_run(Controller.instance().addProject())
+
+
+def test_create_link(http_controller, tmpdir, project, hypervisor, async_run):
+ vm1 = async_run(project.addVM(hypervisor, None))
+ vm2 = async_run(project.addVM(hypervisor, None))
+
+ response = http_controller.post("/projects/{}/links".format(project.id), {
+ "vms": [
+ {
+ "vm_id": vm1.id,
+ "adapter_number": 0,
+ "port_number": 3
+ },
+ {
+ "vm_id": vm2.id,
+ "adapter_number": 2,
+ "port_number": 4
+ }
+ ]
+ }, example=True)
+ assert response.status == 201
+ assert response.json["link_id"] is not None
+ assert len(response.json["vms"]) == 2