2016-03-03 17:02:27 +02:00
|
|
|
#!/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/>.
|
|
|
|
|
2016-08-25 20:14:29 +03:00
|
|
|
import ipaddress
|
2016-03-10 11:32:07 +02:00
|
|
|
import aiohttp
|
|
|
|
import asyncio
|
2018-10-16 11:56:06 +03:00
|
|
|
import async_timeout
|
2016-08-22 19:49:25 +03:00
|
|
|
import socket
|
2016-03-10 11:32:07 +02:00
|
|
|
import json
|
2016-05-25 15:10:03 +03:00
|
|
|
import uuid
|
2016-08-30 10:58:37 +03:00
|
|
|
import sys
|
2016-06-07 20:38:01 +03:00
|
|
|
import io
|
2017-05-05 21:09:51 +03:00
|
|
|
from operator import itemgetter
|
2016-03-03 17:02:27 +02:00
|
|
|
|
2016-05-14 03:48:10 +03:00
|
|
|
from ..utils import parse_version
|
2018-10-15 13:05:49 +03:00
|
|
|
from ..utils.asyncio import locking
|
2020-10-02 09:37:50 +03:00
|
|
|
from ..controller.controller_error import (
|
|
|
|
ControllerError,
|
|
|
|
ControllerNotFoundError,
|
|
|
|
ControllerForbiddenError,
|
|
|
|
ControllerTimeoutError,
|
|
|
|
ControllerUnauthorizedError)
|
2018-08-22 12:54:43 +03:00
|
|
|
from ..version import __version__, __version_info__
|
2016-03-04 17:58:53 +02:00
|
|
|
|
2016-03-04 17:11:31 +02:00
|
|
|
|
2016-03-04 17:55:59 +02:00
|
|
|
import logging
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
2016-03-04 17:11:31 +02:00
|
|
|
|
2016-04-15 18:57:06 +03:00
|
|
|
class ComputeError(ControllerError):
|
2016-03-04 17:11:31 +02:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
2020-10-02 09:37:50 +03:00
|
|
|
# FIXME: broken
|
|
|
|
class ComputeConflict(ComputeError):
|
2016-06-07 20:38:01 +03:00
|
|
|
"""
|
|
|
|
Raise when the compute send a 409 that we can handle
|
|
|
|
|
|
|
|
:param response: The response of the compute
|
|
|
|
"""
|
2016-06-08 15:14:03 +03:00
|
|
|
|
2016-06-07 20:38:01 +03:00
|
|
|
def __init__(self, response):
|
2020-10-02 09:37:50 +03:00
|
|
|
super().__init__(response["message"])
|
2016-06-07 20:38:01 +03:00
|
|
|
self.response = response
|
|
|
|
|
|
|
|
|
2016-04-15 18:57:06 +03:00
|
|
|
class Compute:
|
2016-03-03 17:02:27 +02:00
|
|
|
"""
|
2016-04-15 18:57:06 +03:00
|
|
|
A GNS3 compute.
|
2016-03-03 17:02:27 +02:00
|
|
|
"""
|
|
|
|
|
2020-10-22 08:49:44 +03:00
|
|
|
def __init__(self, compute_id, controller=None, protocol="http", host="localhost", port=3080, user=None,
|
|
|
|
password=None, name=None, console_host=None):
|
|
|
|
|
2016-07-22 12:43:14 +03:00
|
|
|
self._http_session = None
|
2016-03-18 17:55:54 +02:00
|
|
|
assert controller is not None
|
2016-04-15 18:57:06 +03:00
|
|
|
log.info("Create compute %s", compute_id)
|
2016-05-25 15:10:03 +03:00
|
|
|
|
|
|
|
if compute_id is None:
|
|
|
|
self._id = str(uuid.uuid4())
|
|
|
|
else:
|
|
|
|
self._id = compute_id
|
|
|
|
|
2016-05-25 12:27:41 +03:00
|
|
|
self.protocol = protocol
|
2016-10-26 19:32:01 +03:00
|
|
|
self._console_host = console_host
|
2016-05-25 12:27:41 +03:00
|
|
|
self.host = host
|
|
|
|
self.port = port
|
2016-03-16 16:55:07 +02:00
|
|
|
self._user = None
|
|
|
|
self._password = None
|
2016-03-03 17:02:27 +02:00
|
|
|
self._connected = False
|
2019-04-14 13:42:20 +03:00
|
|
|
self._notifications = None
|
2016-08-29 18:36:24 +03:00
|
|
|
self._closed = False # Close mean we are destroying the compute node
|
2016-03-18 17:55:54 +02:00
|
|
|
self._controller = controller
|
2016-05-12 00:19:00 +03:00
|
|
|
self._set_auth(user, password)
|
2020-10-02 09:37:50 +03:00
|
|
|
self._cpu_usage_percent = 0
|
|
|
|
self._memory_usage_percent = 0
|
|
|
|
self._disk_usage_percent = 0
|
2018-08-22 12:54:43 +03:00
|
|
|
self._last_error = None
|
2016-08-29 16:53:10 +03:00
|
|
|
self._capabilities = {
|
2020-10-02 09:37:50 +03:00
|
|
|
"version": "",
|
|
|
|
"platform": "",
|
|
|
|
"cpus": 0,
|
|
|
|
"memory": 0,
|
|
|
|
"disk_size": 0,
|
2016-08-29 16:53:10 +03:00
|
|
|
"node_types": []
|
|
|
|
}
|
2016-05-23 12:20:52 +03:00
|
|
|
self.name = name
|
2016-08-25 20:14:29 +03:00
|
|
|
# Cache of interfaces on remote host
|
|
|
|
self._interfaces_cache = None
|
2016-12-15 22:57:59 +02:00
|
|
|
self._connection_failure = 0
|
|
|
|
|
2016-07-20 13:43:23 +03:00
|
|
|
def _session(self):
|
|
|
|
if self._http_session is None or self._http_session.closed is True:
|
2020-10-22 08:49:44 +03:00
|
|
|
connector = aiohttp.TCPConnector(force_close=True)
|
|
|
|
self._http_session = aiohttp.ClientSession(connector=connector)
|
2016-07-20 13:43:23 +03:00
|
|
|
return self._http_session
|
|
|
|
|
2016-05-12 00:19:00 +03:00
|
|
|
def _set_auth(self, user, password):
|
2016-03-16 16:55:07 +02:00
|
|
|
"""
|
|
|
|
Set authentication parameters
|
|
|
|
"""
|
2016-05-26 11:11:11 +03:00
|
|
|
if user is None or len(user.strip()) == 0:
|
|
|
|
self._user = None
|
|
|
|
self._password = None
|
2016-03-16 16:55:07 +02:00
|
|
|
self._auth = None
|
2016-05-26 11:11:11 +03:00
|
|
|
else:
|
|
|
|
self._user = user.strip()
|
2016-05-26 14:32:52 +03:00
|
|
|
if password:
|
|
|
|
self._password = password.strip()
|
2017-07-20 16:42:07 +03:00
|
|
|
try:
|
|
|
|
self._auth = aiohttp.BasicAuth(self._user, self._password, "utf-8")
|
|
|
|
except ValueError as e:
|
|
|
|
log.error(str(e))
|
2016-05-26 14:32:52 +03:00
|
|
|
else:
|
|
|
|
self._password = None
|
|
|
|
self._auth = aiohttp.BasicAuth(self._user, "")
|
2016-03-16 16:55:07 +02:00
|
|
|
|
2018-08-28 11:42:06 +03:00
|
|
|
def set_last_error(self, msg):
|
|
|
|
"""
|
|
|
|
Set the last error message for this compute.
|
|
|
|
|
|
|
|
:param msg: message
|
|
|
|
"""
|
|
|
|
self._last_error = msg
|
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def interfaces(self):
|
2016-08-25 20:14:29 +03:00
|
|
|
"""
|
|
|
|
Get the list of network on compute
|
|
|
|
"""
|
|
|
|
if not self._interfaces_cache:
|
2018-10-15 13:05:49 +03:00
|
|
|
response = await self.get("/network/interfaces")
|
2016-08-25 20:14:29 +03:00
|
|
|
self._interfaces_cache = response.json
|
|
|
|
return self._interfaces_cache
|
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def update(self, **kwargs):
|
2016-05-25 12:27:41 +03:00
|
|
|
for kw in kwargs:
|
2016-10-27 17:54:05 +03:00
|
|
|
if kw not in ("user", "password"):
|
|
|
|
setattr(self, kw, kwargs[kw])
|
|
|
|
# It's important to set user and password at the same time
|
|
|
|
if "user" in kwargs or "password" in kwargs:
|
|
|
|
self._set_auth(kwargs.get("user", self._user), kwargs.get("password", self._password))
|
2018-10-16 11:56:06 +03:00
|
|
|
if self._http_session and not self._http_session.closed:
|
|
|
|
await self._http_session.close()
|
2016-05-25 12:27:41 +03:00
|
|
|
self._connected = False
|
2018-08-16 17:31:56 +03:00
|
|
|
self._controller.notification.controller_emit("compute.updated", self.__json__())
|
2016-06-08 15:25:11 +03:00
|
|
|
self._controller.save()
|
2016-05-25 12:27:41 +03:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def close(self):
|
2019-04-14 12:48:12 +03:00
|
|
|
|
2016-06-02 14:44:12 +03:00
|
|
|
self._connected = False
|
2018-10-16 11:56:06 +03:00
|
|
|
if self._http_session and not self._http_session.closed:
|
|
|
|
await self._http_session.close()
|
2019-04-14 12:48:12 +03:00
|
|
|
try:
|
2019-04-14 13:42:20 +03:00
|
|
|
if self._notifications:
|
|
|
|
await self._notifications
|
2019-04-14 12:48:12 +03:00
|
|
|
except asyncio.CancelledError:
|
|
|
|
pass
|
2016-08-29 18:36:24 +03:00
|
|
|
self._closed = True
|
2016-06-02 14:44:12 +03:00
|
|
|
|
2016-05-23 12:20:52 +03:00
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""
|
|
|
|
:returns: Compute name
|
|
|
|
"""
|
|
|
|
return self._name
|
|
|
|
|
|
|
|
@name.setter
|
|
|
|
def name(self, name):
|
|
|
|
if name is not None:
|
|
|
|
self._name = name
|
|
|
|
else:
|
2016-05-25 14:58:29 +03:00
|
|
|
if self._user:
|
|
|
|
user = self._user
|
|
|
|
# Due to random user generated by 1.4 it's common to have a very long user
|
|
|
|
if len(user) > 14:
|
|
|
|
user = user[:11] + "..."
|
|
|
|
self._name = "{}://{}@{}:{}".format(self._protocol, user, self._host, self._port)
|
|
|
|
else:
|
|
|
|
self._name = "{}://{}:{}".format(self._protocol, self._host, self._port)
|
2016-05-23 12:20:52 +03:00
|
|
|
|
2016-05-11 17:31:16 +03:00
|
|
|
@property
|
|
|
|
def connected(self):
|
|
|
|
"""
|
|
|
|
:returns: True if compute node is connected
|
|
|
|
"""
|
|
|
|
return self._connected
|
|
|
|
|
2016-03-03 17:02:27 +02:00
|
|
|
@property
|
|
|
|
def id(self):
|
|
|
|
"""
|
2016-04-15 18:57:06 +03:00
|
|
|
:returns: Compute identifier (string)
|
2016-03-03 17:02:27 +02:00
|
|
|
"""
|
|
|
|
return self._id
|
|
|
|
|
|
|
|
@property
|
|
|
|
def host(self):
|
|
|
|
"""
|
2016-04-15 18:57:06 +03:00
|
|
|
:returns: Compute host (string)
|
2016-03-03 17:02:27 +02:00
|
|
|
"""
|
|
|
|
return self._host
|
|
|
|
|
2016-08-25 20:14:29 +03:00
|
|
|
@property
|
|
|
|
def host_ip(self):
|
|
|
|
"""
|
|
|
|
Return the IP associated to the host
|
|
|
|
"""
|
2017-02-06 17:47:40 +02:00
|
|
|
try:
|
|
|
|
return socket.gethostbyname(self._host)
|
|
|
|
except socket.gaierror:
|
|
|
|
return '0.0.0.0'
|
2016-08-25 20:14:29 +03:00
|
|
|
|
2016-05-25 12:27:41 +03:00
|
|
|
@host.setter
|
|
|
|
def host(self, host):
|
|
|
|
self._host = host
|
2016-10-26 19:32:01 +03:00
|
|
|
if self._console_host is None:
|
|
|
|
self._console_host = host
|
2016-05-25 12:27:41 +03:00
|
|
|
|
2016-10-26 15:43:47 +03:00
|
|
|
@property
|
|
|
|
def console_host(self):
|
|
|
|
return self._console_host
|
|
|
|
|
2016-04-19 16:35:50 +03:00
|
|
|
@property
|
|
|
|
def port(self):
|
|
|
|
"""
|
|
|
|
:returns: Compute port (integer)
|
|
|
|
"""
|
|
|
|
return self._port
|
|
|
|
|
2016-05-25 12:27:41 +03:00
|
|
|
@port.setter
|
|
|
|
def port(self, port):
|
|
|
|
self._port = port
|
|
|
|
|
2016-04-19 16:35:50 +03:00
|
|
|
@property
|
|
|
|
def protocol(self):
|
|
|
|
"""
|
|
|
|
:returns: Compute protocol (string)
|
|
|
|
"""
|
|
|
|
return self._protocol
|
|
|
|
|
2016-05-25 12:27:41 +03:00
|
|
|
@protocol.setter
|
|
|
|
def protocol(self, protocol):
|
|
|
|
self._protocol = protocol
|
|
|
|
|
2016-03-16 16:55:07 +02:00
|
|
|
@property
|
|
|
|
def user(self):
|
|
|
|
return self._user
|
|
|
|
|
|
|
|
@user.setter
|
|
|
|
def user(self, value):
|
2016-05-12 00:19:00 +03:00
|
|
|
self._set_auth(value, self._password)
|
2016-03-16 16:55:07 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def password(self):
|
|
|
|
return self._password
|
|
|
|
|
2016-06-02 02:50:31 +03:00
|
|
|
@password.setter
|
2016-03-16 16:55:07 +02:00
|
|
|
def password(self, value):
|
2016-05-12 00:19:00 +03:00
|
|
|
self._set_auth(self._user, value)
|
2016-03-16 16:55:07 +02:00
|
|
|
|
2016-06-30 10:45:11 +03:00
|
|
|
@property
|
|
|
|
def cpu_usage_percent(self):
|
|
|
|
return self._cpu_usage_percent
|
|
|
|
|
|
|
|
@property
|
|
|
|
def memory_usage_percent(self):
|
|
|
|
return self._memory_usage_percent
|
|
|
|
|
2020-07-19 07:46:07 +03:00
|
|
|
@property
|
|
|
|
def disk_usage_percent(self):
|
|
|
|
return self._disk_usage_percent
|
|
|
|
|
2016-06-15 16:12:38 +03:00
|
|
|
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": self._id,
|
|
|
|
"name": self._name,
|
|
|
|
"protocol": self._protocol,
|
|
|
|
"host": self._host,
|
2016-06-15 19:22:11 +03:00
|
|
|
"port": self._port
|
2016-06-15 16:12:38 +03:00
|
|
|
}
|
2016-03-03 17:02:27 +02:00
|
|
|
return {
|
2016-04-15 18:57:06 +03:00
|
|
|
"compute_id": self._id,
|
2016-05-23 12:20:52 +03:00
|
|
|
"name": self._name,
|
2016-03-03 17:02:27 +02:00
|
|
|
"protocol": self._protocol,
|
|
|
|
"host": self._host,
|
|
|
|
"port": self._port,
|
|
|
|
"user": self._user,
|
2016-06-30 10:45:11 +03:00
|
|
|
"connected": self._connected,
|
|
|
|
"cpu_usage_percent": self._cpu_usage_percent,
|
2016-08-29 16:53:10 +03:00
|
|
|
"memory_usage_percent": self._memory_usage_percent,
|
2020-07-19 07:46:07 +03:00
|
|
|
"disk_usage_percent": self._disk_usage_percent,
|
2018-08-22 12:54:43 +03:00
|
|
|
"capabilities": self._capabilities,
|
|
|
|
"last_error": self._last_error
|
2016-03-03 17:02:27 +02:00
|
|
|
}
|
2016-03-10 11:32:07 +02:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def download_file(self, project, path):
|
2016-07-21 21:17:36 +03:00
|
|
|
"""
|
|
|
|
Read file of a project and download it
|
|
|
|
|
|
|
|
:param project: A project object
|
|
|
|
:param path: The path of the file in the project
|
|
|
|
:returns: A file stream
|
|
|
|
"""
|
|
|
|
|
|
|
|
url = self._getUrl("/projects/{}/files/{}".format(project.id, path))
|
2018-10-15 13:05:49 +03:00
|
|
|
response = await self._session().request("GET", url, auth=self._auth)
|
2016-07-21 21:17:36 +03:00
|
|
|
if response.status == 404:
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerNotFoundError("{} not found on compute".format(path))
|
2016-09-19 17:51:15 +03:00
|
|
|
return response
|
2016-07-21 21:17:36 +03:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def download_image(self, image_type, image):
|
2017-11-13 23:12:39 +02:00
|
|
|
"""
|
|
|
|
Read file of a project and download it
|
|
|
|
|
|
|
|
:param image_type: Image type
|
|
|
|
:param image: The path of the image
|
|
|
|
:returns: A file stream
|
|
|
|
"""
|
|
|
|
|
|
|
|
url = self._getUrl("/{}/images/{}".format(image_type, image))
|
2018-10-15 13:05:49 +03:00
|
|
|
response = await self._session().request("GET", url, auth=self._auth)
|
2017-11-13 23:12:39 +02:00
|
|
|
if response.status == 404:
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerNotFoundError("{} not found on compute".format(image))
|
2017-11-13 23:12:39 +02:00
|
|
|
return response
|
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def http_query(self, method, path, data=None, dont_connect=False, **kwargs):
|
2016-12-15 22:57:59 +02:00
|
|
|
"""
|
|
|
|
:param dont_connect: If true do not reconnect if not connected
|
|
|
|
"""
|
2018-01-10 11:22:55 +02:00
|
|
|
|
2016-12-15 22:57:59 +02:00
|
|
|
if not self._connected and not dont_connect:
|
2016-09-01 16:36:41 +03:00
|
|
|
if self._id == "vm" and not self._controller.gns3vm.running:
|
2018-10-15 13:05:49 +03:00
|
|
|
await self._controller.gns3vm.start()
|
|
|
|
await self.connect()
|
2016-12-15 22:57:59 +02:00
|
|
|
if not self._connected and not dont_connect:
|
2018-01-10 11:22:55 +02:00
|
|
|
raise ComputeError("Cannot connect to compute '{}' with request {} {}".format(self._name, method, path))
|
2018-10-15 13:05:49 +03:00
|
|
|
response = await self._run_http_query(method, path, data=data, **kwargs)
|
2016-04-14 13:22:10 +03:00
|
|
|
return response
|
2016-03-18 17:55:54 +02:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def _try_reconnect(self):
|
2017-02-06 18:56:08 +02:00
|
|
|
"""
|
|
|
|
We catch error during reconnect
|
|
|
|
"""
|
|
|
|
try:
|
2018-10-15 13:05:49 +03:00
|
|
|
await self.connect()
|
2020-10-02 09:37:50 +03:00
|
|
|
except ControllerError:
|
2017-02-06 18:56:08 +02:00
|
|
|
pass
|
|
|
|
|
2018-08-25 10:10:47 +03:00
|
|
|
@locking
|
2018-10-15 13:05:49 +03:00
|
|
|
async def connect(self):
|
2016-03-18 17:55:54 +02:00
|
|
|
"""
|
|
|
|
Check if remote server is accessible
|
|
|
|
"""
|
2018-01-10 11:22:55 +02:00
|
|
|
|
2018-08-12 11:49:48 +03:00
|
|
|
if not self._connected and not self._closed and self.host:
|
2016-08-29 16:53:10 +03:00
|
|
|
try:
|
2018-01-10 11:22:55 +02:00
|
|
|
log.info("Connecting to compute '{}'".format(self._id))
|
2018-10-15 13:05:49 +03:00
|
|
|
response = await self._run_http_query("GET", "/capabilities")
|
2018-01-10 11:22:55 +02:00
|
|
|
except ComputeError as e:
|
2018-08-09 12:59:10 +03:00
|
|
|
log.warning("Cannot connect to compute '{}': {}".format(self._id, e))
|
2020-06-01 13:10:53 +03:00
|
|
|
# Try to reconnect after 5 seconds if server unavailable only if not during tests (otherwise we create a ressource usage bomb)
|
2016-08-30 10:58:37 +03:00
|
|
|
if not hasattr(sys, "_called_from_test") or not sys._called_from_test:
|
2020-06-01 13:10:53 +03:00
|
|
|
if self.id != "local" and self.id != "vm" and not self._controller.compute_has_open_project(self):
|
|
|
|
log.warning("Not reconnecting to compute '{}' because there is no project opened on it".format(self._id))
|
2018-08-09 12:59:10 +03:00
|
|
|
return
|
2016-12-15 22:57:59 +02:00
|
|
|
self._connection_failure += 1
|
|
|
|
# After 5 failure we close the project using the compute to avoid sync issues
|
2018-08-09 12:59:10 +03:00
|
|
|
if self._connection_failure == 10:
|
|
|
|
log.error("Could not connect to compute '{}' after multiple attempts: {}".format(self._id, e))
|
2018-10-15 13:05:49 +03:00
|
|
|
await self._controller.close_compute_projects(self)
|
2020-06-01 13:10:53 +03:00
|
|
|
asyncio.get_event_loop().call_later(5, lambda: asyncio.ensure_future(self._try_reconnect()))
|
2016-08-29 16:53:10 +03:00
|
|
|
return
|
2016-11-15 19:28:32 +02:00
|
|
|
except aiohttp.web.HTTPNotFound:
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerNotFoundError("The server {} is not a GNS3 server or it's a 1.X server".format(self._id))
|
2016-12-12 12:27:18 +02:00
|
|
|
except aiohttp.web.HTTPUnauthorized:
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerUnauthorizedError("Invalid auth for server {}".format(self._id))
|
2017-02-06 11:49:09 +02:00
|
|
|
except aiohttp.web.HTTPServiceUnavailable:
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerNotFoundError("The server {} is unavailable".format(self._id))
|
2017-03-13 17:59:42 +02:00
|
|
|
except ValueError:
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ComputeError("Invalid server url for server {}".format(self._id))
|
2016-04-20 17:24:30 +03:00
|
|
|
|
2016-03-16 16:55:07 +02:00
|
|
|
if "version" not in response.json:
|
2018-08-22 12:54:43 +03:00
|
|
|
msg = "The server {} is not a GNS3 server".format(self._id)
|
|
|
|
log.error(msg)
|
2018-10-17 13:32:10 +03:00
|
|
|
await self._http_session.close()
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerNotFoundError(msg)
|
2016-08-29 16:53:10 +03:00
|
|
|
self._capabilities = response.json
|
2018-08-22 12:54:43 +03:00
|
|
|
|
|
|
|
if response.json["version"].split("-")[0] != __version__.split("-")[0]:
|
2018-09-28 16:04:38 +03:00
|
|
|
if self._name.startswith("GNS3 VM"):
|
|
|
|
msg = "GNS3 version {} is not the same as the GNS3 VM version {}. Please upgrade the GNS3 VM.".format(__version__,
|
|
|
|
response.json["version"])
|
|
|
|
else:
|
2018-11-27 13:14:51 +02:00
|
|
|
msg = "GNS3 controller version {} is not the same as compute {} version {}".format(__version__,
|
|
|
|
self._name,
|
|
|
|
response.json["version"])
|
2018-08-22 12:54:43 +03:00
|
|
|
if __version_info__[3] == 0:
|
|
|
|
# Stable release
|
|
|
|
log.error(msg)
|
2018-10-17 13:32:10 +03:00
|
|
|
await self._http_session.close()
|
2018-08-22 12:54:43 +03:00
|
|
|
self._last_error = msg
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerError(msg)
|
2018-08-22 12:54:43 +03:00
|
|
|
elif parse_version(__version__)[:2] != parse_version(response.json["version"])[:2]:
|
|
|
|
# We don't allow different major version to interact even with dev build
|
|
|
|
log.error(msg)
|
2018-10-17 13:32:10 +03:00
|
|
|
await self._http_session.close()
|
2018-08-22 12:54:43 +03:00
|
|
|
self._last_error = msg
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerError(msg)
|
2018-08-22 12:54:43 +03:00
|
|
|
else:
|
|
|
|
msg = "{}\nUsing different versions may result in unexpected problems. Please use at your own risk.".format(msg)
|
2019-03-15 08:14:55 +02:00
|
|
|
self._controller.notification.controller_emit("log.warning", {"message": msg})
|
2016-03-16 16:55:07 +02:00
|
|
|
|
2016-07-21 16:10:11 +03:00
|
|
|
self._notifications = asyncio.gather(self._connect_notification())
|
2016-03-18 17:55:54 +02:00
|
|
|
self._connected = True
|
2016-12-15 22:57:59 +02:00
|
|
|
self._connection_failure = 0
|
2018-08-22 12:54:43 +03:00
|
|
|
self._last_error = None
|
2018-08-16 17:31:56 +03:00
|
|
|
self._controller.notification.controller_emit("compute.updated", self.__json__())
|
2016-03-18 17:55:54 +02:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def _connect_notification(self):
|
2016-03-18 17:55:54 +02:00
|
|
|
"""
|
|
|
|
Connect to the notification stream
|
|
|
|
"""
|
2018-10-16 11:56:06 +03:00
|
|
|
|
2019-04-14 12:48:12 +03:00
|
|
|
ws_url = self._getUrl("/notifications/ws")
|
|
|
|
try:
|
|
|
|
async with self._session().ws_connect(ws_url, auth=self._auth, heartbeat=10) as ws:
|
2020-07-09 13:43:21 +03:00
|
|
|
log.info("Connected to compute '{}' WebSocket '{}'".format(self._id, ws_url))
|
2019-04-14 12:48:12 +03:00
|
|
|
async for response in ws:
|
|
|
|
if response.type == aiohttp.WSMsgType.TEXT:
|
|
|
|
msg = json.loads(response.data)
|
|
|
|
action = msg.pop("action")
|
|
|
|
event = msg.pop("event")
|
|
|
|
project_id = msg.pop("project_id", None)
|
|
|
|
if action == "ping":
|
|
|
|
self._cpu_usage_percent = event["cpu_usage_percent"]
|
|
|
|
self._memory_usage_percent = event["memory_usage_percent"]
|
2020-07-19 07:46:07 +03:00
|
|
|
self._disk_usage_percent = event["disk_usage_percent"]
|
2019-04-14 12:48:12 +03:00
|
|
|
#FIXME: slow down number of compute events
|
|
|
|
self._controller.notification.controller_emit("compute.updated", self.__json__())
|
|
|
|
else:
|
|
|
|
await self._controller.notification.dispatch(action, event, project_id=project_id, compute_id=self.id)
|
2018-10-16 11:56:06 +03:00
|
|
|
else:
|
2019-04-14 12:48:12 +03:00
|
|
|
if response.type == aiohttp.WSMsgType.CLOSE:
|
|
|
|
await ws.close()
|
|
|
|
elif response.type == aiohttp.WSMsgType.ERROR:
|
2020-07-09 13:43:21 +03:00
|
|
|
log.error("Error received on compute '{}' WebSocket '{}': {}".format(self._id, ws_url, ws.exception()))
|
2019-04-14 12:48:12 +03:00
|
|
|
elif response.type == aiohttp.WSMsgType.CLOSED:
|
|
|
|
pass
|
|
|
|
break
|
2020-10-22 08:49:44 +03:00
|
|
|
except aiohttp.ClientError as e:
|
2020-07-09 13:43:21 +03:00
|
|
|
log.error("Client response error received on compute '{}' WebSocket '{}': {}".format(self._id, ws_url,e))
|
2019-04-14 12:48:12 +03:00
|
|
|
finally:
|
2020-03-19 09:32:01 +02:00
|
|
|
self._connected = False
|
2020-07-09 13:43:21 +03:00
|
|
|
log.info("Connection closed to compute '{}' WebSocket '{}'".format(self._id, ws_url))
|
2019-04-14 12:48:12 +03:00
|
|
|
|
|
|
|
# Try to reconnect after 1 second if server unavailable only if not during tests (otherwise we create a ressources usage bomb)
|
2016-08-30 10:58:37 +03:00
|
|
|
if not hasattr(sys, "_called_from_test") or not sys._called_from_test:
|
2018-10-15 13:05:49 +03:00
|
|
|
asyncio.get_event_loop().call_later(1, lambda: asyncio.ensure_future(self.connect()))
|
2018-10-16 11:56:06 +03:00
|
|
|
|
2016-08-29 18:36:24 +03:00
|
|
|
self._cpu_usage_percent = None
|
|
|
|
self._memory_usage_percent = None
|
2020-07-19 07:46:07 +03:00
|
|
|
self._disk_usage_percent = None
|
2018-08-16 17:31:56 +03:00
|
|
|
self._controller.notification.controller_emit("compute.updated", self.__json__())
|
2016-03-18 17:55:54 +02:00
|
|
|
|
|
|
|
def _getUrl(self, path):
|
2016-10-17 15:10:25 +03:00
|
|
|
host = self._host
|
|
|
|
# IPV6
|
2016-10-17 19:20:29 +03:00
|
|
|
if host:
|
|
|
|
# IPV6
|
|
|
|
if ":" in host:
|
|
|
|
# Reduce IPV6 to his simple form
|
|
|
|
host = str(ipaddress.IPv6Address(host))
|
|
|
|
if host == "::":
|
|
|
|
host = "::1"
|
|
|
|
host = "[{}]".format(host)
|
|
|
|
elif host == "0.0.0.0":
|
|
|
|
host = "127.0.0.1"
|
2016-10-17 15:10:25 +03:00
|
|
|
return "{}://{}:{}/v2/compute{}".format(self._protocol, host, self._port, path)
|
2016-03-16 16:55:07 +02:00
|
|
|
|
2017-10-13 12:03:56 +03:00
|
|
|
def get_url(self, path):
|
|
|
|
""" Returns URL for specific path at Compute"""
|
|
|
|
return self._getUrl(path)
|
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def _run_http_query(self, method, path, data=None, timeout=20, raw=False):
|
2018-10-16 11:56:06 +03:00
|
|
|
with async_timeout.timeout(timeout):
|
2016-03-18 17:55:54 +02:00
|
|
|
url = self._getUrl(path)
|
2020-10-22 09:17:11 +03:00
|
|
|
headers = {}
|
|
|
|
headers['content-type'] = 'application/json'
|
2017-05-16 20:28:47 +03:00
|
|
|
chunked = None
|
2016-04-14 13:22:10 +03:00
|
|
|
if data == {}:
|
|
|
|
data = None
|
|
|
|
elif data is not None:
|
2016-03-18 17:55:54 +02:00
|
|
|
if hasattr(data, '__json__'):
|
2016-06-06 20:51:35 +03:00
|
|
|
data = json.dumps(data.__json__())
|
2016-12-15 18:12:54 +02:00
|
|
|
elif isinstance(data, aiohttp.streams.EmptyStreamReader):
|
|
|
|
data = None
|
2016-06-06 20:51:35 +03:00
|
|
|
# Stream the request
|
2016-10-04 11:56:38 +03:00
|
|
|
elif isinstance(data, aiohttp.streams.StreamReader) or isinstance(data, bytes):
|
2016-06-06 20:51:35 +03:00
|
|
|
chunked = True
|
|
|
|
headers['content-type'] = 'application/octet-stream'
|
2016-10-04 11:56:38 +03:00
|
|
|
# If the data is an open file we will iterate on it
|
|
|
|
elif isinstance(data, io.BufferedIOBase):
|
|
|
|
chunked = True
|
|
|
|
headers['content-type'] = 'application/octet-stream'
|
2016-06-06 20:51:35 +03:00
|
|
|
else:
|
2017-05-16 20:28:47 +03:00
|
|
|
data = json.dumps(data).encode("utf-8")
|
2016-11-11 11:38:59 +02:00
|
|
|
try:
|
2018-08-09 12:59:10 +03:00
|
|
|
log.debug("Attempting request to compute: {method} {url} {headers}".format(method=method, url=url, headers=headers))
|
2018-10-15 13:05:49 +03:00
|
|
|
response = await self._session().request(method, url, headers=headers, data=data, auth=self._auth, chunked=chunked, timeout=timeout)
|
2018-06-07 18:26:23 +03:00
|
|
|
except asyncio.TimeoutError:
|
|
|
|
raise ComputeError("Timeout error for {} call to {} after {}s".format(method, url, timeout))
|
2020-10-02 09:37:50 +03:00
|
|
|
except (aiohttp.ClientError, aiohttp.ServerDisconnectedError, aiohttp.ClientResponseError, ValueError, KeyError, socket.gaierror) as e:
|
2017-10-26 17:29:01 +03:00
|
|
|
# aiohttp 2.3.1 raises socket.gaierror when cannot find host
|
2016-11-11 11:38:59 +02:00
|
|
|
raise ComputeError(str(e))
|
2018-10-15 13:05:49 +03:00
|
|
|
body = await response.read()
|
2016-07-27 19:31:02 +03:00
|
|
|
if body and not raw:
|
2016-07-11 10:33:55 +03:00
|
|
|
body = body.decode()
|
|
|
|
|
|
|
|
if response.status >= 300:
|
|
|
|
# Try to decode the GNS3 error
|
2016-07-27 19:31:02 +03:00
|
|
|
if body and not raw:
|
2016-03-18 17:55:54 +02:00
|
|
|
try:
|
2016-07-11 10:33:55 +03:00
|
|
|
msg = json.loads(body)["message"]
|
|
|
|
except (KeyError, ValueError):
|
|
|
|
msg = body
|
|
|
|
else:
|
|
|
|
msg = ""
|
|
|
|
|
2020-10-02 09:37:50 +03:00
|
|
|
if response.status == 401:
|
|
|
|
raise ControllerUnauthorizedError("Invalid authentication for compute {}".format(self.id))
|
2016-07-11 10:33:55 +03:00
|
|
|
elif response.status == 403:
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerForbiddenError(msg)
|
2016-07-11 10:33:55 +03:00
|
|
|
elif response.status == 404:
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerNotFoundError("{} {} not found".format(method, path))
|
2017-05-16 09:36:54 +03:00
|
|
|
elif response.status == 408 or response.status == 504:
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerTimeoutError("{} {} request timeout".format(method, path))
|
2016-07-11 10:33:55 +03:00
|
|
|
elif response.status == 409:
|
|
|
|
try:
|
|
|
|
raise ComputeConflict(json.loads(body))
|
|
|
|
# If the 409 doesn't come from a GNS3 server
|
2016-05-12 00:19:00 +03:00
|
|
|
except ValueError:
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerError(msg)
|
2016-07-11 10:33:55 +03:00
|
|
|
elif response.status == 500:
|
|
|
|
raise aiohttp.web.HTTPInternalServerError(text="Internal server error {}".format(url))
|
|
|
|
elif response.status == 503:
|
|
|
|
raise aiohttp.web.HTTPServiceUnavailable(text="Service unavailable {} {}".format(url, body))
|
2016-06-06 20:51:35 +03:00
|
|
|
else:
|
2019-04-01 21:12:06 +03:00
|
|
|
raise NotImplementedError("{} status code is not supported for {} '{}'".format(response.status, method, url))
|
2016-07-11 10:33:55 +03:00
|
|
|
if body and len(body):
|
2016-07-27 19:31:02 +03:00
|
|
|
if raw:
|
|
|
|
response.body = body
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
response.json = json.loads(body)
|
|
|
|
except ValueError:
|
2020-10-02 09:37:50 +03:00
|
|
|
raise ControllerError("The server {} is not a GNS3 server".format(self._id))
|
2016-07-11 10:33:55 +03:00
|
|
|
else:
|
|
|
|
response.json = {}
|
2016-07-27 19:31:02 +03:00
|
|
|
response.body = b""
|
2016-07-11 10:33:55 +03:00
|
|
|
return response
|
2016-03-10 11:32:07 +02:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def get(self, path, **kwargs):
|
2020-10-22 08:49:44 +03:00
|
|
|
return await self.http_query("GET", path, **kwargs)
|
2016-04-14 13:22:10 +03:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def post(self, path, data={}, **kwargs):
|
|
|
|
response = await self.http_query("POST", path, data, **kwargs)
|
2016-04-14 13:22:10 +03:00
|
|
|
return response
|
2016-03-14 21:54:05 +02:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def put(self, path, data={}, **kwargs):
|
|
|
|
response = await self.http_query("PUT", path, data, **kwargs)
|
2016-04-18 18:36:38 +03:00
|
|
|
return response
|
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def delete(self, path, **kwargs):
|
2020-10-02 09:37:50 +03:00
|
|
|
return await self.http_query("DELETE", path, **kwargs)
|
2016-06-02 17:44:38 +03:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def forward(self, method, type, path, data=None):
|
2016-06-02 17:44:38 +03:00
|
|
|
"""
|
|
|
|
Forward a call to the emulator on compute
|
|
|
|
"""
|
2016-11-02 12:06:45 +02:00
|
|
|
try:
|
2017-05-22 14:57:32 +03:00
|
|
|
action = "/{}/{}".format(type, path)
|
2018-10-15 13:05:49 +03:00
|
|
|
res = await self.http_query(method, action, data=data, timeout=None)
|
2017-05-16 20:28:47 +03:00
|
|
|
except aiohttp.ServerDisconnectedError:
|
2020-10-22 08:49:44 +03:00
|
|
|
raise ControllerError(f"Connection lost to {self._id} during {method} {action}")
|
2016-06-02 19:38:47 +03:00
|
|
|
return res.json
|
2016-06-08 15:14:03 +03:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def images(self, type):
|
2016-06-08 15:14:03 +03:00
|
|
|
"""
|
2019-03-18 10:33:37 +02:00
|
|
|
Return the list of images available for this type on the compute node.
|
2016-06-08 15:14:03 +03:00
|
|
|
"""
|
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
res = await self.http_query("GET", "/{}/images".format(type), timeout=None)
|
2016-06-08 15:14:03 +03:00
|
|
|
images = res.json
|
|
|
|
|
2016-12-12 11:13:29 +02:00
|
|
|
try:
|
|
|
|
if type in ["qemu", "dynamips", "iou"]:
|
2019-03-18 10:33:37 +02:00
|
|
|
#for local_image in list_images(type):
|
|
|
|
# if local_image['filename'] not in [i['filename'] for i in images]:
|
|
|
|
# images.append(local_image)
|
2017-05-09 13:25:15 +03:00
|
|
|
images = sorted(images, key=itemgetter('filename'))
|
|
|
|
else:
|
|
|
|
images = sorted(images, key=itemgetter('image'))
|
2016-12-12 11:13:29 +02:00
|
|
|
except OSError as e:
|
2018-08-09 12:59:10 +03:00
|
|
|
raise ComputeError("Cannot list images: {}".format(str(e)))
|
2016-06-08 15:14:03 +03:00
|
|
|
return images
|
2016-07-21 21:17:36 +03:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def list_files(self, project):
|
2016-07-21 21:17:36 +03:00
|
|
|
"""
|
|
|
|
List files in the project on computes
|
|
|
|
"""
|
|
|
|
path = "/projects/{}/files".format(project.id)
|
2018-10-15 13:05:49 +03:00
|
|
|
res = await self.http_query("GET", path, timeout=None)
|
2016-07-21 21:17:36 +03:00
|
|
|
return res.json
|
2016-08-25 20:14:29 +03:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
async def get_ip_on_same_subnet(self, other_compute):
|
2016-08-25 20:14:29 +03:00
|
|
|
"""
|
2018-03-15 09:17:39 +02:00
|
|
|
Try to find the best ip for communication from one compute
|
2016-08-25 20:14:29 +03:00
|
|
|
to another
|
|
|
|
|
|
|
|
:returns: Tuple (ip_for_this_compute, ip_for_other_compute)
|
|
|
|
"""
|
|
|
|
if other_compute == self:
|
2020-10-22 08:49:44 +03:00
|
|
|
return self.host_ip, self.host_ip
|
2016-08-25 20:14:29 +03:00
|
|
|
|
2016-12-13 17:46:09 +02:00
|
|
|
# Perhaps the user has correct network gateway, we trust him
|
2020-10-22 08:49:44 +03:00
|
|
|
if self.host_ip not in ('0.0.0.0', '127.0.0.1') and other_compute.host_ip not in ('0.0.0.0', '127.0.0.1'):
|
|
|
|
return self.host_ip, other_compute.host_ip
|
2016-12-13 17:46:09 +02:00
|
|
|
|
2018-10-15 13:05:49 +03:00
|
|
|
this_compute_interfaces = await self.interfaces()
|
|
|
|
other_compute_interfaces = await other_compute.interfaces()
|
2016-08-25 20:14:29 +03:00
|
|
|
|
|
|
|
# Sort interface to put the compute host in first position
|
|
|
|
# we guess that if user specified this host it could have a reason (VMware Nat / Host only interface)
|
|
|
|
this_compute_interfaces = sorted(this_compute_interfaces, key=lambda i: i["ip_address"] != self.host_ip)
|
|
|
|
other_compute_interfaces = sorted(other_compute_interfaces, key=lambda i: i["ip_address"] != other_compute.host_ip)
|
|
|
|
|
|
|
|
for this_interface in this_compute_interfaces:
|
2016-09-05 19:02:49 +03:00
|
|
|
# Skip if no ip or no netmask (vbox when stopped set a null netmask)
|
|
|
|
if len(this_interface["ip_address"]) == 0 or this_interface["netmask"] is None:
|
2016-08-25 20:14:29 +03:00
|
|
|
continue
|
2016-11-10 15:46:25 +02:00
|
|
|
# Ignore 169.254 network because it's for Windows special purpose
|
|
|
|
if this_interface["ip_address"].startswith("169.254."):
|
|
|
|
continue
|
2016-08-25 20:14:29 +03:00
|
|
|
|
|
|
|
this_network = ipaddress.ip_network("{}/{}".format(this_interface["ip_address"], this_interface["netmask"]), strict=False)
|
|
|
|
|
|
|
|
for other_interface in other_compute_interfaces:
|
2016-11-11 17:08:52 +02:00
|
|
|
if len(other_interface["ip_address"]) == 0 or other_interface["netmask"] is None:
|
2016-08-25 20:14:29 +03:00
|
|
|
continue
|
|
|
|
|
|
|
|
# Avoid stuff like 127.0.0.1
|
|
|
|
if other_interface["ip_address"] == this_interface["ip_address"]:
|
|
|
|
continue
|
|
|
|
|
|
|
|
other_network = ipaddress.ip_network("{}/{}".format(other_interface["ip_address"], other_interface["netmask"]), strict=False)
|
|
|
|
if this_network.overlaps(other_network):
|
2020-10-22 08:49:44 +03:00
|
|
|
return this_interface["ip_address"], other_interface["ip_address"]
|
2016-11-10 23:39:16 +02:00
|
|
|
|
2016-08-25 20:14:29 +03:00
|
|
|
raise ValueError("No common subnet for compute {} and {}".format(self.name, other_compute.name))
|