Drop Python 3.4 and switch to async / await syntax for asyncio. Fixes #1425

This commit is contained in:
grossmj committed 2018-10-15 17:05:49 +07:00
1 parent 8baa480b79
commit 76af98404a
119 files changed
+2432 -3084

No files matched your search

+46 -61
View File
@@ -65,8 +65,7 @@ class Controller:
log.info("Load controller configuration file {}".format(self._config_file))
@locking
@asyncio.coroutine
def download_appliance_templates(self):
async def download_appliance_templates(self):
session = aiohttp.ClientSession()
try:
@@ -74,7 +73,7 @@ class Controller:
if self._appliance_templates_etag:
log.info("Checking if appliance templates are up-to-date (ETag {})".format(self._appliance_templates_etag))
headers["If-None-Match"] = self._appliance_templates_etag
response = yield from session.get('https://api.github.com/repos/GNS3/gns3-registry/contents/appliances', headers=headers)
response = await session.get('https://api.github.com/repos/GNS3/gns3-registry/contents/appliances', headers=headers)
if response.status == 304:
log.info("Appliance templates are already up-to-date (ETag {})".format(self._appliance_templates_etag))
return
@@ -84,19 +83,19 @@ class Controller:
if etag:
self._appliance_templates_etag = etag
self.save()
json_data = yield from response.json()
json_data = await response.json()
response.close()
appliances_dir = get_resource('appliances')
for appliance in json_data:
if appliance["type"] == "file":
appliance_name = appliance["name"]
log.info("Download appliance template file from '{}'".format(appliance["download_url"]))
response = yield from session.get(appliance["download_url"])
response = await session.get(appliance["download_url"])
if response.status != 200:
log.warning("Could not download '{}' due to HTTP error code {}".format(appliance["download_url"], response.status))
continue
try:
appliance_data = yield from response.read()
appliance_data = await response.read()
except asyncio.TimeoutError:
log.warning("Timeout while downloading '{}'".format(appliance["download_url"]))
continue
@@ -215,8 +214,7 @@ class Controller:
for b in builtins:
self._appliances[b.id] = b
@asyncio.coroutine
def start(self):
async def start(self):
log.info("Controller is starting")
self.load_base_files()
@@ -235,9 +233,9 @@ class Controller:
if name == "gns3vm":
name = "Main server"
computes = yield from self._load_controller_settings()
computes = await self._load_controller_settings()
try:
self._local_server = yield from self.add_compute(compute_id="local",
self._local_server = await self.add_compute(compute_id="local",
name=name,
protocol=server_config.get("protocol", "http"),
host=host,
@@ -251,15 +249,15 @@ class Controller:
sys.exit(1)
for c in computes:
try:
yield from self.add_compute(**c)
await self.add_compute(**c)
except (aiohttp.web.HTTPError, KeyError):
pass # Skip not available servers at loading
yield from self.load_projects()
await self.load_projects()
try:
yield from self.gns3vm.auto_start_vm()
await self.gns3vm.auto_start_vm()
except GNS3VMError as e:
log.warning(str(e))
yield from self._project_auto_open()
await self._project_auto_open()
def _update_config(self):
"""
@@ -271,19 +269,18 @@ class Controller:
self._local_server.user = server_config.get("user")
self._local_server.password = server_config.get("password")
@asyncio.coroutine
def stop(self):
async def stop(self):
log.info("Controller is Stopping")
for project in self._projects.values():
yield from project.close()
await project.close()
for compute in self._computes.values():
try:
yield from compute.close()
await compute.close()
# We don't care if a compute is down at this step
except (ComputeError, aiohttp.web.HTTPError, OSError):
pass
yield from self.gns3vm.exit_vm()
await self.gns3vm.exit_vm()
self._computes = {}
self._projects = {}
@@ -321,15 +318,14 @@ class Controller:
except OSError as e:
log.error("Cannnot write configuration file '{}': {}".format(self._config_file, e))
@asyncio.coroutine
def _load_controller_settings(self):
async def _load_controller_settings(self):
"""
Reload the controller configuration from disk
"""
try:
if not os.path.exists(self._config_file):
yield from self._import_gns3_gui_conf()
await self._import_gns3_gui_conf()
self.save()
with open(self._config_file) as f:
data = json.load(f)
@@ -350,8 +346,7 @@ class Controller:
self.load_appliances()
return data.get("computes", [])
@asyncio.coroutine
def load_projects(self):
async def load_projects(self):
"""
Preload the list of projects from disk
"""
@@ -366,7 +361,7 @@ class Controller:
for file in os.listdir(project_dir):
if file.endswith(".gns3"):
try:
yield from self.load_project(os.path.join(project_dir, file), load=False)
await self.load_project(os.path.join(project_dir, file), load=False)
except (aiohttp.web.HTTPConflict, NotImplementedError):
pass # Skip not compatible projects
except OSError as e:
@@ -417,8 +412,7 @@ class Controller:
os.makedirs(appliances_path, exist_ok=True)
return appliances_path
@asyncio.coroutine
def _import_gns3_gui_conf(self):
async def _import_gns3_gui_conf(self):
"""
Import old config from GNS3 GUI
"""
@@ -430,7 +424,7 @@ class Controller:
server_settings = data.get("Servers", {})
for remote in server_settings.get("remote_servers", []):
try:
yield from self.add_compute(
await self.add_compute(
host=remote.get("host", "localhost"),
port=remote.get("port", 3080),
protocol=remote.get("protocol", "http"),
@@ -488,8 +482,7 @@ class Controller:
self.load_appliances()
self.notification.controller_emit("settings.updated", val)
@asyncio.coroutine
def add_compute(self, compute_id=None, name=None, force=False, connect=True, **kwargs):
async def add_compute(self, compute_id=None, name=None, force=False, connect=True, **kwargs):
"""
Add a server to the dictionary of compute servers controlled by this controller
@@ -519,24 +512,23 @@ class Controller:
self._computes[compute.id] = compute
self.save()
if connect:
yield from compute.connect()
await compute.connect()
self.notification.controller_emit("compute.created", compute.__json__())
return compute
else:
if connect:
yield from self._computes[compute_id].connect()
await self._computes[compute_id].connect()
self.notification.controller_emit("compute.updated", self._computes[compute_id].__json__())
return self._computes[compute_id]
@asyncio.coroutine
def close_compute_projects(self, compute):
async def close_compute_projects(self, compute):
"""
Close projects running on a compute
"""
for project in self._projects.values():
if compute in project.computes:
yield from project.close()
await project.close()
def compute_has_open_project(self, compute):
"""
@@ -550,8 +542,7 @@ class Controller:
return True
return False
@asyncio.coroutine
def delete_compute(self, compute_id):
async def delete_compute(self, compute_id):
"""
Delete a compute node. Project using this compute will be close
@@ -562,8 +553,8 @@ class Controller:
compute = self.get_compute(compute_id)
except aiohttp.web.HTTPNotFound:
return
yield from self.close_compute_projects(compute)
yield from compute.close()
await self.close_compute_projects(compute)
await compute.close()
del self._computes[compute_id]
self.save()
self.notification.controller_emit("compute.deleted", compute.__json__())
@@ -603,8 +594,7 @@ class Controller:
return compute_id in self._computes
@asyncio.coroutine
def add_project(self, project_id=None, name=None, path=None, **kwargs):
async def add_project(self, project_id=None, name=None, path=None, **kwargs):
"""
Creates a project or returns an existing project
@@ -635,8 +625,7 @@ class Controller:
except KeyError:
raise aiohttp.web.HTTPNotFound(text="Project ID {} doesn't exist".format(project_id))
@asyncio.coroutine
def get_loaded_project(self, project_id):
async def get_loaded_project(self, project_id):
"""
Returns a project or raise a 404 error.
@@ -644,7 +633,7 @@ class Controller:
"""
project = self.get_project(project_id)
yield from project.wait_loaded()
await project.wait_loaded()
return project
def remove_project(self, project):
@@ -652,8 +641,7 @@ class Controller:
if project.id in self._projects:
del self._projects[project.id]
@asyncio.coroutine
def load_project(self, path, load=True):
async def load_project(self, path, load=True):
"""
Load a project from a .gns3
@@ -670,20 +658,19 @@ class Controller:
if topo_data["project_id"] in self._projects:
project = self._projects[topo_data["project_id"]]
else:
project = yield from self.add_project(path=os.path.dirname(path), status="closed", filename=os.path.basename(path), **topo_data)
project = await self.add_project(path=os.path.dirname(path), status="closed", filename=os.path.basename(path), **topo_data)
if load or project.auto_open:
yield from project.open()
await project.open()
return project
@asyncio.coroutine
def _project_auto_open(self):
async def _project_auto_open(self):
"""
Auto open the project with auto open enable
"""
for project in self._projects.values():
if project.auto_open:
yield from project.open()
await project.open()
def get_free_project_name(self, base_name):
"""
@@ -747,8 +734,7 @@ class Controller:
Controller._instance = Controller()
return Controller._instance
@asyncio.coroutine
def autoidlepc(self, compute_id, platform, image, ram):
async def autoidlepc(self, compute_id, platform, image, ram):
"""
Compute and IDLE PC value for an image
@@ -761,17 +747,16 @@ class Controller:
compute = self.get_compute(compute_id)
for project in list(self._projects.values()):
if project.name == "AUTOIDLEPC":
yield from project.delete()
await project.delete()
self.remove_project(project)
project = yield from self.add_project(name="AUTOIDLEPC")
node = yield from project.add_node(compute, "AUTOIDLEPC", str(uuid.uuid4()), node_type="dynamips", platform=platform, image=image, ram=ram)
res = yield from node.dynamips_auto_idlepc()
yield from project.delete()
project = await self.add_project(name="AUTOIDLEPC")
node = await project.add_node(compute, "AUTOIDLEPC", str(uuid.uuid4()), node_type="dynamips", platform=platform, image=image, ram=ram)
res = await node.dynamips_auto_idlepc()
await project.delete()
self.remove_project(project)
return res
@asyncio.coroutine
def compute_ports(self, compute_id):
async def compute_ports(self, compute_id):
"""
Get the ports used by a compute.
@@ -779,5 +764,5 @@ class Controller:
"""
compute = self.get_compute(compute_id)
response = yield from compute.get("/network/ports")
response = await compute.get("/network/ports")
return response.json
+48 -67
View File
@@ -27,7 +27,7 @@ from operator import itemgetter
from ..utils import parse_version
from ..utils.images import list_images
from ..utils.asyncio import locking, asyncio_ensure_future
from ..utils.asyncio import locking
from ..controller.controller_error import ControllerError
from ..version import __version__, __version_info__
@@ -146,18 +146,16 @@ class Compute:
"""
self._last_error = msg
@asyncio.coroutine
def interfaces(self):
async def interfaces(self):
"""
Get the list of network on compute
"""
if not self._interfaces_cache:
response = yield from self.get("/network/interfaces")
response = await self.get("/network/interfaces")
self._interfaces_cache = response.json
return self._interfaces_cache
@asyncio.coroutine
def update(self, **kwargs):
async def update(self, **kwargs):
for kw in kwargs:
if kw not in ("user", "password"):
setattr(self, kw, kwargs[kw])
@@ -170,13 +168,12 @@ class Compute:
self._controller.notification.controller_emit("compute.updated", self.__json__())
self._controller.save()
@asyncio.coroutine
def close(self):
async def close(self):
self._connected = False
if self._http_session:
self._http_session.close()
if self._ws:
yield from self._ws.close()
await self._ws.close()
self._ws = None
self._closed = True
@@ -314,8 +311,7 @@ class Compute:
"last_error": self._last_error
}
@asyncio.coroutine
def download_file(self, project, path):
async def download_file(self, project, path):
"""
Read file of a project and download it
@@ -325,13 +321,12 @@ class Compute:
"""
url = self._getUrl("/projects/{}/files/{}".format(project.id, path))
response = yield from self._session().request("GET", url, auth=self._auth)
response = await self._session().request("GET", url, auth=self._auth)
if response.status == 404:
raise aiohttp.web.HTTPNotFound(text="{} not found on compute".format(path))
return response
@asyncio.coroutine
def download_image(self, image_type, image):
async def download_image(self, image_type, image):
"""
Read file of a project and download it
@@ -341,13 +336,12 @@ class Compute:
"""
url = self._getUrl("/{}/images/{}".format(image_type, image))
response = yield from self._session().request("GET", url, auth=self._auth)
response = await self._session().request("GET", url, auth=self._auth)
if response.status == 404:
raise aiohttp.web.HTTPNotFound(text="{} not found on compute".format(image))
return response
@asyncio.coroutine
def stream_file(self, project, path, timeout=None):
async def stream_file(self, project, path, timeout=None):
"""
Read file of a project and stream it
@@ -372,7 +366,7 @@ class Compute:
self._response.close()
url = self._getUrl("/projects/{}/stream/{}".format(project.id, path))
response = yield from self._session().request("GET", url, auth=self._auth, timeout=timeout)
response = await self._session().request("GET", url, auth=self._auth, timeout=timeout)
if response.status == 404:
raise aiohttp.web.HTTPNotFound(text="{} not found on compute".format(path))
elif response.status == 403:
@@ -383,34 +377,31 @@ class Compute:
path))
return StreamResponse(response)
@asyncio.coroutine
def http_query(self, method, path, data=None, dont_connect=False, **kwargs):
async def http_query(self, method, path, data=None, dont_connect=False, **kwargs):
"""
:param dont_connect: If true do not reconnect if not connected
"""
if not self._connected and not dont_connect:
if self._id == "vm" and not self._controller.gns3vm.running:
yield from self._controller.gns3vm.start()
yield from self.connect()
await self._controller.gns3vm.start()
await self.connect()
if not self._connected and not dont_connect:
raise ComputeError("Cannot connect to compute '{}' with request {} {}".format(self._name, method, path))
response = yield from self._run_http_query(method, path, data=data, **kwargs)
response = await self._run_http_query(method, path, data=data, **kwargs)
return response
@asyncio.coroutine
def _try_reconnect(self):
async def _try_reconnect(self):
"""
We catch error during reconnect
"""
try:
yield from self.connect()
await self.connect()
except aiohttp.web.HTTPConflict:
pass
@locking
@asyncio.coroutine
def connect(self):
async def connect(self):
"""
Check if remote server is accessible
"""
@@ -418,7 +409,7 @@ class Compute:
if not self._connected and not self._closed and self.host:
try:
log.info("Connecting to compute '{}'".format(self._id))
response = yield from self._run_http_query("GET", "/capabilities")
response = await self._run_http_query("GET", "/capabilities")
except ComputeError as e:
log.warning("Cannot connect to compute '{}': {}".format(self._id, e))
# Try to reconnect after 2 seconds if server unavailable only if not during tests (otherwise we create a ressource usage bomb)
@@ -430,8 +421,8 @@ class Compute:
# After 5 failure we close the project using the compute to avoid sync issues
if self._connection_failure == 10:
log.error("Could not connect to compute '{}' after multiple attempts: {}".format(self._id, e))
yield from self._controller.close_compute_projects(self)
asyncio.get_event_loop().call_later(2, lambda: asyncio_ensure_future(self._try_reconnect()))
await self._controller.close_compute_projects(self)
asyncio.get_event_loop().call_later(2, lambda: asyncio.ensure_future(self._try_reconnect()))
return
except aiohttp.web.HTTPNotFound:
raise aiohttp.web.HTTPConflict(text="The server {} is not a GNS3 server or it's a 1.X server".format(self._id))
@@ -479,18 +470,17 @@ class Compute:
self._last_error = None
self._controller.notification.controller_emit("compute.updated", self.__json__())
@asyncio.coroutine
def _connect_notification(self):
async def _connect_notification(self):
"""
Connect to the notification stream
"""
try:
self._ws = yield from self._session().ws_connect(self._getUrl("/notifications/ws"), auth=self._auth)
self._ws = await self._session().ws_connect(self._getUrl("/notifications/ws"), auth=self._auth)
except (aiohttp.WSServerHandshakeError, aiohttp.ClientResponseError):
self._ws = None
while self._ws is not None:
try:
response = yield from self._ws.receive()
response = await self._ws.receive()
except aiohttp.WSServerHandshakeError:
self._ws = None
break
@@ -505,13 +495,13 @@ class Compute:
self._memory_usage_percent = event["memory_usage_percent"]
self._controller.notification.controller_emit("compute.updated", self.__json__())
else:
yield from self._controller.notification.dispatch(action, event, compute_id=self.id)
await self._controller.notification.dispatch(action, event, compute_id=self.id)
if self._ws:
yield from self._ws.close()
await self._ws.close()
# Try to reconnect after 1 seconds if server unavailable only if not during tests (otherwise we create a ressources usage bomb)
if not hasattr(sys, "_called_from_test") or not sys._called_from_test:
asyncio.get_event_loop().call_later(1, lambda: asyncio_ensure_future(self.connect()))
asyncio.get_event_loop().call_later(1, lambda: asyncio.ensure_future(self.connect()))
self._ws = None
self._cpu_usage_percent = None
self._memory_usage_percent = None
@@ -536,8 +526,7 @@ class Compute:
""" Returns URL for specific path at Compute"""
return self._getUrl(path)
@asyncio.coroutine
def _run_http_query(self, method, path, data=None, timeout=20, raw=False):
async def _run_http_query(self, method, path, data=None, timeout=20, raw=False):
with Timeout(timeout):
url = self._getUrl(path)
headers = {}
@@ -562,13 +551,13 @@ class Compute:
data = json.dumps(data).encode("utf-8")
try:
log.debug("Attempting request to compute: {method} {url} {headers}".format(method=method, url=url, headers=headers))
response = yield from self._session().request(method, url, headers=headers, data=data, auth=self._auth, chunked=chunked, timeout=timeout)
response = await self._session().request(method, url, headers=headers, data=data, auth=self._auth, chunked=chunked, timeout=timeout)
except asyncio.TimeoutError:
raise ComputeError("Timeout error for {} call to {} after {}s".format(method, url, timeout))
except (aiohttp.ClientError, aiohttp.ServerDisconnectedError, ValueError, KeyError, socket.gaierror) as e:
# aiohttp 2.3.1 raises socket.gaierror when cannot find host
raise ComputeError(str(e))
body = yield from response.read()
body = await response.read()
if body and not raw:
body = body.decode()
@@ -617,46 +606,40 @@ class Compute:
response.body = b""
return response
@asyncio.coroutine
def get(self, path, **kwargs):
return (yield from self.http_query("GET", path, **kwargs))
async def get(self, path, **kwargs):
return (await self.http_query("GET", path, **kwargs))
@asyncio.coroutine
def post(self, path, data={}, **kwargs):
response = yield from self.http_query("POST", path, data, **kwargs)
async def post(self, path, data={}, **kwargs):
response = await self.http_query("POST", path, data, **kwargs)
return response
@asyncio.coroutine
def put(self, path, data={}, **kwargs):
response = yield from self.http_query("PUT", path, data, **kwargs)
async def put(self, path, data={}, **kwargs):
response = await self.http_query("PUT", path, data, **kwargs)
return response
@asyncio.coroutine
def delete(self, path, **kwargs):
return (yield from self.http_query("DELETE", path, **kwargs))
async def delete(self, path, **kwargs):
return (await self.http_query("DELETE", path, **kwargs))
@asyncio.coroutine
def forward(self, method, type, path, data=None):
async def forward(self, method, type, path, data=None):
"""
Forward a call to the emulator on compute
"""
try:
action = "/{}/{}".format(type, path)
res = yield from self.http_query(method, action, data=data, timeout=None)
res = await self.http_query(method, action, data=data, timeout=None)
except aiohttp.ServerDisconnectedError:
log.error("Connection lost to %s during %s %s", self._id, method, action)
raise aiohttp.web.HTTPGatewayTimeout()
return res.json
@asyncio.coroutine
def images(self, type):
async def images(self, type):
"""
Return the list of images available for this type on controller
and on the compute node.
"""
images = []
res = yield from self.http_query("GET", "/{}/images".format(type), timeout=None)
res = await self.http_query("GET", "/{}/images".format(type), timeout=None)
images = res.json
try:
@@ -671,17 +654,15 @@ class Compute:
raise ComputeError("Cannot list images: {}".format(str(e)))
return images
@asyncio.coroutine
def list_files(self, project):
async def list_files(self, project):
"""
List files in the project on computes
"""
path = "/projects/{}/files".format(project.id)
res = yield from self.http_query("GET", path, timeout=None)
res = await self.http_query("GET", path, timeout=None)
return res.json
@asyncio.coroutine
def get_ip_on_same_subnet(self, other_compute):
async def get_ip_on_same_subnet(self, other_compute):
"""
Try to find the best ip for communication from one compute
to another
@@ -695,8 +676,8 @@ class Compute:
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)
this_compute_interfaces = yield from self.interfaces()
other_compute_interfaces = yield from other_compute.interfaces()
this_compute_interfaces = await self.interfaces()
other_compute_interfaces = await other_compute.interfaces()
# 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)
+1 -2
View File
@@ -165,8 +165,7 @@ class Drawing:
def rotation(self, val):
self._rotation = val
@asyncio.coroutine
def update(self, **kwargs):
async def update(self, **kwargs):
"""
Update the drawing
+10 -13
View File
@@ -30,8 +30,7 @@ import logging
log = logging.getLogger(__name__)
@asyncio.coroutine
def export_project(project, temporary_dir, include_images=False, keep_compute_id=False, allow_all_nodes=False):
async def export_project(project, temporary_dir, include_images=False, keep_compute_id=False, allow_all_nodes=False):
"""
Export a project to a zip file.
@@ -61,7 +60,7 @@ def export_project(project, temporary_dir, include_images=False, keep_compute_id
# First we process the .gns3 in order to be sure we don't have an error
for file in os.listdir(project._path):
if file.endswith(".gns3"):
yield from _patch_project_file(project, os.path.join(project._path, file), zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir)
await _patch_project_file(project, os.path.join(project._path, file), zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir)
# Export the local files
for root, dirs, files in os.walk(project._path, topdown=True):
@@ -86,15 +85,15 @@ def export_project(project, temporary_dir, include_images=False, keep_compute_id
downloaded_files = set()
for compute in project.computes:
if compute.id != "local":
compute_files = yield from compute.list_files(project)
compute_files = await compute.list_files(project)
for compute_file in compute_files:
if _is_exportable(compute_file["path"]):
(fd, temp_path) = tempfile.mkstemp(dir=temporary_dir)
f = open(fd, "wb", closefd=True)
response = yield from compute.download_file(project, compute_file["path"])
response = await compute.download_file(project, compute_file["path"])
while True:
try:
data = yield from response.content.read(1024)
data = await response.content.read(1024)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when downloading file '{}' from remote compute server {}:{}".format(compute_file["path"], compute.host, compute.port))
if not data:
@@ -154,8 +153,7 @@ def _is_exportable(path):
return True
@asyncio.coroutine
def _patch_project_file(project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir):
async def _patch_project_file(project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir):
"""
Patch a project file (.gns3) to export a project.
The .gns3 file is renamed to project.gns3
@@ -219,7 +217,7 @@ def _patch_project_file(project, path, zstream, include_images, keep_compute_id,
for i in images if i['compute_id'] != 'local'])
for compute_id, image_type, image in remote_images:
yield from _export_remote_images(project, compute_id, image_type, image, zstream, temporary_dir)
await _export_remote_images(project, compute_id, image_type, image, zstream, temporary_dir)
zstream.writestr("project.gns3", json.dumps(topology).encode())
return images
@@ -253,8 +251,7 @@ def _export_local_image(image, zstream):
return
@asyncio.coroutine
def _export_remote_images(project, compute_id, image_type, image, project_zipfile, temporary_dir):
async def _export_remote_images(project, compute_id, image_type, image, project_zipfile, temporary_dir):
"""
Export specific image from remote compute.
"""
@@ -268,14 +265,14 @@ def _export_remote_images(project, compute_id, image_type, image, project_zipfil
(fd, temp_path) = tempfile.mkstemp(dir=temporary_dir)
f = open(fd, "wb", closefd=True)
response = yield from compute.download_image(image_type, image)
response = await compute.download_image(image_type, image)
if response.status != 200:
raise aiohttp.web.HTTPConflict(text="Cannot export image from '{}' compute. Compute returned status code {}.".format(compute_id, response.status))
while True:
try:
data = yield from response.content.read(1024)
data = await response.content.read(1024)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when downloading image '{}' from remote compute server {}:{}".format(image, compute.host, compute.port))
if not data:
+30 -38
View File
@@ -21,7 +21,7 @@ import asyncio
import aiohttp
import ipaddress
from ...utils.asyncio import locking, asyncio_ensure_future
from ...utils.asyncio import locking
from .vmware_gns3_vm import VMwareGNS3VM
from .virtualbox_gns3_vm import VirtualBoxGNS3VM
from .hyperv_gns3_vm import HyperVGNS3VM
@@ -204,8 +204,7 @@ class GNS3VM:
self._settings.update(val)
@asyncio.coroutine
def update_settings(self, settings):
async def update_settings(self, settings):
"""
Update settings and will restart the VM if require
"""
@@ -213,15 +212,15 @@ class GNS3VM:
new_settings = copy.copy(self._settings)
new_settings.update(settings)
if self.settings != new_settings:
yield from self._stop()
await self._stop()
self._settings = settings
self._controller.save()
if self.enable:
yield from self.start()
await self.start()
else:
# When user fix something on his system and try again
if self.enable and not self.current_engine().running:
yield from self.start()
await self.start()
def _get_engine(self, engine):
"""
@@ -248,8 +247,7 @@ class GNS3VM:
def __json__(self):
return self._settings
@asyncio.coroutine
def list(self, engine):
async def list(self, engine):
"""
List VMS for an engine
"""
@@ -257,7 +255,7 @@ class GNS3VM:
engine = self._get_engine(engine)
vms = []
try:
for vm in (yield from engine.list()):
for vm in (await engine.list()):
vms.append({"vmname": vm["vmname"]})
except GNS3VMError as e:
# We raise error only if user activated the GNS3 VM
@@ -266,19 +264,18 @@ class GNS3VM:
raise e
return vms
@asyncio.coroutine
def auto_start_vm(self):
async def auto_start_vm(self):
"""
Auto start the GNS3 VM if require
"""
if self.enable:
try:
yield from self.start()
await self.start()
except GNS3VMError as e:
# User will receive the error later when they will try to use the node
try:
compute = yield from self._controller.add_compute(compute_id="vm",
compute = await self._controller.add_compute(compute_id="vm",
name="GNS3 VM ({})".format(self.current_engine().vmname),
host=None,
force=True)
@@ -288,21 +285,19 @@ class GNS3VM:
pass
log.error("Cannot start the GNS3 VM: {}".format(e))
@asyncio.coroutine
def exit_vm(self):
async def exit_vm(self):
if self.enable:
try:
if self._settings["when_exit"] == "stop":
yield from self._stop()
await self._stop()
elif self._settings["when_exit"] == "suspend":
yield from self._suspend()
await self._suspend()
except GNS3VMError as e:
log.warning(str(e))
@locking
@asyncio.coroutine
def start(self):
async def start(self):
"""
Start the GNS3 VM
"""
@@ -316,22 +311,22 @@ class GNS3VM:
engine.ram = self._settings["ram"]
engine.vcpus = self._settings["vcpus"]
engine.headless = self._settings["headless"]
compute = yield from self._controller.add_compute(compute_id="vm",
compute = await self._controller.add_compute(compute_id="vm",
name="GNS3 VM is starting ({})".format(engine.vmname),
host=None,
force=True,
connect=False)
try:
yield from engine.start()
await engine.start()
except Exception as e:
yield from self._controller.delete_compute("vm")
await self._controller.delete_compute("vm")
log.error("Cannot start the GNS3 VM: {}".format(str(e)))
yield from compute.update(name="GNS3 VM ({})".format(engine.vmname))
await compute.update(name="GNS3 VM ({})".format(engine.vmname))
compute.set_last_error(str(e))
raise e
yield from compute.connect() # we can connect now that the VM has started
yield from compute.update(name="GNS3 VM ({})".format(engine.vmname),
await compute.connect() # we can connect now that the VM has started
await compute.update(name="GNS3 VM ({})".format(engine.vmname),
protocol=self.protocol,
host=self.ip_address,
port=self.port,
@@ -340,16 +335,15 @@ class GNS3VM:
# check if the VM is in the same subnet as the local server, start 10 seconds later to give
# some time for the compute in the VM to be ready for requests
asyncio.get_event_loop().call_later(10, lambda: asyncio_ensure_future(self._check_network(compute)))
asyncio.get_event_loop().call_later(10, lambda: asyncio.ensure_future(self._check_network(compute)))
@asyncio.coroutine
def _check_network(self, compute):
async def _check_network(self, compute):
"""
Check that the VM is in the same subnet as the local server
"""
try:
vm_interfaces = yield from compute.interfaces()
vm_interfaces = await compute.interfaces()
vm_interface_netmask = None
for interface in vm_interfaces:
if interface["ip_address"] == self.ip_address:
@@ -360,7 +354,7 @@ class GNS3VM:
for compute_id in self._controller.computes:
if compute_id == "local":
compute = self._controller.get_compute(compute_id)
interfaces = yield from compute.interfaces()
interfaces = await compute.interfaces()
netmask = None
for interface in interfaces:
if interface["ip_address"] == compute.host_ip:
@@ -378,27 +372,25 @@ class GNS3VM:
log.warning("Could not check the VM is in the same subnet as the local server: {}".format(e.text))
@locking
@asyncio.coroutine
def _suspend(self):
async def _suspend(self):
"""
Suspend the GNS3 VM
"""
engine = self.current_engine()
if "vm" in self._controller.computes:
yield from self._controller.delete_compute("vm")
await self._controller.delete_compute("vm")
if engine.running:
log.info("Suspend the GNS3 VM")
yield from engine.suspend()
await engine.suspend()
@locking
@asyncio.coroutine
def _stop(self):
async def _stop(self):
"""
Stop the GNS3 VM
"""
engine = self.current_engine()
if "vm" in self._controller.computes:
yield from self._controller.delete_compute("vm")
await self._controller.delete_compute("vm")
if engine.running:
log.info("Stop the GNS3 VM")
yield from engine.stop()
await engine.stop()
+4 -8
View File
@@ -254,32 +254,28 @@ class BaseGNS3VM:
return self._engine
@asyncio.coroutine
def list(self):
async def list(self):
"""
List all VMs
"""
raise NotImplementedError
@asyncio.coroutine
def start(self):
async def start(self):
"""
Starts the GNS3 VM.
"""
raise NotImplementedError
@asyncio.coroutine
def suspend(self):
async def suspend(self):
"""
Suspend the GNS3 VM.
"""
raise NotImplementedError
@asyncio.coroutine
def stop(self):
async def stop(self):
"""
Stops the GNS3 VM.
"""
+10 -15
View File
@@ -164,8 +164,7 @@ class HyperVGNS3VM(BaseGNS3VM):
except Exception as e:
raise GNS3VMError("Could not set to {} and RAM amount set to {}: {}".format(vcpus, ram, e))
@asyncio.coroutine
def list(self):
async def list(self):
"""
List all Hyper-V VMs
"""
@@ -189,8 +188,7 @@ class HyperVGNS3VM(BaseGNS3VM):
return wmi.WMI(moniker=path.replace('\\', '/'))
@asyncio.coroutine
def _set_state(self, state):
async def _set_state(self, state):
"""
Set the desired state of the VM
"""
@@ -203,15 +201,14 @@ class HyperVGNS3VM(BaseGNS3VM):
if ret == HyperVGNS3VM._WMI_JOB_STATUS_STARTED:
job = self._get_wmi_obj(job_path)
while job.JobState == HyperVGNS3VM._WMI_JOB_STATE_RUNNING:
yield from asyncio.sleep(0.1)
await asyncio.sleep(0.1)
job = self._get_wmi_obj(job_path)
if job.JobState != HyperVGNS3VM._WMI_JOB_STATE_COMPLETED:
raise GNS3VMError("Error while changing state: {}".format(job.ErrorSummaryDescription))
elif ret != 0 or ret != 32775:
raise GNS3VMError("Failed to change state to {}".format(state))
@asyncio.coroutine
def start(self):
async def start(self):
"""
Starts the GNS3 VM.
"""
@@ -228,7 +225,7 @@ class HyperVGNS3VM(BaseGNS3VM):
# start the VM
try:
yield from self._set_state(HyperVGNS3VM._HYPERV_VM_STATE_ENABLED)
await self._set_state(HyperVGNS3VM._HYPERV_VM_STATE_ENABLED)
except GNS3VMError as e:
raise GNS3VMError("Failed to start the GNS3 VM: {}".format(e))
log.info("GNS3 VM has been started")
@@ -259,32 +256,30 @@ class HyperVGNS3VM(BaseGNS3VM):
break
elif trial == 0:
raise GNS3VMError("Could not find guest IP address for {}".format(self.vmname))
yield from asyncio.sleep(1)
await asyncio.sleep(1)
self.ip_address = guest_ip_address
log.info("GNS3 VM IP address set to {}".format(guest_ip_address))
self.running = True
@asyncio.coroutine
def suspend(self):
async def suspend(self):
"""
Suspends the GNS3 VM.
"""
try:
yield from self._set_state(HyperVGNS3VM._HYPERV_VM_STATE_PAUSED)
await self._set_state(HyperVGNS3VM._HYPERV_VM_STATE_PAUSED)
except GNS3VMError as e:
raise GNS3VMError("Failed to suspend the GNS3 VM: {}".format(e))
log.info("GNS3 VM has been suspended")
self.running = False
@asyncio.coroutine
def stop(self):
async def stop(self):
"""
Stops the GNS3 VM.
"""
try:
yield from self._set_state(HyperVGNS3VM._HYPERV_VM_STATE_SHUTDOWN)
await self._set_state(HyperVGNS3VM._HYPERV_VM_STATE_SHUTDOWN)
except GNS3VMError as e:
raise GNS3VMError("Failed to stop the GNS3 VM: {}".format(e))
log.info("GNS3 VM has been stopped")
@@ -32,8 +32,7 @@ class RemoteGNS3VM(BaseGNS3VM):
self._engine = "remote"
super().__init__(controller)
@asyncio.coroutine
def list(self):
async def list(self):
"""
List all VMs
"""
@@ -45,8 +44,7 @@ class RemoteGNS3VM(BaseGNS3VM):
res.append({"vmname": compute.name})
return res
@asyncio.coroutine
def start(self):
async def start(self):
"""
Starts the GNS3 VM.
"""
@@ -65,15 +63,13 @@ class RemoteGNS3VM(BaseGNS3VM):
return
raise GNS3VMError("Can't start the GNS3 VM remote VM {} not found".format(self.vmname))
@asyncio.coroutine
def suspend(self):
async def suspend(self):
"""
Suspend do nothing for remote server
"""
self.running = False
@asyncio.coroutine
def stop(self):
async def stop(self):
"""
Stops the GNS3 VM.
"""
@@ -40,24 +40,22 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
super().__init__(controller)
self._virtualbox_manager = VirtualBox()
@asyncio.coroutine
def _execute(self, subcommand, args, timeout=60):
async def _execute(self, subcommand, args, timeout=60):
try:
result = yield from self._virtualbox_manager.execute(subcommand, args, timeout)
result = await self._virtualbox_manager.execute(subcommand, args, timeout)
return ("\n".join(result))
except VirtualBoxError as e:
raise GNS3VMError("Error while executing VBoxManage command: {}".format(e))
@asyncio.coroutine
def _get_state(self):
async def _get_state(self):
"""
Returns the VM state (e.g. running, paused etc.)
:returns: state (string)
"""
result = yield from self._execute("showvminfo", [self._vmname, "--machinereadable"])
result = await self._execute("showvminfo", [self._vmname, "--machinereadable"])
for info in result.splitlines():
if '=' in info:
name, value = info.split('=', 1)
@@ -65,15 +63,14 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
return value.strip('"')
return "unknown"
@asyncio.coroutine
def _look_for_interface(self, network_backend):
async def _look_for_interface(self, network_backend):
"""
Look for an interface with a specific network backend.
:returns: interface number or -1 if none is found
"""
result = yield from self._execute("showvminfo", [self._vmname, "--machinereadable"])
result = await self._execute("showvminfo", [self._vmname, "--machinereadable"])
interface = -1
for info in result.splitlines():
if '=' in info:
@@ -86,15 +83,14 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
continue
return interface
@asyncio.coroutine
def _look_for_vboxnet(self, interface_number):
async def _look_for_vboxnet(self, interface_number):
"""
Look for the VirtualBox network name associated with a host only interface.
:returns: None or vboxnet name
"""
result = yield from self._execute("showvminfo", [self._vmname, "--machinereadable"])
result = await self._execute("showvminfo", [self._vmname, "--machinereadable"])
for info in result.splitlines():
if '=' in info:
name, value = info.split('=', 1)
@@ -102,8 +98,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
return value.strip('"')
return None
@asyncio.coroutine
def _check_dhcp_server(self, vboxnet):
async def _check_dhcp_server(self, vboxnet):
"""
Check if the DHCP server associated with a vboxnet is enabled.
@@ -111,7 +106,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
:returns: boolean
"""
properties = yield from self._execute("list", ["dhcpservers"])
properties = await self._execute("list", ["dhcpservers"])
flag_dhcp_server_found = False
for prop in properties.splitlines():
try:
@@ -125,8 +120,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
return True
return False
@asyncio.coroutine
def _check_vboxnet_exists(self, vboxnet):
async def _check_vboxnet_exists(self, vboxnet):
"""
Check if the vboxnet interface exists
@@ -134,7 +128,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
:returns: boolean
"""
properties = yield from self._execute("list", ["hostonlyifs"])
properties = await self._execute("list", ["hostonlyifs"])
for prop in properties.splitlines():
try:
name, value = prop.split(':', 1)
@@ -144,13 +138,12 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
return True
return False
@asyncio.coroutine
def _find_first_available_vboxnet(self):
async def _find_first_available_vboxnet(self):
"""
Find the first available vboxnet.
"""
properties = yield from self._execute("list", ["hostonlyifs"])
properties = await self._execute("list", ["hostonlyifs"])
for prop in properties.splitlines():
try:
name, value = prop.split(':', 1)
@@ -160,15 +153,14 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
return value.strip()
return None
@asyncio.coroutine
def _check_vbox_port_forwarding(self):
async def _check_vbox_port_forwarding(self):
"""
Checks if the NAT port forwarding rule exists.
:returns: boolean
"""
result = yield from self._execute("showvminfo", [self._vmname, "--machinereadable"])
result = await self._execute("showvminfo", [self._vmname, "--machinereadable"])
for info in result.splitlines():
if '=' in info:
name, value = info.split('=', 1)
@@ -176,66 +168,64 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
return True
return False
@asyncio.coroutine
def list(self):
async def list(self):
"""
List all VirtualBox VMs
"""
return (yield from self._virtualbox_manager.list_vms())
return (await self._virtualbox_manager.list_vms())
@asyncio.coroutine
def start(self):
async def start(self):
"""
Start the GNS3 VM.
"""
# get a NAT interface number
nat_interface_number = yield from self._look_for_interface("nat")
nat_interface_number = await self._look_for_interface("nat")
if nat_interface_number < 0:
raise GNS3VMError('VM "{}" must have a NAT interface configured in order to start'.format(self.vmname))
hostonly_interface_number = yield from self._look_for_interface("hostonly")
hostonly_interface_number = await self._look_for_interface("hostonly")
if hostonly_interface_number < 0:
raise GNS3VMError('VM "{}" must have a host-only interface configured in order to start'.format(self.vmname))
vboxnet = yield from self._look_for_vboxnet(hostonly_interface_number)
vboxnet = await self._look_for_vboxnet(hostonly_interface_number)
if vboxnet is None:
raise GNS3VMError('A VirtualBox host-only network could not be found on network adapter {} for "{}"'.format(hostonly_interface_number, self._vmname))
if not (yield from self._check_vboxnet_exists(vboxnet)):
if not (await self._check_vboxnet_exists(vboxnet)):
if sys.platform.startswith("win") and vboxnet == "vboxnet0":
# The GNS3 VM is configured with vboxnet0 by default which is not available
# on Windows. Try to patch this with the first available vboxnet we find.
first_available_vboxnet = yield from self._find_first_available_vboxnet()
first_available_vboxnet = await self._find_first_available_vboxnet()
if first_available_vboxnet is None:
raise GNS3VMError('Please add a VirtualBox host-only network with DHCP enabled and attached it to network adapter {} for "{}"'.format(hostonly_interface_number, self._vmname))
yield from self.set_hostonly_network(hostonly_interface_number, first_available_vboxnet)
await self.set_hostonly_network(hostonly_interface_number, first_available_vboxnet)
vboxnet = first_available_vboxnet
else:
raise GNS3VMError('VirtualBox host-only network "{}" does not exist, please make the sure the network adapter {} configuration is valid for "{}"'.format(vboxnet,
hostonly_interface_number,
self._vmname))
if not (yield from self._check_dhcp_server(vboxnet)):
if not (await self._check_dhcp_server(vboxnet)):
raise GNS3VMError('DHCP must be enabled on VirtualBox host-only network "{}"'.format(vboxnet))
vm_state = yield from self._get_state()
vm_state = await self._get_state()
log.info('"{}" state is {}'.format(self._vmname, vm_state))
if vm_state == "poweroff":
yield from self.set_vcpus(self.vcpus)
yield from self.set_ram(self.ram)
await self.set_vcpus(self.vcpus)
await self.set_ram(self.ram)
if vm_state in ("poweroff", "saved"):
# start the VM if it is not running
args = [self._vmname]
if self._headless:
args.extend(["--type", "headless"])
yield from self._execute("startvm", args)
await self._execute("startvm", args)
elif vm_state == "paused":
args = [self._vmname, "resume"]
yield from self._execute("controlvm", args)
await self._execute("controlvm", args)
ip_address = "127.0.0.1"
try:
# get a random port on localhost
@@ -246,23 +236,22 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
except OSError as e:
raise GNS3VMError("Error while getting random port: {}".format(e))
if (yield from self._check_vbox_port_forwarding()):
if (await self._check_vbox_port_forwarding()):
# delete the GNS3VM NAT port forwarding rule if it exists
log.info("Removing GNS3VM NAT port forwarding rule from interface {}".format(nat_interface_number))
yield from self._execute("controlvm", [self._vmname, "natpf{}".format(nat_interface_number), "delete", "GNS3VM"])
await self._execute("controlvm", [self._vmname, "natpf{}".format(nat_interface_number), "delete", "GNS3VM"])
# add a GNS3VM NAT port forwarding rule to redirect 127.0.0.1 with random port to port 3080 in the VM
log.info("Adding GNS3VM NAT port forwarding rule with port {} to interface {}".format(api_port, nat_interface_number))
yield from self._execute("controlvm", [self._vmname, "natpf{}".format(nat_interface_number),
await self._execute("controlvm", [self._vmname, "natpf{}".format(nat_interface_number),
"GNS3VM,tcp,{},{},,3080".format(ip_address, api_port)])
self.ip_address = yield from self._get_ip(hostonly_interface_number, api_port)
self.ip_address = await self._get_ip(hostonly_interface_number, api_port)
self.port = 3080
log.info("GNS3 VM has been started with IP {}".format(self.ip_address))
self.running = True
@asyncio.coroutine
def _get_ip(self, hostonly_interface_number, api_port):
async def _get_ip(self, hostonly_interface_number, api_port):
"""
Get the IP from VirtualBox.
@@ -276,14 +265,14 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
session = aiohttp.ClientSession()
try:
resp = None
resp = yield from session.get('http://127.0.0.1:{}/v2/compute/network/interfaces'.format(api_port))
resp = await session.get('http://127.0.0.1:{}/v2/compute/network/interfaces'.format(api_port))
except (OSError, aiohttp.ClientError, TimeoutError, asyncio.TimeoutError):
pass
if resp:
if resp.status < 300:
try:
json_data = yield from resp.json()
json_data = await resp.json()
except ValueError:
pass
resp.close()
@@ -296,35 +285,33 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
if "ip_address" in interface and len(interface["ip_address"]) > 0:
return interface["ip_address"]
remaining_try -= 1
yield from asyncio.sleep(1)
await asyncio.sleep(1)
raise GNS3VMError("Could not get the GNS3 VM ip make sure the VM receive an IP from VirtualBox")
@asyncio.coroutine
def suspend(self):
async def suspend(self):
"""
Suspend the GNS3 VM.
"""
yield from self._execute("controlvm", [self._vmname, "savestate"], timeout=3)
await self._execute("controlvm", [self._vmname, "savestate"], timeout=3)
log.info("GNS3 VM has been suspend")
self.running = False
@asyncio.coroutine
def stop(self):
async def stop(self):
"""
Stops the GNS3 VM.
"""
vm_state = yield from self._get_state()
vm_state = await self._get_state()
if vm_state == "poweroff":
self.running = False
return
yield from self._execute("controlvm", [self._vmname, "acpipowerbutton"], timeout=3)
await self._execute("controlvm", [self._vmname, "acpipowerbutton"], timeout=3)
trial = 120
while True:
try:
vm_state = yield from self._get_state()
vm_state = await self._get_state()
# During a small amount of time the command will fail
except GNS3VMError:
vm_state = "running"
@@ -332,37 +319,34 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
break
trial -= 1
if trial == 0:
yield from self._execute("controlvm", [self._vmname, "poweroff"], timeout=3)
await self._execute("controlvm", [self._vmname, "poweroff"], timeout=3)
break
yield from asyncio.sleep(1)
await asyncio.sleep(1)
log.info("GNS3 VM has been stopped")
self.running = False
@asyncio.coroutine
def set_vcpus(self, vcpus):
async def set_vcpus(self, vcpus):
"""
Set the number of vCPU cores for the GNS3 VM.
:param vcpus: number of vCPU cores
"""
yield from self._execute("modifyvm", [self._vmname, "--cpus", str(vcpus)], timeout=3)
await self._execute("modifyvm", [self._vmname, "--cpus", str(vcpus)], timeout=3)
log.info("GNS3 VM vCPU count set to {}".format(vcpus))
@asyncio.coroutine
def set_ram(self, ram):
async def set_ram(self, ram):
"""
Set the RAM amount for the GNS3 VM.
:param ram: amount of memory
"""
yield from self._execute("modifyvm", [self._vmname, "--memory", str(ram)], timeout=3)
await self._execute("modifyvm", [self._vmname, "--memory", str(ram)], timeout=3)
log.info("GNS3 VM RAM amount set to {}".format(ram))
@asyncio.coroutine
def set_hostonly_network(self, adapter_number, hostonly_network_name):
async def set_hostonly_network(self, adapter_number, hostonly_network_name):
"""
Set a VirtualBox host-only network on a network adapter for the GNS3 VM.
@@ -370,7 +354,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
:param hostonly_network_name: name of the VirtualBox host-only network
"""
yield from self._execute("modifyvm", [self._vmname, "--hostonlyadapter{}".format(adapter_number), hostonly_network_name], timeout=3)
await self._execute("modifyvm", [self._vmname, "--hostonlyadapter{}".format(adapter_number), hostonly_network_name], timeout=3)
log.info('VirtualBox host-only network "{}" set on network adapter {} for "{}"'.format(hostonly_network_name,
adapter_number,
self._vmname))
+22 -30
View File
@@ -43,24 +43,21 @@ class VMwareGNS3VM(BaseGNS3VM):
def vmx_path(self):
return self._vmx_path
@asyncio.coroutine
def _execute(self, subcommand, args, timeout=60, log_level=logging.INFO):
async def _execute(self, subcommand, args, timeout=60, log_level=logging.INFO):
try:
result = yield from self._vmware_manager.execute(subcommand, args, timeout, log_level=log_level)
result = await self._vmware_manager.execute(subcommand, args, timeout, log_level=log_level)
return (''.join(result))
except VMwareError as e:
raise GNS3VMError("Error while executing VMware command: {}".format(e))
@asyncio.coroutine
def _is_running(self):
result = yield from self._vmware_manager.execute("list", [])
async def _is_running(self):
result = await self._vmware_manager.execute("list", [])
if self._vmx_path in result:
return True
return False
@asyncio.coroutine
def _set_vcpus_ram(self, vcpus, ram):
async def _set_vcpus_ram(self, vcpus, ram):
"""
Set the number of vCPU cores and amount of RAM for the GNS3 VM.
@@ -85,8 +82,7 @@ class VMwareGNS3VM(BaseGNS3VM):
except OSError as e:
raise GNS3VMError('Could not read/write VMware VMX file "{}": {}'.format(self._vmx_path, e))
@asyncio.coroutine
def _set_extra_options(self):
async def _set_extra_options(self):
try:
"""
Due to bug/change in VMWare 14 we're not able to pass Hardware Virtualization in GNS3VM.
@@ -109,23 +105,21 @@ class VMwareGNS3VM(BaseGNS3VM):
except OSError as e:
raise GNS3VMError('Could not read/write VMware VMX file "{}": {}'.format(self._vmx_path, e))
@asyncio.coroutine
def list(self):
async def list(self):
"""
List all VMware VMs
"""
try:
return (yield from self._vmware_manager.list_vms())
return (await self._vmware_manager.list_vms())
except VMwareError as e:
raise GNS3VMError("Could not list VMware VMs: {}".format(str(e)))
@asyncio.coroutine
def start(self):
async def start(self):
"""
Starts the GNS3 VM.
"""
vms = yield from self.list()
vms = await self.list()
for vm in vms:
if vm["vmname"] == self.vmname:
self._vmx_path = vm["vmx_path"]
@@ -138,25 +132,25 @@ class VMwareGNS3VM(BaseGNS3VM):
raise GNS3VMError("VMware VMX file {} doesn't exist".format(self._vmx_path))
# check if the VMware guest tools are installed
vmware_tools_state = yield from self._execute("checkToolsState", [self._vmx_path])
vmware_tools_state = await self._execute("checkToolsState", [self._vmx_path])
if vmware_tools_state not in ("installed", "running"):
raise GNS3VMError("VMware tools are not installed in {}".format(self.vmname))
try:
running = yield from self._is_running()
running = await self._is_running()
except VMwareError as e:
raise GNS3VMError("Could not list VMware VMs: {}".format(str(e)))
if not running:
log.info("Update GNS3 VM settings")
# set the number of vCPUs and amount of RAM
yield from self._set_vcpus_ram(self.vcpus, self.ram)
yield from self._set_extra_options()
await self._set_vcpus_ram(self.vcpus, self.ram)
await self._set_extra_options()
# start the VM
args = [self._vmx_path]
if self._headless:
args.extend(["nogui"])
yield from self._execute("start", args)
await self._execute("start", args)
log.info("GNS3 VM has been started")
# get the guest IP address (first adapter only)
@@ -164,7 +158,7 @@ class VMwareGNS3VM(BaseGNS3VM):
guest_ip_address = ""
log.info("Waiting for GNS3 VM IP")
while True:
guest_ip_address = yield from self._execute("readVariable", [self._vmx_path, "guestVar", "gns3.eth0"], timeout=120, log_level=logging.DEBUG)
guest_ip_address = await self._execute("readVariable", [self._vmx_path, "guestVar", "gns3.eth0"], timeout=120, log_level=logging.DEBUG)
guest_ip_address = guest_ip_address.strip()
if len(guest_ip_address) != 0:
break
@@ -172,15 +166,14 @@ class VMwareGNS3VM(BaseGNS3VM):
# If ip not found fallback on old method
if trial == 0:
log.warning("No IP found for the VM via readVariable fallback to getGuestIPAddress")
guest_ip_address = yield from self._execute("getGuestIPAddress", [self._vmx_path, "-wait"], timeout=120)
guest_ip_address = await self._execute("getGuestIPAddress", [self._vmx_path, "-wait"], timeout=120)
break
yield from asyncio.sleep(1)
await asyncio.sleep(1)
self.ip_address = guest_ip_address
log.info("GNS3 VM IP address set to {}".format(guest_ip_address))
self.running = True
@asyncio.coroutine
def suspend(self):
async def suspend(self):
"""
Suspend the GNS3 VM.
"""
@@ -188,14 +181,13 @@ class VMwareGNS3VM(BaseGNS3VM):
if self._vmx_path is None:
raise GNS3VMError("No VMX path configured, can't suspend the VM")
try:
yield from self._execute("suspend", [self._vmx_path])
await self._execute("suspend", [self._vmx_path])
except GNS3VMError as e:
log.warning("Error when suspending the VM: {}".format(str(e)))
log.info("GNS3 VM has been suspended")
self.running = False
@asyncio.coroutine
def stop(self):
async def stop(self):
"""
Stops the GNS3 VM.
"""
@@ -203,7 +195,7 @@ class VMwareGNS3VM(BaseGNS3VM):
if self._vmx_path is None:
raise GNS3VMError("No VMX path configured, can't stop the VM")
try:
yield from self._execute("stop", [self._vmx_path, "soft"])
await self._execute("stop", [self._vmx_path, "soft"])
except GNS3VMError as e:
log.warning("Error when stopping the VM: {}".format(str(e)))
log.info("GNS3 VM has been stopped")
+10 -13
View File
@@ -33,8 +33,7 @@ Handle the import of project from a .gns3project
"""
@asyncio.coroutine
def import_project(controller, project_id, stream, location=None, name=None, keep_compute_id=False):
async def import_project(controller, project_id, stream, location=None, name=None, keep_compute_id=False):
"""
Import a project contain in a zip file
@@ -87,7 +86,7 @@ def import_project(controller, project_id, stream, location=None, name=None, kee
try:
with zipfile.ZipFile(stream) as zip_file:
yield from wait_run_in_executor(zip_file.extractall, path)
await wait_run_in_executor(zip_file.extractall, path)
except zipfile.BadZipFile:
raise aiohttp.web.HTTPConflict(text="Cannot extract files from GNS3 project (invalid zip)")
@@ -138,9 +137,9 @@ def import_project(controller, project_id, stream, location=None, name=None, kee
# Project created on the remote GNS3 VM?
if node["compute_id"] not in compute_created:
compute = controller.get_compute(node["compute_id"])
yield from compute.post("/projects", data={"name": project_name, "project_id": project_id,})
await compute.post("/projects", data={"name": project_name, "project_id": project_id,})
compute_created.add(node["compute_id"])
yield from _move_files_to_compute(compute, project_id, path, os.path.join("project-files", node["node_type"], node["node_id"]))
await _move_files_to_compute(compute, project_id, path, os.path.join("project-files", node["node_type"], node["node_id"]))
# And we dump the updated.gns3
dot_gns3_path = os.path.join(path, project_name + ".gns3")
@@ -153,7 +152,7 @@ def import_project(controller, project_id, stream, location=None, name=None, kee
if os.path.exists(os.path.join(path, "images")):
_import_images(controller, path)
project = yield from controller.load_project(dot_gns3_path, load=False)
project = await controller.load_project(dot_gns3_path, load=False)
return project
@@ -176,8 +175,7 @@ def _move_node_file(path, old_id, new_id):
shutil.move(node_dir, os.path.join(module_dir, new_id))
@asyncio.coroutine
def _move_files_to_compute(compute, project_id, directory, files_path):
async def _move_files_to_compute(compute, project_id, directory, files_path):
"""
Move files to a remote compute
"""
@@ -188,12 +186,11 @@ def _move_files_to_compute(compute, project_id, directory, files_path):
for filename in filenames:
path = os.path.join(dirpath, filename)
dst = os.path.relpath(path, directory)
yield from _upload_file(compute, project_id, path, dst)
yield from wait_run_in_executor(shutil.rmtree, os.path.join(directory, files_path))
await _upload_file(compute, project_id, path, dst)
await wait_run_in_executor(shutil.rmtree, os.path.join(directory, files_path))
@asyncio.coroutine
def _upload_file(compute, project_id, file_path, path):
async def _upload_file(compute, project_id, file_path, path):
"""
Upload a file to a remote project
@@ -203,7 +200,7 @@ def _upload_file(compute, project_id, file_path, path):
path = "/projects/{}/files/{}".format(project_id, path.replace("\\", "/"))
with open(file_path, "rb") as f:
yield from compute.http_query("POST", path, f, timeout=None)
await compute.http_query("POST", path, f, timeout=None)
def _import_images(controller, path):
+18 -32
View File
@@ -22,8 +22,6 @@ import html
import asyncio
import aiohttp
from gns3server.utils.asyncio import asyncio_ensure_future
import logging
log = logging.getLogger(__name__)
@@ -151,8 +149,7 @@ class Link:
return {"frequency_drop": [-1]}
return self._filters
@asyncio.coroutine
def update_filters(self, filters):
async def update_filters(self, filters):
"""
Modify the filters list.
@@ -173,15 +170,14 @@ class Link:
if new_filters != self.filters:
self._filters = new_filters
if self._created:
yield from self.update()
await self.update()
self._project.controller.notification.project_emit("link.updated", self.__json__())
self._project.dump()
@asyncio.coroutine
def update_suspend(self, value):
async def update_suspend(self, value):
if value != self._suspended:
self._suspended = value
yield from self.update()
await self.update()
self._project.controller.notification.project_emit("link.updated", self.__json__())
self._project.dump()
@@ -192,8 +188,7 @@ class Link:
"""
return self._created
@asyncio.coroutine
def add_node(self, node, adapter_number, port_number, label=None, dump=True):
async def add_node(self, node, adapter_number, port_number, label=None, dump=True):
"""
Add a node to the link
@@ -241,7 +236,7 @@ class Link:
})
if len(self._nodes) == 2:
yield from self.create()
await self.create()
for n in self._nodes:
n["node"].add_link(self)
n["port"].link = self
@@ -251,8 +246,7 @@ class Link:
if dump:
self._project.dump()
@asyncio.coroutine
def update_nodes(self, nodes):
async def update_nodes(self, nodes):
for node_data in nodes:
node = self._project.get_node(node_data["node_id"])
for port in self._nodes:
@@ -263,23 +257,20 @@ class Link:
self._project.controller.notification.project_emit("link.updated", self.__json__())
self._project.dump()
@asyncio.coroutine
def create(self):
async def create(self):
"""
Create the link
"""
raise NotImplementedError
@asyncio.coroutine
def update(self):
async def update(self):
"""
Update a link
"""
raise NotImplementedError
@asyncio.coroutine
def delete(self):
async def delete(self):
"""
Delete the link
"""
@@ -289,8 +280,7 @@ class Link:
n["port"].link = None
n["node"].remove_link(self)
@asyncio.coroutine
def start_capture(self, data_link_type="DLT_EN10MB", capture_file_name=None):
async def start_capture(self, data_link_type="DLT_EN10MB", capture_file_name=None):
"""
Start capture on the link
@@ -299,11 +289,10 @@ class Link:
self._capturing = True
self._capture_file_name = capture_file_name
self._streaming_pcap = asyncio_ensure_future(self._start_streaming_pcap())
self._streaming_pcap = asyncio.ensure_future(self._start_streaming_pcap())
self._project.controller.notification.project_emit("link.updated", self.__json__())
@asyncio.coroutine
def _start_streaming_pcap(self):
async def _start_streaming_pcap(self):
"""
Dump a pcap file on disk
"""
@@ -315,7 +304,7 @@ class Link:
raise aiohttp.web.HTTPConflict(text="Could not delete old capture file '{}': {}".format(self.capture_file_path, e))
try:
stream_content = yield from self.read_pcap_from_source()
stream_content = await self.read_pcap_from_source()
except aiohttp.web.HTTPException as e:
error_msg = "Could not stream PCAP file: error {}: {}".format(e.status, e.text)
log.error(error_msg)
@@ -328,7 +317,7 @@ class Link:
with open(self.capture_file_path, "wb") as f:
while self._capturing:
# We read 1 bytes by 1 otherwise the remaining data is not read if the traffic stops
data = yield from stream.read(1)
data = await stream.read(1)
if data:
f.write(data)
# Flush to disk otherwise the live is not really live
@@ -338,8 +327,7 @@ class Link:
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Could not write capture file '{}': {}".format(self.capture_file_path, e))
@asyncio.coroutine
def stop_capture(self):
async def stop_capture(self):
"""
Stop capture on the link
"""
@@ -347,16 +335,14 @@ class Link:
self._capturing = False
self._project.controller.notification.project_emit("link.updated", self.__json__())
@asyncio.coroutine
def _read_pcap_from_source(self):
async def _read_pcap_from_source(self):
"""
Return a FileStream of the Pcap from the compute server
"""
raise NotImplementedError
@asyncio.coroutine
def node_updated(self, node):
async def node_updated(self, node):
"""
Called when a node member of the link is updated
"""
+35 -49
View File
@@ -343,8 +343,7 @@ class Node:
def links(self):
return self._links
@asyncio.coroutine
def create(self):
async def create(self):
"""
Create the node on the compute server
"""
@@ -358,21 +357,20 @@ class Node:
trial = 0
while trial != 6:
try:
response = yield from self._compute.post("/projects/{}/{}/nodes".format(self._project.id, self._node_type), data=data, timeout=timeout)
response = await self._compute.post("/projects/{}/{}/nodes".format(self._project.id, self._node_type), data=data, timeout=timeout)
except ComputeConflict as e:
if e.response.get("exception") == "ImageMissingError":
res = yield from self._upload_missing_image(self._node_type, e.response["image"])
res = await self._upload_missing_image(self._node_type, e.response["image"])
if not res:
raise e
else:
raise e
else:
yield from self.parse_node_response(response.json)
await self.parse_node_response(response.json)
return True
trial += 1
@asyncio.coroutine
def update(self, **kwargs):
async def update(self, **kwargs):
"""
Update the node on the compute server
@@ -403,15 +401,14 @@ class Node:
self._list_ports()
if update_compute:
data = self._node_data(properties=compute_properties)
response = yield from self.put(None, data=data)
yield from self.parse_node_response(response.json)
response = await self.put(None, data=data)
await self.parse_node_response(response.json)
elif old_json != self.__json__():
# We send notif only if object has changed
self.project.controller.notification.project_emit("node.updated", self.__json__())
self.project.dump()
@asyncio.coroutine
def parse_node_response(self, response):
async def parse_node_response(self, response):
"""
Update the object with the remote node object
"""
@@ -439,7 +436,7 @@ class Node:
self._properties[key] = value
self._list_ports()
for link in self._links:
yield from link.node_updated(self)
await link.node_updated(self)
def _node_data(self, properties=None):
"""
@@ -477,12 +474,10 @@ class Node:
del data[key]
return data
@asyncio.coroutine
def destroy(self):
yield from self.delete()
async def destroy(self):
await self.delete()
@asyncio.coroutine
def start(self, data=None):
async def start(self, data=None):
"""
Start a node
"""
@@ -493,57 +488,52 @@ class Node:
licence = self._project.controller.settings["IOU"]["iourc_content"]
except KeyError:
raise aiohttp.web.HTTPConflict(text="IOU licence is not configured")
yield from self.post("/start", timeout=240, data={"iourc_content": licence})
await self.post("/start", timeout=240, data={"iourc_content": licence})
else:
yield from self.post("/start", data=data, timeout=240)
await self.post("/start", data=data, timeout=240)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when starting {}".format(self._name))
@asyncio.coroutine
def stop(self):
async def stop(self):
"""
Stop a node
"""
try:
yield from self.post("/stop", timeout=240, dont_connect=True)
await self.post("/stop", timeout=240, dont_connect=True)
# We don't care if a node is down at this step
except (ComputeError, aiohttp.ClientError, aiohttp.web.HTTPError):
pass
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when stopping {}".format(self._name))
@asyncio.coroutine
def suspend(self):
async def suspend(self):
"""
Suspend a node
"""
try:
yield from self.post("/suspend", timeout=240)
await self.post("/suspend", timeout=240)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when reloading {}".format(self._name))
@asyncio.coroutine
def reload(self):
async def reload(self):
"""
Suspend a node
"""
try:
yield from self.post("/reload", timeout=240)
await self.post("/reload", timeout=240)
except asyncio.TimeoutError:
raise aiohttp.web.HTTPRequestTimeout(text="Timeout when reloading {}".format(self._name))
@asyncio.coroutine
def post(self, path, data=None, **kwargs):
async def post(self, path, data=None, **kwargs):
"""
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, **kwargs))
return (await self._compute.post("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path), data=data, **kwargs))
else:
return (yield from self._compute.post("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path), **kwargs))
return (await self._compute.post("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path), **kwargs))
@asyncio.coroutine
def put(self, path, data=None, **kwargs):
async def put(self, path, data=None, **kwargs):
"""
HTTP post on the node
"""
@@ -552,22 +542,20 @@ class Node:
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, **kwargs))
return (await self._compute.put(path, data=data, **kwargs))
else:
return (yield from self._compute.put(path, **kwargs))
return (await self._compute.put(path, **kwargs))
@asyncio.coroutine
def delete(self, path=None, **kwargs):
async def delete(self, path=None, **kwargs):
"""
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), **kwargs))
return (await self._compute.delete("/projects/{}/{}/nodes/{}".format(self._project.id, self._node_type, self._id), **kwargs))
else:
return (yield from self._compute.delete("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path), **kwargs))
return (await self._compute.delete("/projects/{}/{}/nodes/{}{}".format(self._project.id, self._node_type, self._id, path), **kwargs))
@asyncio.coroutine
def _upload_missing_image(self, type, img):
async def _upload_missing_image(self, type, img):
"""
Search an image on local computer and upload it to remote compute
if the image exists
@@ -578,26 +566,24 @@ class Node:
self.project.controller.notification.project_emit("log.info", {"message": "Uploading missing image {}".format(img)})
try:
with open(image, 'rb') as f:
yield from self._compute.post("/{}/images/{}".format(self._node_type, os.path.basename(img)), data=f, timeout=None)
await self._compute.post("/{}/images/{}".format(self._node_type, os.path.basename(img)), data=f, timeout=None)
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Can't upload {}: {}".format(image, str(e)))
self.project.controller.notification.project_emit("log.info", {"message": "Upload finished for {}".format(img)})
return True
return False
@asyncio.coroutine
def dynamips_auto_idlepc(self):
async 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
return (await 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):
async 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
return (await self._compute.get("/projects/{}/{}/nodes/{}/idlepc_proposals".format(self._project.id, self._node_type, self._id), timeout=240)).json
def get_port(self, adapter_number, port_number):
"""
+2 -3
View File
@@ -88,8 +88,7 @@ class Notification:
"""
return project.id in self._project_listeners and len(self._project_listeners[project.id]) > 0
@asyncio.coroutine
def dispatch(self, action, event, compute_id):
async def dispatch(self, action, event, compute_id):
"""
Notification received from compute node. Send it directly
to clients or process it
@@ -103,7 +102,7 @@ class Notification:
# Update controller node data and send the event node.updated
project = self._controller.get_project(event["project_id"])
node = project.get_node(event["node_id"])
yield from node.parse_node_response(event)
await node.parse_node_response(event)
self.project_emit("node.updated", node.__json__())
except (aiohttp.web.HTTPNotFound, aiohttp.web.HTTPForbidden): # Project closing
+58 -80
View File
@@ -38,7 +38,6 @@ from ..utils.path import check_path_allowed, get_default_project_directory
from ..utils.asyncio.pool import Pool
from ..utils.asyncio import locking
from ..utils.asyncio import wait_run_in_executor
from ..utils.asyncio import asyncio_ensure_future
from .export_project import export_project
from .import_project import import_project
@@ -122,8 +121,7 @@ class Project:
assert self._status != "closed"
self.dump()
@asyncio.coroutine
def update(self, **kwargs):
async def update(self, **kwargs):
"""
Update the project
:param kwargs: Project properties
@@ -141,7 +139,7 @@ class Project:
# update on computes
for compute in list(self._project_created_on_compute):
yield from compute.put(
await compute.put(
"/projects/{}".format(self._id), {
"variables": self.variables
}
@@ -462,8 +460,7 @@ class Project:
return new_name
@open_required
@asyncio.coroutine
def add_node_from_appliance(self, appliance_id, x=0, y=0, compute_id=None):
async def add_node_from_appliance(self, appliance_id, x=0, y=0, compute_id=None):
"""
Create a node from an appliance
"""
@@ -481,12 +478,11 @@ class Project:
default_name_format = template.pop("default_name_format", "{name}-{0}")
name = default_name_format.replace("{name}", name)
node_id = str(uuid.uuid4())
node = yield from self.add_node(compute, name, node_id, node_type=node_type, appliance_id=appliance_id, **template)
node = await self.add_node(compute, name, node_id, node_type=node_type, appliance_id=appliance_id, **template)
return node
@open_required
@asyncio.coroutine
def add_node(self, compute, name, node_id, dump=True, node_type=None, **kwargs):
async def add_node(self, compute, name, node_id, dump=True, node_type=None, **kwargs):
"""
Create a node or return an existing node
@@ -515,10 +511,10 @@ class Project:
if self._variables:
data["variables"] = self._variables
yield from compute.post("/projects", data=data)
await compute.post("/projects", data=data)
self._project_created_on_compute.add(compute)
yield from node.create()
await node.create()
self._nodes[node.id] = node
self.controller.notification.project_emit("node.created", node.__json__())
if dump:
@@ -526,8 +522,7 @@ class Project:
return node
@locking
@asyncio.coroutine
def __delete_node_links(self, node):
async def __delete_node_links(self, node):
"""
Delete all link connected to this node.
@@ -536,16 +531,15 @@ class Project:
"""
for link in list(self._links.values()):
if node in link.nodes:
yield from self.delete_link(link.id, force_delete=True)
await self.delete_link(link.id, force_delete=True)
@open_required
@asyncio.coroutine
def delete_node(self, node_id):
async def delete_node(self, node_id):
node = self.get_node(node_id)
yield from self.__delete_node_links(node)
await self.__delete_node_links(node)
self.remove_allocated_node_name(node.name)
del self._nodes[node.id]
yield from node.destroy()
await node.destroy()
self.dump()
self.controller.notification.project_emit("node.deleted", node.__json__())
@@ -602,8 +596,7 @@ class Project:
return self._drawings
@open_required
@asyncio.coroutine
def add_drawing(self, drawing_id=None, dump=True, **kwargs):
async def add_drawing(self, drawing_id=None, dump=True, **kwargs):
"""
Create an drawing or return an existing drawing
@@ -630,16 +623,14 @@ class Project:
raise aiohttp.web.HTTPNotFound(text="Drawing ID {} doesn't exist".format(drawing_id))
@open_required
@asyncio.coroutine
def delete_drawing(self, drawing_id):
async def delete_drawing(self, drawing_id):
drawing = self.get_drawing(drawing_id)
del self._drawings[drawing.id]
self.dump()
self.controller.notification.project_emit("drawing.deleted", drawing.__json__())
@open_required
@asyncio.coroutine
def add_link(self, link_id=None, dump=True):
async def add_link(self, link_id=None, dump=True):
"""
Create a link. By default the link is empty
@@ -654,12 +645,11 @@ class Project:
return link
@open_required
@asyncio.coroutine
def delete_link(self, link_id, force_delete=False):
async def delete_link(self, link_id, force_delete=False):
link = self.get_link(link_id)
del self._links[link.id]
try:
yield from link.delete()
await link.delete()
except Exception:
if force_delete is False:
raise
@@ -703,8 +693,7 @@ class Project:
raise aiohttp.web.HTTPNotFound(text="Snapshot ID {} doesn't exist".format(snapshot_id))
@open_required
@asyncio.coroutine
def snapshot(self, name):
async def snapshot(self, name):
"""
Snapshot the project
@@ -714,25 +703,23 @@ class Project:
if name in [snap.name for snap in self._snapshots.values()]:
raise aiohttp.web.HTTPConflict(text="The snapshot name {} already exists".format(name))
snapshot = Snapshot(self, name=name)
yield from snapshot.create()
await snapshot.create()
self._snapshots[snapshot.id] = snapshot
return snapshot
@open_required
@asyncio.coroutine
def delete_snapshot(self, snapshot_id):
async def delete_snapshot(self, snapshot_id):
snapshot = self.get_snapshot(snapshot_id)
del self._snapshots[snapshot.id]
os.remove(snapshot.path)
@asyncio.coroutine
def close(self, ignore_notification=False):
async def close(self, ignore_notification=False):
if self._status == "closed":
return
yield from self.stop_all()
await self.stop_all()
for compute in list(self._project_created_on_compute):
try:
yield from compute.post("/projects/{}/close".format(self._id), dont_connect=True)
await compute.post("/projects/{}/close".format(self._id), dont_connect=True)
# We don't care if a compute is down at this step
except (ComputeError, aiohttp.web.HTTPError, aiohttp.ClientResponseError, TimeoutError):
pass
@@ -771,30 +758,28 @@ class Project:
except OSError as e:
log.warning(str(e))
@asyncio.coroutine
def delete(self):
async def delete(self):
if self._status != "opened":
try:
yield from self.open()
await self.open()
except aiohttp.web.HTTPConflict as e:
# ignore missing images or other conflicts when deleting a project
log.warning("Conflict while deleting project: {}".format(e.text))
yield from self.delete_on_computes()
yield from self.close()
await self.delete_on_computes()
await self.close()
try:
shutil.rmtree(self.path)
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Can not delete project directory {}: {}".format(self.path, str(e)))
@asyncio.coroutine
def delete_on_computes(self):
async def delete_on_computes(self):
"""
Delete the project on computes but not on controller
"""
for compute in list(self._project_created_on_compute):
if compute.id != "local":
yield from compute.delete("/projects/{}".format(self._id))
await compute.delete("/projects/{}".format(self._id))
self._project_created_on_compute.remove(compute)
@classmethod
@@ -817,8 +802,7 @@ class Project:
return os.path.join(self.path, self._filename)
@locking
@asyncio.coroutine
def open(self):
async def open(self):
"""
Load topology elements
"""
@@ -862,19 +846,19 @@ class Project:
topology = project_data["topology"]
for compute in topology.get("computes", []):
yield from self.controller.add_compute(**compute)
await self.controller.add_compute(**compute)
for node in topology.get("nodes", []):
compute = self.controller.get_compute(node.pop("compute_id"))
name = node.pop("name")
node_id = node.pop("node_id", str(uuid.uuid4()))
yield from self.add_node(compute, name, node_id, dump=False, **node)
await self.add_node(compute, name, node_id, dump=False, **node)
for link_data in topology.get("links", []):
if 'link_id' not in link_data.keys():
# skip the link
continue
link = yield from self.add_link(link_id=link_data["link_id"])
link = await self.add_link(link_id=link_data["link_id"])
if "filters" in link_data:
yield from link.update_filters(link_data["filters"])
await link.update_filters(link_data["filters"])
for node_link in link_data.get("nodes", []):
node = self.get_node(node_link["node_id"])
port = node.get_port(node_link["adapter_number"], node_link["port_number"])
@@ -884,19 +868,19 @@ class Project:
if port.link is not None:
log.warning("Port {}/{} is already connected to link ID {}".format(node_link["adapter_number"], node_link["port_number"], port.link.id))
continue
yield from link.add_node(node, node_link["adapter_number"], node_link["port_number"], label=node_link.get("label"), dump=False)
await link.add_node(node, node_link["adapter_number"], node_link["port_number"], label=node_link.get("label"), dump=False)
if len(link.nodes) != 2:
# a link should have 2 attached nodes, this can happen with corrupted projects
yield from self.delete_link(link.id, force_delete=True)
await self.delete_link(link.id, force_delete=True)
for drawing_data in topology.get("drawings", []):
yield from self.add_drawing(dump=False, **drawing_data)
await self.add_drawing(dump=False, **drawing_data)
self.dump()
# We catch all error to be able to rollback the .gns3 to the previous state
except Exception as e:
for compute in list(self._project_created_on_compute):
try:
yield from compute.post("/projects/{}/close".format(self._id))
await compute.post("/projects/{}/close".format(self._id))
# We don't care if a compute is down at this step
except (ComputeError, aiohttp.web.HTTPNotFound, aiohttp.web.HTTPConflict, aiohttp.ServerDisconnectedError):
pass
@@ -922,15 +906,14 @@ class Project:
# Start all in the background without waiting for completion
# we ignore errors because we want to let the user open
# their project and fix it
asyncio_ensure_future(self.start_all())
asyncio.ensure_future(self.start_all())
@asyncio.coroutine
def wait_loaded(self):
async def wait_loaded(self):
"""
Wait until the project finish loading
"""
while self._loading:
yield from asyncio.sleep(0.5)
await asyncio.sleep(0.5)
def _create_duplicate_project_file(self, path, zipstream):
"""
@@ -941,8 +924,7 @@ class Project:
for data in zipstream:
f.write(data)
@asyncio.coroutine
def duplicate(self, name=None, location=None):
async def duplicate(self, name=None, location=None):
"""
Duplicate a project
@@ -956,22 +938,22 @@ class Project:
# If the project was not open we open it temporary
previous_status = self._status
if self._status == "closed":
yield from self.open()
await self.open()
self.dump()
assert self._status != "closed"
try:
with tempfile.TemporaryDirectory() as tmpdir:
zipstream = yield from export_project(self, tmpdir, keep_compute_id=True, allow_all_nodes=True)
zipstream = await export_project(self, tmpdir, keep_compute_id=True, allow_all_nodes=True)
project_path = os.path.join(tmpdir, "project.gns3p")
yield from wait_run_in_executor(self._create_duplicate_project_file, project_path, zipstream)
await wait_run_in_executor(self._create_duplicate_project_file, project_path, zipstream)
with open(project_path, "rb") as f:
project = yield from import_project(self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_id=True)
project = await import_project(self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_id=True)
except (ValueError, OSError, UnicodeEncodeError) as e:
raise aiohttp.web.HTTPConflict(text="Cannot duplicate project: {}".format(str(e)))
if previous_status == "closed":
yield from self.close()
await self.close()
return project
@@ -999,38 +981,34 @@ class Project:
except OSError as e:
raise aiohttp.web.HTTPInternalServerError(text="Could not write topology: {}".format(e))
@asyncio.coroutine
def start_all(self):
async def start_all(self):
"""
Start all nodes
"""
pool = Pool(concurrency=3)
for node in self.nodes.values():
pool.append(node.start)
yield from pool.join()
await pool.join()
@asyncio.coroutine
def stop_all(self):
async def stop_all(self):
"""
Stop all nodes
"""
pool = Pool(concurrency=3)
for node in self.nodes.values():
pool.append(node.stop)
yield from pool.join()
await pool.join()
@asyncio.coroutine
def suspend_all(self):
async def suspend_all(self):
"""
Suspend all nodes
"""
pool = Pool(concurrency=3)
for node in self.nodes.values():
pool.append(node.suspend)
yield from pool.join()
await pool.join()
@asyncio.coroutine
def duplicate_node(self, node, x, y, z):
async def duplicate_node(self, node, x, y, z):
"""
Duplicate a node
@@ -1061,21 +1039,21 @@ class Project:
data['y'] = y
data['z'] = z
new_node_uuid = str(uuid.uuid4())
new_node = yield from self.add_node(
new_node = await self.add_node(
node.compute,
node.name,
new_node_uuid,
node_type=node_type,
**data)
try:
yield from node.post("/duplicate", timeout=None, data={
await node.post("/duplicate", timeout=None, data={
"destination_node_id": new_node_uuid
})
except aiohttp.web.HTTPNotFound as e:
yield from self.delete_node(new_node_uuid)
await self.delete_node(new_node_uuid)
raise aiohttp.web.HTTPConflict(text="This node type cannot be duplicated")
except aiohttp.web.HTTPConflict as e:
yield from self.delete_node(new_node_uuid)
await self.delete_node(new_node_uuid)
raise e
return new_node
+9 -11
View File
@@ -82,8 +82,7 @@ class Snapshot:
for data in zipstream:
f.write(data)
@asyncio.coroutine
def create(self):
async def create(self):
"""
Create the snapshot
"""
@@ -99,31 +98,30 @@ class Snapshot:
try:
with tempfile.TemporaryDirectory() as tmpdir:
zipstream = yield from export_project(self._project, tmpdir, keep_compute_id=True, allow_all_nodes=True)
yield from wait_run_in_executor(self._create_snapshot_file, zipstream)
zipstream = await export_project(self._project, tmpdir, keep_compute_id=True, allow_all_nodes=True)
await wait_run_in_executor(self._create_snapshot_file, zipstream)
except (ValueError, OSError, RuntimeError) as e:
raise aiohttp.web.HTTPConflict(text="Could not create snapshot file '{}': {}".format(self.path, e))
@asyncio.coroutine
def restore(self):
async def restore(self):
"""
Restore the snapshot
"""
yield from self._project.delete_on_computes()
await self._project.delete_on_computes()
# We don't send close notification to clients because the close / open dance is purely internal
yield from self._project.close(ignore_notification=True)
await self._project.close(ignore_notification=True)
try:
# delete the current project files
project_files_path = os.path.join(self._project.path, "project-files")
if os.path.exists(project_files_path):
yield from wait_run_in_executor(shutil.rmtree, project_files_path)
await wait_run_in_executor(shutil.rmtree, project_files_path)
with open(self._path, "rb") as f:
project = yield from import_project(self._project.controller, self._project.id, f, location=self._project.path)
project = await import_project(self._project.controller, self._project.id, f, location=self._project.path)
except (OSError, PermissionError) as e:
raise aiohttp.web.HTTPConflict(text=str(e))
yield from project.open()
await project.open()
self._project.controller.notification.project_emit("snapshot.restored", self.__json__())
return self._project
+23 -30
View File
@@ -37,8 +37,7 @@ class UDPLink(Link):
"""
return self._link_data
@asyncio.coroutine
def create(self):
async def create(self):
"""
Create the link on the nodes
"""
@@ -52,14 +51,14 @@ class UDPLink(Link):
# Get an IP allowing communication between both host
try:
(node1_host, node2_host) = yield from node1.compute.get_ip_on_same_subnet(node2.compute)
(node1_host, node2_host) = await node1.compute.get_ip_on_same_subnet(node2.compute)
except ValueError as e:
raise aiohttp.web.HTTPConflict(text="Cannot get an IP address on same subnet: {}".format(e))
# Reserve a UDP port on both side
response = yield from node1.compute.post("/projects/{}/ports/udp".format(self._project.id))
response = await node1.compute.post("/projects/{}/ports/udp".format(self._project.id))
self._node1_port = response.json["udp_port"]
response = yield from node2.compute.post("/projects/{}/ports/udp".format(self._project.id))
response = await node2.compute.post("/projects/{}/ports/udp".format(self._project.id))
self._node2_port = response.json["udp_port"]
node1_filters = {}
@@ -79,7 +78,7 @@ class UDPLink(Link):
"filters": node1_filters,
"suspend": self._suspended
})
yield from node1.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), data=self._link_data[0], timeout=120)
await node1.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), data=self._link_data[0], timeout=120)
self._link_data.append({
"lport": self._node2_port,
@@ -90,15 +89,14 @@ class UDPLink(Link):
"suspend": self._suspended
})
try:
yield from node2.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), data=self._link_data[1], timeout=120)
await node2.post("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), data=self._link_data[1], timeout=120)
except Exception as e:
# We clean the first NIO
yield from node1.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), timeout=120)
await node1.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), timeout=120)
raise e
self._created = True
@asyncio.coroutine
def update(self):
async def update(self):
"""
Update the link on the nodes
"""
@@ -120,16 +118,15 @@ class UDPLink(Link):
port_number1 = self._nodes[0]["port_number"]
self._link_data[0]["filters"] = node1_filters
self._link_data[0]["suspend"] = self._suspended
yield from node1.put("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), data=self._link_data[0], timeout=120)
await node1.put("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), data=self._link_data[0], timeout=120)
adapter_number2 = self._nodes[1]["adapter_number"]
port_number2 = self._nodes[1]["port_number"]
self._link_data[1]["filters"] = node2_filters
self._link_data[1]["suspend"] = self._suspended
yield from node2.put("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), data=self._link_data[1], timeout=221)
await node2.put("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), data=self._link_data[1], timeout=221)
@asyncio.coroutine
def delete(self):
async def delete(self):
"""
Delete the link and free the resources
"""
@@ -142,7 +139,7 @@ class UDPLink(Link):
except IndexError:
return
try:
yield from node1.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), timeout=120)
await node1.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number1, port_number=port_number1), timeout=120)
# If the node is already delete (user selected multiple element and delete all in the same time)
except aiohttp.web.HTTPNotFound:
pass
@@ -154,14 +151,13 @@ class UDPLink(Link):
except IndexError:
return
try:
yield from node2.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), timeout=120)
await node2.delete("/adapters/{adapter_number}/ports/{port_number}/nio".format(adapter_number=adapter_number2, port_number=port_number2), timeout=120)
# If the node is already delete (user selected multiple element and delete all in the same time)
except aiohttp.web.HTTPNotFound:
pass
yield from super().delete()
await super().delete()
@asyncio.coroutine
def start_capture(self, data_link_type="DLT_EN10MB", capture_file_name=None):
async def start_capture(self, data_link_type="DLT_EN10MB", capture_file_name=None):
"""
Start capture on a link
"""
@@ -172,18 +168,17 @@ class UDPLink(Link):
"capture_file_name": capture_file_name,
"data_link_type": data_link_type
}
yield from self._capture_node["node"].post("/adapters/{adapter_number}/ports/{port_number}/start_capture".format(adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"]), data=data)
yield from super().start_capture(data_link_type=data_link_type, capture_file_name=capture_file_name)
await self._capture_node["node"].post("/adapters/{adapter_number}/ports/{port_number}/start_capture".format(adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"]), data=data)
await super().start_capture(data_link_type=data_link_type, capture_file_name=capture_file_name)
@asyncio.coroutine
def stop_capture(self):
async def stop_capture(self):
"""
Stop capture on a link
"""
if self._capture_node:
yield from self._capture_node["node"].post("/adapters/{adapter_number}/ports/{port_number}/stop_capture".format(adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"]))
await self._capture_node["node"].post("/adapters/{adapter_number}/ports/{port_number}/stop_capture".format(adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"]))
self._capture_node = None
yield from super().stop_capture()
await super().stop_capture()
def _choose_capture_side(self):
"""
@@ -215,8 +210,7 @@ class UDPLink(Link):
raise aiohttp.web.HTTPConflict(text="Cannot capture because there is no running device on this link")
@asyncio.coroutine
def read_pcap_from_source(self):
async def read_pcap_from_source(self):
"""
Return a FileStream of the Pcap from the compute node
"""
@@ -224,10 +218,9 @@ class UDPLink(Link):
compute = self._capture_node["node"].compute
return compute.stream_file(self._project, "tmp/captures/" + self._capture_file_name)
@asyncio.coroutine
def node_updated(self, node):
async def node_updated(self, node):
"""
Called when a node member of the link is updated
"""
if self._capture_node and node == self._capture_node["node"] and node.status != "started":
yield from self.stop_capture()
await self.stop_capture()