diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py
index b8ca9ee4..15a996b3 100644
--- a/gns3server/controller/node.py
+++ b/gns3server/controller/node.py
@@ -25,18 +25,18 @@ import os
from .compute import ComputeConflict
from ..utils.images import images_directories
-class Node:
- def __init__(self, project, compute, node_id=None, node_type=None, name=None, console=None, console_type=None, properties={}):
+class Node:
+ # This properties are used only on controller and are not forwarded to the compute
+ CONTROLLER_ONLY_PROPERTIES = ["x", "y", "z", "symbol", "label", "console_host"]
+
+ def __init__(self, project, compute, node_id=None, node_type=None, **kwargs):
"""
:param project: Project of the node
:param compute: Compute server where the server will run
:param node_id: UUID of the node (integer)
:param node_type: Type of emulator
- :param name: Name of the node
- :param console: TCP port of the console
- :param console_type: Type of the console (telnet, vnc, serial..)
- :param properties: Emulator specific properties of the node
+ :param kwargs: Node properties
"""
if node_id is None:
@@ -44,16 +44,31 @@ class Node:
else:
self._id = node_id
- self._name = name
self._project = project
self._compute = compute
self._node_type = node_type
- self._console = console
- self._console_type = console_type
- self._properties = properties
+
+ self._name = None
+ self._console = None
+ self._console_type = None
+ self._properties = {}
self._command_line = None
self._node_directory = None
self._status = "stopped"
+ self._x = 0
+ self._y = 0
+ self._z = 0
+ self._symbol = ":/symbols/computer.svg"
+ self._label = {
+ "color": "#ff000000",
+ "y": -25.0,
+ "text": "",
+ "font": "TypeWriter,10,-1,5,75,0,0,0,0,0",
+ "x": -17.0234375
+ }
+ # Update node properties with additional elements
+ for prop in kwargs:
+ setattr(self, prop, kwargs[prop])
@property
def id(self):
@@ -67,6 +82,12 @@ class Node:
def name(self):
return self._name
+ @name.setter
+ def name(self, val):
+ self._name = val
+ # The text in label need to be always the node name
+ self._label["text"] = val
+
@property
def node_type(self):
return self._node_type
@@ -75,14 +96,26 @@ class Node:
def console(self):
return self._console
+ @console.setter
+ def console(self, val):
+ self._console = val
+
@property
def console_type(self):
return self._console_type
+ @console_type.setter
+ def console_type(self, val):
+ self._console_type = val
+
@property
def properties(self):
return self._properties
+ @properties.setter
+ def properties(self, val):
+ self._properties = val
+
@property
def project(self):
return self._project
@@ -98,6 +131,48 @@ class Node:
"""
return self._compute.host
+ @property
+ def x(self):
+ return self._x
+
+ @x.setter
+ def x(self, val):
+ self._x = val
+
+ @property
+ def y(self):
+ return self._y
+
+ @y.setter
+ def y(self, val):
+ self._y = val
+
+ @property
+ def z(self):
+ return self._z
+
+ @z.setter
+ def z(self, val):
+ self._z = val
+
+ @property
+ def symbol(self):
+ return self._symbol
+
+ @symbol.setter
+ def symbol(self, val):
+ self._symbol = val
+
+ @property
+ def label(self):
+ return self._label
+
+ @label.setter
+ def label(self, val):
+ # The text in label need to be always the node name
+ val["text"] = self._name
+ self._label = val
+
@asyncio.coroutine
def create(self):
"""
@@ -110,7 +185,7 @@ class Node:
try:
response = yield from self._compute.post("/projects/{}/{}/nodes".format(self._project.id, self._node_type), data=data)
except ComputeConflict as e:
- if e.response.get("exception") == "ImageMissingError":
+ if e.response.get("exception") == "ImageMissingError":
res = yield from self._upload_missing_image(self._node_type, e.response["image"])
if not res:
raise e
@@ -120,30 +195,28 @@ class Node:
trial += 1
@asyncio.coroutine
- def update(self, name=None, console=None, console_type=None, properties={}):
+ def update(self, **kwargs):
"""
Update the node on the compute server
- :param node_id: UUID of the node
- :param node_type: Type of emulator
- :param name: Name of the node
- :param console: TCP port of the console
- :param console_type: Type of the console (telnet, vnc, serial..)
- :param properties: Emulator specific properties of the node
-
+ :param kwargs: Node properties
"""
- if name:
- self._name = name
- if console:
- self._console = console
- if console_type:
- self._console_type = console_type
- if properties != {}:
- self._properties = properties
- data = self._node_data()
- response = yield from self.put(None, data=data)
- self.parse_node_response(response.json)
+ # When updating properties used only on controller we don't need to call the compute
+ update_compute = False
+
+ # Update node properties with additional elements
+ for prop in kwargs:
+ if getattr(self, prop) != kwargs[prop]:
+ if prop not in self.CONTROLLER_ONLY_PROPERTIES:
+ update_compute = True
+ setattr(self, prop, kwargs[prop])
+
+ self.project.controller.notification.emit("node.updated", self.__json__())
+ if update_compute:
+ data = self._node_data()
+ response = yield from self.put(None, data=data)
+ self.parse_node_response(response.json)
def parse_node_response(self, response):
"""
@@ -181,7 +254,7 @@ class Node:
# None properties are not be send. Because it can mean the emulator doesn't support it
for key in list(data.keys()):
- if data[key] is None or key in ["console_host"]:
+ if data[key] is None or data[key] is {} or key in self.CONTROLLER_ONLY_PROPERTIES:
del data[key]
return data
@@ -297,5 +370,10 @@ class Node:
"console_type": self._console_type,
"command_line": self._command_line,
"properties": self._properties,
- "status": self._status
+ "status": self._status,
+ "label": self._label,
+ "x": self._x,
+ "y": self._y,
+ "z": self._z,
+ "symbol": self._symbol
}
diff --git a/gns3server/schemas/label.py b/gns3server/schemas/label.py
new file mode 100644
index 00000000..84719dd2
--- /dev/null
+++ b/gns3server/schemas/label.py
@@ -0,0 +1,41 @@
+#!/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 .
+
+LABEL_OBJECT_SCHEMA = {
+ "type": "object",
+ "properties": {
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9a-f]{6,8}$"
+ },
+ "font": {
+ "type": "string",
+ "minLength": 1
+ },
+ "text": {"type": "string"},
+ "x": {"type": "number"},
+ "y": {"type": "number"},
+ "z": {"type": "number"},
+ "rotation": {"type": "number"}
+ },
+ "required": [
+ "text",
+ "x",
+ "y"
+ ],
+ "additionalProperties": False
+}
diff --git a/gns3server/schemas/node.py b/gns3server/schemas/node.py
index bbb2c103..a18da641 100644
--- a/gns3server/schemas/node.py
+++ b/gns3server/schemas/node.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 .label import LABEL_OBJECT_SCHEMA
NODE_LIST_IMAGES_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
@@ -136,6 +137,24 @@ NODE_OBJECT_SCHEMA = {
"status": {
"description": "Status of the node",
"enum": ["stopped", "started", "suspended"]
+ },
+ "label": LABEL_OBJECT_SCHEMA,
+ "symbol": {
+ "description": "Symbol of the node",
+ "type": "string",
+ "minLength": 1
+ },
+ "x": {
+ "description": "X position of the node",
+ "type": "number"
+ },
+ "y": {
+ "description": "Y position of the node",
+ "type": "number"
+ },
+ "z": {
+ "description": "Z position of the node",
+ "type": "number"
}
},
"additionalProperties": False,
diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py
index a001ad2a..39786ae4 100644
--- a/tests/controller/test_node.py
+++ b/tests/controller/test_node.py
@@ -60,7 +60,12 @@ def test_json(node, compute):
"command_line": None,
"node_directory": None,
"properties": node.properties,
- "status": node.status
+ "status": node.status,
+ "x": node.x,
+ "y": node.y,
+ "z": node.z,
+ "symbol": node.symbol,
+ "label": node.label
}
@@ -95,6 +100,7 @@ def test_create_image_missing(node, compute, project, async_run):
node._console = 2048
node.__calls = 0
+
@asyncio.coroutine
def resp(*args, **kwargs):
node.__calls += 1
@@ -113,12 +119,13 @@ def test_create_image_missing(node, compute, project, async_run):
node._upload_missing_image.called is True
-def test_update(node, compute, project, async_run):
+def test_update(node, compute, project, async_run, controller):
response = MagicMock()
response.json = {"console": 2048}
compute.put = AsyncioMagicMock(return_value=response)
+ controller._notification = AsyncioMagicMock()
- async_run(node.update(console=2048, console_type="vnc", properties={"startup_script": "echo test"}, name="demo"))
+ async_run(node.update(x=42, console=2048, console_type="vnc", properties={"startup_script": "echo test"}, name="demo"))
data = {
"console": 2048,
"console_type": "vnc",
@@ -127,7 +134,38 @@ def test_update(node, compute, project, async_run):
}
compute.put.assert_called_with("/projects/{}/vpcs/nodes/{}".format(node.project.id, node.id), data=data)
assert node._console == 2048
+ assert node.x == 42
assert node._properties == {"startup_script": "echo test"}
+ controller._notification.emit.assert_called_with("node.updated", node.__json__())
+
+
+def test_update_only_controller(node, compute, project, async_run):
+ """
+ When updating property used only on controller we don't need to
+ call the compute
+ """
+ compute.put = AsyncioMagicMock()
+
+ async_run(node.update(x=42))
+ assert not compute.put.called
+ assert node.x == 42
+
+
+def test_update_no_changes(node, compute, project, async_run):
+ """
+ We don't call the compute node if all compute properties has not changed
+ """
+ response = MagicMock()
+ response.json = {"console": 2048}
+ compute.put = AsyncioMagicMock(return_value=response)
+
+ async_run(node.update(console=2048, x=42))
+ assert compute.put.called
+
+ compute.put = AsyncioMagicMock()
+ async_run(node.update(console=2048, x=43))
+ assert not compute.put.called
+ assert node.x == 43
def test_start(node, compute, project, async_run):
@@ -229,3 +267,14 @@ def test_upload_missing_image(compute, controller, async_run, images_dir):
assert async_run(node._upload_missing_image("qemu", "linux.img")) is True
compute.post.assert_called_with("/qemu/images/linux.img", data=ANY, timeout=None)
+
+def test_update_label(node):
+ """
+ The text in label need to be always the
+ node name
+ """
+ node.name = "Test"
+ assert node.label["text"] == "Test"
+ node.label = {"text": "Wrong", "x": 12}
+ assert node.label["text"] == "Test"
+ assert node.label["x"] == 12