#!/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
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)
async 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:
await self._stop()
self._settings = settings
self._controller.save()
if self.enable:
await self.start()
else:
# When user fix something on his system and try again
if self.enable and not self.current_engine().running:
await 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
async def list(self, engine):
"""
List VMS for an engine
"""
engine = self._get_engine(engine)
vms = []
try:
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
# otherwise you have noise when VMware is not installed
if self.enable:
raise e
return vms
async def auto_start_vm(self):
"""
Auto start the GNS3 VM if require
"""
if self.enable:
try:
await self.start()
except GNS3VMError as e:
# User will receive the error later when they will try to use the node
try:
compute = await 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))
async def exit_vm(self):
if self.enable:
try:
if self._settings["when_exit"] == "stop":
await self._stop()
elif self._settings["when_exit"] == "suspend":
await self._suspend()
except GNS3VMError as e:
log.warning(str(e))
@locking
async 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 = await self._controller.add_compute(compute_id="vm",
name="GNS3 VM is starting ({})".format(engine.vmname),
host=None,
force=True,
connect=False)
try:
await engine.start()
except Exception as e:
await self._controller.delete_compute("vm")
log.error("Cannot start the GNS3 VM: {}".format(str(e)))
await compute.update(name="GNS3 VM ({})".format(engine.vmname))
compute.set_last_error(str(e))
raise e
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,
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)))
async def _check_network(self, compute):
"""
Check that the VM is in the same subnet as the local server
"""
try:
vm_interfaces = await 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 = await 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 (IP={}, NETWORK={}) is not on the same network as the {} server (IP={}, NETWORK={}), please make sure the local server binding is in the same network as the GNS3 VM".format(self.ip_address,
vm_network,
compute_id,
compute.host_ip,
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
async def _suspend(self):
"""
Suspend the GNS3 VM
"""
engine = self.current_engine()
if "vm" in self._controller.computes:
await self._controller.delete_compute("vm")
if engine.running:
log.info("Suspend the GNS3 VM")
await engine.suspend()
@locking
async def _stop(self):
"""
Stop the GNS3 VM
"""
engine = self.current_engine()
if "vm" in self._controller.computes:
await self._controller.delete_compute("vm")
if engine.running:
log.info("Stop the GNS3 VM")
await engine.stop()