Files
gns3-gui/gns3/node.py
2017-03-07 18:10:15 +01:00

552 lines
18 KiB
Python

# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 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/>.
import os
import uuid
import pathlib
from gns3.local_server import LocalServer
from gns3.controller import Controller
from gns3.ports.ethernet_port import EthernetPort
from gns3.ports.serial_port import SerialPort
from gns3.qt import QtGui, QtCore
from .base_node import BaseNode
import logging
log = logging.getLogger(__name__)
class Node(BaseNode):
def __init__(self, module, compute, project):
super().__init__(module, compute, project)
self._node_id = str(uuid.uuid4())
self._node_directory = None
self._command_line = None
self._always_on = False
# minimum required base settings
self._settings = {"name": "", "x": None, "y": None, "z": 1}
def get(self, path, *args, **kwargs):
return self.controllerHttpGet("/nodes/{node_id}{path}".format(node_id=self._node_id, path=path), *args, **kwargs)
def post(self, path, *args, **kwargs):
return self.controllerHttpPost("/nodes/{node_id}{path}".format(node_id=self._node_id, path=path), *args, **kwargs)
def importFile(self, path, source_path):
self.post("/files/{path}".format(path=path), self._importFileCallback, body=pathlib.Path(source_path), timeout=None)
def _importFileCallback(self, result, error=False, **kwargs):
if error:
log.error("Error while importing file: {}".format(result["message"]))
self.server_error_signal.emit(self.id(), result["message"])
return False
def exportFile(self, path, output_path):
self.get("/files/{path}".format(path=path), self._exportFileCallback, context={"path": output_path}, raw=True)
def _exportFileCallback(self, result, error=False, raw_body=None, context={}, **kwargs):
if not error:
with open(context["path"], "wb+") as f:
f.write(raw_body)
def settings(self):
return self._settings
def setSettingValue(self, key, value):
"""
Set settings
"""
self._settings[key] = value
def setGraphics(self, node_item):
"""
Sync the remote object with the node_item
"""
data = {
"x": int(node_item.pos().x()),
"y": int(node_item.pos().y()),
"z": int(node_item.zValue()),
"symbol": node_item.symbol()
}
if node_item.label() is not None:
data["label"] = node_item.label().dump()
# Send the change of if stuff changed
changed = False
for key in data:
if key not in self._settings or self._settings[key] != data[key]:
changed = True
if not changed:
return
# If it's the initialization we don't resend it
# to the server
if self._settings["x"] is not None:
self._update(data)
else:
self._settings.update(data)
def setSymbol(self, symbol):
self._settings["symbol"] = symbol
def symbol(self):
return self._settings["symbol"]
def setPos(self, x, y):
self._settings["x"] = int(x)
self._settings["y"] = int(y)
def x(self):
return self._settings["x"]
def y(self):
return self._settings["y"]
def z(self):
return self._settings["z"]
def isAlwaysOn(self):
"""
Whether the node is always on.
:returns: boolean
"""
return self._always_on
def consoleCommand(self, console_type=None):
"""
:returns: The console command for this host
"""
from .main_window import MainWindow
general_settings = MainWindow.instance().settings()
if console_type != "telnet":
console_type = self.consoleType()
if console_type == "vnc":
return general_settings["vnc_console_command"]
return general_settings["telnet_console_command"]
def consoleType(self):
"""
Get the console type (serial, telnet or VNC)
"""
console_type = "telnet"
if "console_type" in self.settings():
return self.settings()["console_type"]
return console_type
def consoleHost(self):
host = self.settings()["console_host"]
if host is None or host == "::" or host == "0.0.0.0":
host = Controller.instance().host()
return host
def node_id(self):
"""
Return the ID of this device
:returns: identifier (string)
"""
return self._node_id
def nodeDir(self):
"""
Return the working directory of this node
:returns: identifier (string)
"""
return self._node_directory
def commandLine(self):
"""
Return the command line used to run this node
:returns: identifier (string)
"""
return self._command_line
def _prepareBody(self, params):
"""
:returns: Body for Create and update
"""
assert self._node_id is not None
body = {"properties": {},
"node_type": self.URL_PREFIX,
"node_id": self._node_id,
"compute_id": self._compute.id()}
# We have two kind of properties. The general properties common to all
# nodes and the specific that we need to put in the properties field
node_general_properties = ("name", "console", "console_type", "x", "y", "z", "symbol", "label", "port_name_format", "port_segment_size", "first_port_name")
# No need to send this back to the server because it's read only
ignore_properties = ("console_host", "symbol_url", "width", "height", "node_id")
for key, value in params.items():
if key in node_general_properties:
body[key] = value
elif key in ignore_properties:
pass
else:
body["properties"][key] = value
return body
def _update(self, params, timeout=60):
"""
Update the node on the controller
"""
if self.initialized():
log.debug("{} is updating settings: {}".format(self.name(), params))
body = self._prepareBody(params)
self.controllerHttpPut("/nodes/{node_id}".format(node_id=self._node_id), self.updateNodeCallback, body=body, timeout=timeout)
def updateNodeCallback(self, result, error=False, **kwargs):
"""
Callback for update.
:param result: server response (dict)
:param error: indicates an error (boolean)
"""
if error:
log.error("error while updating {}: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["message"])
return False
result = self._parseResponse(result)
self._updateCallback(result)
self.updated_signal.emit()
return True
def _parseResponse(self, result):
"""
Parse node object from API
"""
if "node_id" in result:
self._node_id = result["node_id"]
if "name" in result:
self.setName(result["name"])
if "command_line" in result:
self._command_line = result["command_line"]
if "node_directory" in result:
self._node_directory = result["node_directory"]
if "status" in result:
if result["status"] == "started":
self.setStatus(Node.started)
elif result["status"] == "stopped":
self.setStatus(Node.stopped)
elif result["status"] == "suspended":
self.setStatus(Node.suspended)
if "ports" in result:
self._updatePorts(result["ports"])
if "properties" in result:
for name, value in result["properties"].items():
if name.startswith("slot") or name.startswith("wic"):
pass
elif name in self._settings and self._settings[name] != value:
log.debug("{} setting up and updating {} from '{}' to '{}'".format(self.name(), name, self._settings[name], value))
self._settings[name] = value
result.update(result["properties"])
del result["properties"]
# Update common element of all nodes
for key in ["x", "y", "z", "symbol", "label", "console_host", "console", "console_type"]:
if key in result:
self._settings[key] = result[key]
return result
def _updatePorts(self, ports):
self._settings["ports"] = ports
old_ports = self._ports.copy()
self._ports = []
for port in ports:
new_port = None
# Update port if already exist
for old_port in old_ports:
if old_port.adapterNumber() == port["adapter_number"] and old_port.portNumber() == port["port_number"] and old_port.name() == port["name"]:
new_port = old_port
old_ports.remove(old_port)
break
if new_port is None:
if port["link_type"] == "serial":
new_port = SerialPort(port["name"])
else:
new_port = EthernetPort(port["name"])
new_port.setShortName(port["short_name"])
new_port.setAdapterNumber(port["adapter_number"])
new_port.setPortNumber(port["port_number"])
new_port.setDataLinkTypes(port["data_link_types"])
new_port.setStatus(self.status())
self._ports.append(new_port)
def createNodeCallback(self, result, error=False, **kwargs):
"""
Callback for create.
:param result: server response
:param error: indicates an error (boolean)
:returns: Boolean success or not
"""
if error:
self.server_error_signal.emit(self.id(), "Error while setting up node: {}".format(result["message"]))
self.deleted_signal.emit()
self._module.removeNode(self)
return False
result = self._parseResponse(result)
self._created = True
self._createCallback(result)
if self._loading:
self.loaded_signal.emit()
else:
self.setInitialized(True)
log.info("Node instance {} has been created".format(self.name()))
self.created_signal.emit(self.id())
self._module.addNode(self)
def _createCallback(self, result):
"""
Create callback compatible with the compute api.
"""
pass
def _updateCallback(self, result):
"""
Update callback compatible with the compute api.
"""
pass
def delete(self, skip_controller=False):
"""
Deletes this node instance.
:param skip_controller: True to not delete on the controller (often it's when it's already deleted on the server)
"""
log.info("{} is being deleted".format(self.name()))
if not skip_controller:
self.controllerHttpDelete("/nodes/{node_id}".format(node_id=self._node_id), self._deleteCallback)
else:
self.deleted_signal.emit()
self._module.removeNode(self)
def _deleteCallback(self, result, error=False, **kwargs):
"""
Callback for delete.
:param result: server response (dict)
:param error: indicates an error (boolean)
"""
if error:
log.error("error while deleting {}: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["message"])
log.info("{} has been deleted".format(self.name()))
self.deleted_signal.emit()
self._module.removeNode(self)
def isStarted(self):
"""
:returns: Boolean True if started
"""
return self.status() == Node.started
def start(self):
"""
Starts this node instance.
"""
if self.isStarted():
log.debug("{} is already running".format(self.name()))
return
log.debug("{} is starting".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/start".format(node_id=self._node_id), self._startCallback, progressText="{} is starting".format(self.name()))
def _startCallback(self, result, error=False, **kwargs):
"""
Callback for start.
:param result: server response (dict)
:param error: indicates an error (boolean)
"""
if error:
log.error("error while starting {}: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["message"])
else:
self._parseResponse(result)
def stop(self):
"""
Stops this node instance.
"""
if self.status() == Node.stopped:
log.debug("{} is already stopped".format(self.name()))
return
log.debug("{} is stopping".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/stop".format(node_id=self._node_id), self._stopCallback, progressText="{} is stopping".format(self.name()))
def _stopCallback(self, result, error=False, **kwargs):
"""
Callback for stop.
:param result: server response (dict)
:param error: indicates an error (boolean)
"""
if error:
log.error("error while stopping {}: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["message"])
# To avoid blocking the client we consider node as stopped if the node no longer exists or server doesn't answer
if "status" not in result or result["status"] == 404:
self.setStatus(Node.stopped)
else:
self._parseResponse(result)
def suspend(self):
"""
Suspends this router.
"""
if self.status() == Node.suspended:
log.debug("{} is already suspended".format(self.name()))
return
log.debug("{} is being suspended".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/suspend".format(node_id=self._node_id), self._suspendCallback)
def _suspendCallback(self, result, error=False, **kwargs):
"""
Callback for suspend.
:param result: server response (dict)
:param error: indicates an error (boolean)
"""
if error:
log.error("error while suspending {}: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["message"])
else:
self._parseResponse(result)
def reload(self):
"""
Reloads this node instance.
"""
log.debug("{} is being reloaded".format(self.name()))
self.controllerHttpPost("/nodes/{node_id}/reload".format(node_id=self._node_id), self._reloadCallback)
def _reloadCallback(self, result, error=False, **kwargs):
"""
Callback for reload.
:param result: server response (dict)
:param error: indicates an error (boolean)
"""
if error:
log.error("error while reloading {}: {}".format(self.name(), result["message"]))
self.server_error_signal.emit(self.id(), result["message"])
else:
log.info("{} has reloaded".format(self.name()))
def openConsole(self, command=None, aux=False):
if command is None:
if aux:
command = self.consoleCommand(console_type="telnet")
else:
command = self.consoleCommand()
console_type = "telnet"
if aux:
console_port = self.auxConsole()
if console_port is None:
raise ValueError("AUX console port not allocated for {}".format(self.name()))
# Aux console is always telnet
console_type = "telnet"
else:
console_port = self.console()
if "console_type" in self.settings():
console_type = self.settings()["console_type"]
if console_type == "telnet":
from .telnet_console import nodeTelnetConsole
nodeTelnetConsole(self, console_port, command)
elif console_type == "vnc":
from .vnc_console import vncConsole
vncConsole(self.consoleHost(), console_port, command)
elif console_type == "http" or console_type == "https":
QtGui.QDesktopServices.openUrl(QtCore.QUrl("{console_type}://{host}:{port}{path}".format(console_type=console_type, host=self.consoleHost(), port=console_port, path=self.consoleHttpPath())))
def setName(self, name):
"""
Set a name for a node.
:param name: node name
"""
self._settings["name"] = name
def name(self):
"""
Returns the name of this node.
:returns: name (string)
"""
return self._settings["name"]
def settings(self):
"""
Returns all the node settings.
:returns: settings dictionary
"""
return self._settings