#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import sys
import copy
import asyncio
import aiohttp
import ipaddress
from ...utils.asyncio import locking, asyncio_ensure_future
from .vmware_gns3_vm import VMwareGNS3VM
from .virtualbox_gns3_vm import VirtualBoxGNS3VM
from .hyperv_gns3_vm import HyperVGNS3VM
from .remote_gns3_vm import RemoteGNS3VM
from .gns3_vm_error import GNS3VMError
from ...version import __version__
from ..compute import ComputeError
import logging
log = logging.getLogger(__name__)
class GNS3VM:
"""
Proxy between the controller and the GNS3 VM engine
"""
def __init__(self, controller, settings={}):
self._controller = controller
# Keep instance of the loaded engines
self._engines = {}
self._settings = {
"vmname": None,
"when_exit": "stop",
"headless": False,
"enable": False,
"engine": "vmware",
"ram": 2048,
"vcpus": 1
}
self.settings = settings
def engine_list(self):
"""
:returns: Return list of engines supported by GNS3 for the GNS3VM
"""
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VMware.Workstation.{version}.zip".format(version=__version__)
vmware_info = {
"engine_id": "vmware",
"description": 'VMware is the recommended choice for best performances.
The GNS3 VM can be downloaded here.'.format(download_url),
"support_when_exit": True,
"support_headless": True,
"support_ram": True
}
if sys.platform.startswith("darwin"):
vmware_info["name"] = "VMware Fusion (recommended)"
else:
vmware_info["name"] = "VMware Workstation / Player (recommended)"
hyperv_info = {
"engine_id": "hyper-v",
"name": "Hyper-V",
"description": 'Hyper-V support (Windows 10/Server 2016 and above). Nested virtualization must be supported and enabled (Intel processor only)',
"support_when_exit": True,
"support_headless": False,
"support_ram": True
}
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VirtualBox.{version}.zip".format(version=__version__)
virtualbox_info = {
"engine_id": "virtualbox",
"name": "VirtualBox (deprecated)",
"description": 'VirtualBox doesn\'t support nested virtualization, this means Qemu based VMs will run extremely slowly. This feature is marked as deprecated and support may be removed from future GNS3 releases.
The GNS3 VM can be downloaded here'.format(download_url),
"support_when_exit": True,
"support_headless": True,
"support_ram": True
}
remote_info = {
"engine_id": "remote",
"name": "Remote",
"description": "Use a remote GNS3 server as the GNS3 VM.",
"support_when_exit": False,
"support_headless": False,
"support_ram": False
}
engines = [vmware_info,
virtualbox_info,
remote_info]
if sys.platform.startswith("win"):
engines.append(hyperv_info)
return engines
def current_engine(self):
return self._get_engine(self._settings["engine"])
@property
def engine(self):
return self._settings["engine"]
@property
def ip_address(self):
"""
Returns the GNS3 VM IP address.
:returns: VM IP address
"""
return self.current_engine().ip_address
@property
def running(self):
"""
Returns if the GNS3 VM is running.
:returns: Boolean
"""
return self.current_engine().running
@property
def user(self):
"""
Returns the GNS3 VM user.
:returns: VM user
"""
return self.current_engine().user
@property
def password(self):
"""
Returns the GNS3 VM password.
:returns: VM password
"""
return self.current_engine().password
@property
def port(self):
"""
Returns the GNS3 VM port.
:returns: VM port
"""
return self.current_engine().port
@property
def protocol(self):
"""
Returns the GNS3 VM protocol.
:returns: VM protocol
"""
return self.current_engine().protocol
@property
def enable(self):
"""
The GNSVM is activated
"""
return self._settings.get("enable", False)
@property
def when_exit(self):
"""
What should be done when exit
"""
return self._settings["when_exit"]
@property
def settings(self):
return self._settings
@settings.setter
def settings(self, val):
self._settings.update(val)
@asyncio.coroutine
def update_settings(self, settings):
"""
Update settings and will restart the VM if require
"""
new_settings = copy.copy(self._settings)
new_settings.update(settings)
if self.settings != new_settings:
yield from self._stop()
self._settings = settings
self._controller.save()
if self.enable:
yield from 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()
def _get_engine(self, engine):
"""
Load an engine
"""
if engine in self._engines:
return self._engines[engine]
if engine == "vmware":
self._engines["vmware"] = VMwareGNS3VM(self._controller)
return self._engines["vmware"]
elif engine == "hyper-v":
self._engines["hyper-v"] = HyperVGNS3VM(self._controller)
return self._engines["hyper-v"]
elif engine == "virtualbox":
self._engines["virtualbox"] = VirtualBoxGNS3VM(self._controller)
return self._engines["virtualbox"]
elif engine == "remote":
self._engines["remote"] = RemoteGNS3VM(self._controller)
return self._engines["remote"]
raise NotImplementedError("The engine {} for the GNS3 VM is not supported".format(engine))
def __json__(self):
return self._settings
@asyncio.coroutine
def list(self, engine):
"""
List VMS for an engine
"""
engine = self._get_engine(engine)
vms = []
try:
for vm in (yield from engine.list()):
vms.append({"vmname": vm["vmname"]})
except GNS3VMError as e:
# We raise error only if user activated the GNS3 VM
# otherwise you have noise when VMware is not installed
if self.enable:
raise e
return vms
@asyncio.coroutine
def auto_start_vm(self):
"""
Auto start the GNS3 VM if require
"""
if self.enable:
try:
yield from 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",
name="GNS3 VM ({})".format(self.current_engine().vmname),
host=None,
force=True)
compute.set_last_error(str(e))
except aiohttp.web.HTTPConflict:
pass
log.error("Cannot start the GNS3 VM: {}".format(e))
@asyncio.coroutine
def exit_vm(self):
if self.enable:
try:
if self._settings["when_exit"] == "stop":
yield from self._stop()
elif self._settings["when_exit"] == "suspend":
yield from self._suspend()
except GNS3VMError as e:
log.warning(str(e))
@locking
@asyncio.coroutine
def start(self):
"""
Start the GNS3 VM
"""
engine = self.current_engine()
if not engine.running:
if self._settings["vmname"] is None:
return
log.info("Start the GNS3 VM")
engine.vmname = self._settings["vmname"]
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",
name="GNS3 VM is starting ({})".format(engine.vmname),
host=None,
force=True,
connect=False)
try:
yield from engine.start()
except Exception as e:
yield from 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))
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),
protocol=self.protocol,
host=self.ip_address,
port=self.port,
user=self.user,
password=self.password)
# 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.coroutine
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_interface_netmask = None
for interface in vm_interfaces:
if interface["ip_address"] == self.ip_address:
vm_interface_netmask = interface["netmask"]
break
if vm_interface_netmask:
vm_network = ipaddress.ip_interface("{}/{}".format(compute.host_ip, vm_interface_netmask)).network
for compute_id in self._controller.computes:
if compute_id == "local":
compute = self._controller.get_compute(compute_id)
interfaces = yield from compute.interfaces()
netmask = None
for interface in interfaces:
if interface["ip_address"] == compute.host_ip:
netmask = interface["netmask"]
break
if netmask:
compute_network = ipaddress.ip_interface("{}/{}".format(compute.host_ip, netmask)).network
if vm_network.compare_networks(compute_network) != 0:
msg = "The GNS3 VM ({}) is not on the same network as the {} server ({}), please make sure the local server binding is in the same network as the GNS3 VM".format(
vm_network, compute_id, compute_network)
self._controller.notification.controller_emit("log.warning", {"message": msg})
except ComputeError as e:
log.warning("Could not check the VM is in the same subnet as the local server: {}".format(e))
except aiohttp.web.HTTPConflict as e:
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):
"""
Suspend the GNS3 VM
"""
engine = self.current_engine()
if "vm" in self._controller.computes:
yield from self._controller.delete_compute("vm")
if engine.running:
log.info("Suspend the GNS3 VM")
yield from engine.suspend()
@locking
@asyncio.coroutine
def _stop(self):
"""
Stop the GNS3 VM
"""
engine = self.current_engine()
if "vm" in self._controller.computes:
yield from self._controller.delete_compute("vm")
if engine.running:
log.info("Stop the GNS3 VM")
yield from engine.stop()