diff --git a/gns3server/controller/gns3vm/__init__.py b/gns3server/controller/gns3vm/__init__.py index 7dad3ca5..ea5a30a4 100644 --- a/gns3server/controller/gns3vm/__init__.py +++ b/gns3server/controller/gns3vm/__init__.py @@ -24,6 +24,7 @@ import ipaddress from ...utils.asyncio import locked_coroutine, 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__ @@ -71,6 +72,15 @@ class GNS3VM: 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": True, + "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", @@ -90,11 +100,14 @@ class GNS3VM: "support_ram": False } - return [ - vmware_info, - virtualbox_info, - remote_info - ] + engines = [vmware_info, + virtualbox_info, + remote_info] + + if sys.platform.startswith("win"): + engines.append(hyperv_info) + + return engines def current_engine(self): @@ -221,6 +234,9 @@ class GNS3VM: 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"] diff --git a/gns3server/controller/gns3vm/hyperv_gns3_vm.py b/gns3server/controller/gns3vm/hyperv_gns3_vm.py new file mode 100644 index 00000000..89218971 --- /dev/null +++ b/gns3server/controller/gns3vm/hyperv_gns3_vm.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 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 logging +import asyncio +import psutil + +if sys.platform.startswith("win"): + import wmi + +from .base_gns3_vm import BaseGNS3VM +from .gns3_vm_error import GNS3VMError +log = logging.getLogger(__name__) + + +class HyperVGNS3VM(BaseGNS3VM): + + _HYPERV_VM_STATE_ENABLED = 2 + _HYPERV_VM_STATE_DISABLED = 3 + _HYPERV_VM_STATE_PAUSED = 9 + + _WMI_JOB_STATUS_STARTED = 4096 + _WMI_JOB_STATE_RUNNING = 4 + _WMI_JOB_STATE_COMPLETED = 7 + + def __init__(self, controller): + + self._engine = "hyper-v" + super().__init__(controller) + self._conn = None + self._vm = None + self._management = None + + def _check_requirements(self): + """ + Checks if the GNS3 VM can run on Hyper-V. + """ + + if not sys.platform.startswith("win") or sys.getwindowsversion().major < 10: + raise GNS3VMError("Hyper-V nested virtualization is only supported on Windows 10 and Windows Server 2016 or later") + + conn = wmi.WMI() + + if conn.Win32_Processor()[0].Manufacturer != "GenuineIntel": + raise GNS3VMError("An Intel processor is required by Hyper-V to support nested virtualization") + + if not conn.Win32_ComputerSystem()[0].HypervisorPresent: + raise GNS3VMError("Hyper-V is not installed") + + if not conn.Win32_Processor()[0].VirtualizationFirmwareEnabled: + raise GNS3VMError("Nested Virtualization (VT-x) is not enabled on this system") + + def _connect(self): + """ + Connects to local host using WMI. + """ + + self._check_requirements() + + try: + self._conn = wmi.WMI(namespace=r"root\virtualization\v2") + except wmi.x_wmi as e: + print("Could not connect to WMI {}".format(e)) + + if not self._conn.Msvm_VirtualSystemManagementService(): + raise GNS3VMError("The Windows account running GNS3 does not have the required permissions for Hyper-V") + + self._management = self._conn.Msvm_VirtualSystemManagementService()[0] + self._vm = self._find_vm(self.vmname) + + def _find_vm(self, vm_name): + """ + Finds a Hyper-V VM. + """ + + vms = self._conn.Msvm_ComputerSystem(ElementName=vm_name) + nb_vms = len(vms) + if nb_vms == 0: + return None + elif nb_vms > 1: + raise GNS3VMError("Duplicate VM name found for {}".format(vm_name)) + else: + return vms[0] + + def _is_running(self): + """ + Checks if the VM is running. + """ + + if self._vm is not None and self._vm.EnabledState == HyperVGNS3VM._HYPERV_VM_STATE_ENABLED: + return True + return False + + def _set_vcpus_ram(self, vcpus, ram): + """ + Set the number of vCPU cores and amount of RAM for the GNS3 VM. + + :param vcpus: number of vCPU cores + :param ram: amount of RAM + """ + + available_vcpus = psutil.cpu_count(logical=False) + if vcpus > available_vcpus: + raise GNS3VMError("You have allocated too many vCPUs for the GNS3 VM! (max available is {} vCPUs)".format(available_vcpus)) + + try: + vm_settings = self._vm.associators(wmi_result_class='Msvm_VirtualSystemSettingData')[0] + mem_settings = vm_settings.associators(wmi_result_class='Msvm_MemorySettingData')[0] + cpu_settings = vm_settings.associators(wmi_result_class='Msvm_ProcessorSettingData')[0] + + mem_settings.VirtualQuantity = ram + mem_settings.Reservation = ram + mem_settings.Limit = ram + self._management.ModifyResourceSettings(ResourceSettings=[mem_settings.GetText_(1)]) + + cpu_settings.VirtualQuantity = vcpus + cpu_settings.Reservation = vcpus + cpu_settings.Limit = 100000 # use 100% of CPU + cpu_settings.ExposeVirtualizationExtensions = True # allow the VM to use nested virtualization + self._management.ModifyResourceSettings(ResourceSettings=[cpu_settings.GetText_(1)]) + + log.info("GNS3 VM vCPU count set to {} and RAM amount set to {}".format(vcpus, ram)) + except Exception as e: + raise GNS3VMError("Could not set to {} and RAM amount set to {}: {}".format(vcpus, ram, e)) + + @asyncio.coroutine + def list(self): + """ + List all Hyper-V VMs + """ + + vms = [] + try: + for vm in self._conn.Msvm_ComputerSystem(): + if vm.Caption == "Virtual Machine": + vms.append(vm.ElementName) + except wmi.x_wmi as e: + raise GNS3VMError("Could not list Hyper-V VMs: {}".format(e)) + return vms + + def _get_wmi_obj(self, path): + """ + Gets the WMI object. + """ + + return wmi.WMI(moniker=path.replace('\\', '/')) + + @asyncio.coroutine + def _set_state(self, state): + """ + Set the desired state of the VM + """ + + job_path, ret = self._vm.RequestStateChange(state) + 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) + 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): + """ + Starts the GNS3 VM. + """ + + if self._conn is None: + self._connect() + + if not self._is_running(): + + log.info("Update GNS3 VM settings") + # set the number of vCPUs and amount of RAM + self._set_vcpus_ram(self.vcpus, self.ram) + + # start the VM + try: + yield from 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") + + #TODO: get the guest IP address + #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): + """ + Suspend the GNS3 VM. + """ + + if self._conn is None: + self._connect() + + try: + yield from 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): + """ + Stops the GNS3 VM. + """ + + if self._conn is None: + self._connect() + + try: + yield from self._set_state(HyperVGNS3VM._HYPERV_VM_STATE_DISABLED) + except GNS3VMError as e: + raise GNS3VMError("Failed to stop the GNS3 VM: {}".format(e)) + log.info("GNS3 VM has been stopped") + self.running = False diff --git a/win-requirements.txt b/win-requirements.txt index 017a1d07..851b9264 100644 --- a/win-requirements.txt +++ b/win-requirements.txt @@ -1,3 +1,4 @@ -rrequirements.txt pywin32>=223 # pyup: ignore +wmi==1.4.9