mirror of
https://github.com/GNS3/gns3-server
synced 2024-11-28 11:18:11 +00:00
Support for x,y,z,symbols,label in node object on controller
https://github.com/GNS3/gns3-gui/issues/1297
This commit is contained in:
parent
3deb555981
commit
dee9d423a3
@ -25,18 +25,18 @@ import os
|
|||||||
from .compute import ComputeConflict
|
from .compute import ComputeConflict
|
||||||
from ..utils.images import images_directories
|
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 project: Project of the node
|
||||||
:param compute: Compute server where the server will run
|
:param compute: Compute server where the server will run
|
||||||
:param node_id: UUID of the node (integer)
|
:param node_id: UUID of the node (integer)
|
||||||
:param node_type: Type of emulator
|
:param node_type: Type of emulator
|
||||||
:param name: Name of the node
|
:param kwargs: Node properties
|
||||||
: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
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if node_id is None:
|
if node_id is None:
|
||||||
@ -44,16 +44,31 @@ class Node:
|
|||||||
else:
|
else:
|
||||||
self._id = node_id
|
self._id = node_id
|
||||||
|
|
||||||
self._name = name
|
|
||||||
self._project = project
|
self._project = project
|
||||||
self._compute = compute
|
self._compute = compute
|
||||||
self._node_type = node_type
|
self._node_type = node_type
|
||||||
self._console = console
|
|
||||||
self._console_type = console_type
|
self._name = None
|
||||||
self._properties = properties
|
self._console = None
|
||||||
|
self._console_type = None
|
||||||
|
self._properties = {}
|
||||||
self._command_line = None
|
self._command_line = None
|
||||||
self._node_directory = None
|
self._node_directory = None
|
||||||
self._status = "stopped"
|
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
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
@ -67,6 +82,12 @@ class Node:
|
|||||||
def name(self):
|
def name(self):
|
||||||
return self._name
|
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
|
@property
|
||||||
def node_type(self):
|
def node_type(self):
|
||||||
return self._node_type
|
return self._node_type
|
||||||
@ -75,14 +96,26 @@ class Node:
|
|||||||
def console(self):
|
def console(self):
|
||||||
return self._console
|
return self._console
|
||||||
|
|
||||||
|
@console.setter
|
||||||
|
def console(self, val):
|
||||||
|
self._console = val
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def console_type(self):
|
def console_type(self):
|
||||||
return self._console_type
|
return self._console_type
|
||||||
|
|
||||||
|
@console_type.setter
|
||||||
|
def console_type(self, val):
|
||||||
|
self._console_type = val
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def properties(self):
|
def properties(self):
|
||||||
return self._properties
|
return self._properties
|
||||||
|
|
||||||
|
@properties.setter
|
||||||
|
def properties(self, val):
|
||||||
|
self._properties = val
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project(self):
|
def project(self):
|
||||||
return self._project
|
return self._project
|
||||||
@ -98,6 +131,48 @@ class Node:
|
|||||||
"""
|
"""
|
||||||
return self._compute.host
|
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
|
@asyncio.coroutine
|
||||||
def create(self):
|
def create(self):
|
||||||
"""
|
"""
|
||||||
@ -110,7 +185,7 @@ class Node:
|
|||||||
try:
|
try:
|
||||||
response = yield from self._compute.post("/projects/{}/{}/nodes".format(self._project.id, self._node_type), data=data)
|
response = yield from self._compute.post("/projects/{}/{}/nodes".format(self._project.id, self._node_type), data=data)
|
||||||
except ComputeConflict as e:
|
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"])
|
res = yield from self._upload_missing_image(self._node_type, e.response["image"])
|
||||||
if not res:
|
if not res:
|
||||||
raise e
|
raise e
|
||||||
@ -120,30 +195,28 @@ class Node:
|
|||||||
trial += 1
|
trial += 1
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def update(self, name=None, console=None, console_type=None, properties={}):
|
def update(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Update the node on the compute server
|
Update the node on the compute server
|
||||||
|
|
||||||
:param node_id: UUID of the node
|
:param kwargs: Node properties
|
||||||
: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
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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()
|
# When updating properties used only on controller we don't need to call the compute
|
||||||
response = yield from self.put(None, data=data)
|
update_compute = False
|
||||||
self.parse_node_response(response.json)
|
|
||||||
|
# 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):
|
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
|
# None properties are not be send. Because it can mean the emulator doesn't support it
|
||||||
for key in list(data.keys()):
|
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]
|
del data[key]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -297,5 +370,10 @@ class Node:
|
|||||||
"console_type": self._console_type,
|
"console_type": self._console_type,
|
||||||
"command_line": self._command_line,
|
"command_line": self._command_line,
|
||||||
"properties": self._properties,
|
"properties": self._properties,
|
||||||
"status": self._status
|
"status": self._status,
|
||||||
|
"label": self._label,
|
||||||
|
"x": self._x,
|
||||||
|
"y": self._y,
|
||||||
|
"z": self._z,
|
||||||
|
"symbol": self._symbol
|
||||||
}
|
}
|
||||||
|
41
gns3server/schemas/label.py
Normal file
41
gns3server/schemas/label.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -15,6 +15,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from .label import LABEL_OBJECT_SCHEMA
|
||||||
|
|
||||||
NODE_LIST_IMAGES_SCHEMA = {
|
NODE_LIST_IMAGES_SCHEMA = {
|
||||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
@ -136,6 +137,24 @@ NODE_OBJECT_SCHEMA = {
|
|||||||
"status": {
|
"status": {
|
||||||
"description": "Status of the node",
|
"description": "Status of the node",
|
||||||
"enum": ["stopped", "started", "suspended"]
|
"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,
|
"additionalProperties": False,
|
||||||
|
@ -60,7 +60,12 @@ def test_json(node, compute):
|
|||||||
"command_line": None,
|
"command_line": None,
|
||||||
"node_directory": None,
|
"node_directory": None,
|
||||||
"properties": node.properties,
|
"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._console = 2048
|
||||||
|
|
||||||
node.__calls = 0
|
node.__calls = 0
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def resp(*args, **kwargs):
|
def resp(*args, **kwargs):
|
||||||
node.__calls += 1
|
node.__calls += 1
|
||||||
@ -113,12 +119,13 @@ def test_create_image_missing(node, compute, project, async_run):
|
|||||||
node._upload_missing_image.called is True
|
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 = MagicMock()
|
||||||
response.json = {"console": 2048}
|
response.json = {"console": 2048}
|
||||||
compute.put = AsyncioMagicMock(return_value=response)
|
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 = {
|
data = {
|
||||||
"console": 2048,
|
"console": 2048,
|
||||||
"console_type": "vnc",
|
"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)
|
compute.put.assert_called_with("/projects/{}/vpcs/nodes/{}".format(node.project.id, node.id), data=data)
|
||||||
assert node._console == 2048
|
assert node._console == 2048
|
||||||
|
assert node.x == 42
|
||||||
assert node._properties == {"startup_script": "echo test"}
|
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):
|
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
|
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)
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user