#!/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 aiohttp import asyncio import copy import uuid import os from .compute import ComputeConflict from ..utils.images import images_directories from ..utils.qt import qt_font_to_style import logging log = logging.getLogger(__name__) class Node: # This properties are used only on controller and are not forwarded to the compute CONTROLLER_ONLY_PROPERTIES = ["x", "y", "z", "width", "height", "symbol", "label", "console_host"] def __init__(self, project, compute, name, node_id=None, node_type=None, **kwargs): """ :param project: Project of the node :param compute: Compute server where the server will run :param name: Node name :param node_id: UUID of the node (integer) :param node_type: Type of emulator :param kwargs: Node properties """ assert node_type if node_id is None: self._id = str(uuid.uuid4()) else: self._id = node_id self._project = project self._compute = compute self._node_type = node_type self._label = None self._name = None self.name = name 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 = None # Update node properties with additional elements # This properties will be recompute ignore_properties = ("width", "height") for prop in kwargs: if prop not in ignore_properties: try: setattr(self, prop, kwargs[prop]) except AttributeError as e: log.critical("Can't set attribute %s", prop) raise e if self._symbol is None: self.symbol = ":/symbols/computer.svg" @property def id(self): return self._id @property def status(self): return self._status @property def name(self): return self._name @name.setter def name(self, new_name): self._name = self._project.update_node_name(self, new_name) # The text in label need to be always the node name if self.label and self._label["text"] != self._name: self._label["text"] = self._name self._label["x"] = None # Center text @property def node_type(self): return self._node_type @property 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 @property def compute(self): return self._compute @property def host(self): """ :returns: Domain or ip for console connection """ 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 width(self): return self._width @property def height(self): return self._height @property def symbol(self): return self._symbol @symbol.setter def symbol(self, val): self._symbol = val try: self._width, self._height, filetype = self._project.controller.symbols.get_size(val) # If symbol is invalid we replace it by default except (ValueError, OSError): self.symbol = ":/symbols/computer.svg" if self._label is None: # Apply to label user style or default try: style = qt_font_to_style( self._project.controller.settings["GraphicsView"]["default_label_font"], self._project.controller.settings["GraphicsView"]["default_label_color"]) except KeyError: style = "font-size: 10;font-familly: Verdana" self._label = { "y": round(self._height / 2 + 10) * -1, "text": self._name, "style": style, "x": None, # None: mean the client should center it "rotation": 0 } @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): """ Create the node on the compute server """ data = self._node_data() data["node_id"] = self._id trial = 0 while trial != 6: 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": res = yield from self._upload_missing_image(self._node_type, e.response["image"]) if not res: raise e else: raise e else: self.parse_node_response(response.json) return True trial += 1 @asyncio.coroutine def update(self, **kwargs): """ Update the node on the compute server :param kwargs: Node properties """ # When updating properties used only on controller we don't need to call the compute update_compute = False old_json = self.__json__() compute_properties = None # 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 # We update properties on the compute and wait for the anwser from the compute node if prop == "properties": compute_properties = kwargs[prop] else: setattr(self, prop, kwargs[prop]) # We send notif only if object has changed if old_json != self.__json__(): self.project.controller.notification.emit("node.updated", self.__json__()) if update_compute: data = self._node_data(properties=compute_properties) response = yield from self.put(None, data=data) self.parse_node_response(response.json) self.project.dump() def parse_node_response(self, response): """ Update the object with the remote node object """ for key, value in response.items(): if key == "console": self._console = value elif key == "node_directory": self._node_directory = value elif key == "command_line": self._command_line = value elif key == "status": self._status = value elif key == "console_type": self._console_type = value elif key == "name": self.name = value elif key in ["node_id", "project_id", "console_host"]: pass else: self._properties[key] = value def _node_data(self, properties=None): """ Prepare node data to send to the remote controller :param properties: If properties is None use actual property otherwise use the parameter """ if properties: data = copy.copy(properties) else: data = copy.copy(self._properties) data["name"] = self._name if self._console: # console is optional for builtin nodes data["console"] = self._console if self._console_type: data["console_type"] = self._console_type # 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 data[key] is {} or key in self.CONTROLLER_ONLY_PROPERTIES: del data[key] return data @asyncio.coroutine def destroy(self): yield from self.delete() @asyncio.coroutine def start(self): """ Start a node """ yield from self.post("/start") @asyncio.coroutine def stop(self): """ Stop a node """ try: yield from self.post("/stop") # We don't care if a compute is down at this step except (aiohttp.errors.ClientOSError, aiohttp.errors.ClientRequestError, aiohttp.web.HTTPNotFound, aiohttp.web.HTTPConflict): pass @asyncio.coroutine def suspend(self): """ Suspend a node """ yield from self.post("/suspend") @asyncio.coroutine def reload(self): """ Suspend a node """ yield from self.post("/reload") @asyncio.coroutine def post(self, path, data=None): """ HTTP post on the node """ if data: return (yield from self._compute.post("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path), data=data)) else: return (yield from self._compute.post("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path))) @asyncio.coroutine def put(self, path, data=None): """ HTTP post on the node """ if path is None: path = "/projects/{}/{}/nodes/{}".format(self._project.id, self._node_type, self._id) else: path = "/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path) if data: return (yield from self._compute.put(path, data=data)) else: return (yield from self._compute.put(path)) @asyncio.coroutine def delete(self, path=None): """ HTTP post on the node """ if path is None: return (yield from self._compute.delete("/projects/{}/{}/nodes/{}".format(self._project.id, self._node_type, self._id))) else: return (yield from self._compute.delete("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path))) @asyncio.coroutine def _upload_missing_image(self, type, img): """ Search an image on local computer and upload it to remote compute if the image exists """ for directory in images_directories(type): image = os.path.join(directory, img) if os.path.exists(image): self.project.controller.notification.emit("log.info", {"message": "Uploading missing image {}".format(img)}) with open(image, 'rb') as f: yield from self._compute.post("/{}/images/{}".format(self._node_type, os.path.basename(img)), data=f, timeout=None) self.project.controller.notification.emit("log.info", {"message": "Upload finished for {}".format(img)}) return True return False @asyncio.coroutine def dynamips_auto_idlepc(self): """ Compute the idle PC for a dynamips node """ return (yield from self._compute.get("/projects/{}/{}/nodes/{}/auto_idlepc".format(self._project.id, self._node_type, self._id), timeout=240)).json @asyncio.coroutine def dynamips_idlepc_proposals(self): """ Compute a list of potential idle PC """ return (yield from self._compute.get("/projects/{}/{}/nodes/{}/idlepc_proposals".format(self._project.id, self._node_type, self._id), timeout=240)).json def __repr__(self): return "".format(self._node_type, self._name) def __eq__(self, other): if not isinstance(other, Node): return False return self.id == other.id and other.project.id == self.project.id def __json__(self, topology_dump=False): """ :param topology_dump: Filter to keep only properties require for saving on disk """ if topology_dump: return { "compute_id": str(self._compute.id), "node_id": self._id, "node_type": self._node_type, "name": self._name, "console": self._console, "console_type": self._console_type, "properties": self._properties, "label": self._label, "x": self._x, "y": self._y, "z": self._z, "width": self._width, "height": self._height, "symbol": self._symbol } return { "compute_id": str(self._compute.id), "project_id": self._project.id, "node_id": self._id, "node_type": self._node_type, "node_directory": self._node_directory, "name": self._name, "console": self._console, "console_host": str(self._compute.host), "console_type": self._console_type, "command_line": self._command_line, "properties": self._properties, "status": self._status, "label": self._label, "x": self._x, "y": self._y, "z": self._z, "width": self._width, "height": self._height, "symbol": self._symbol }