2016-06-24 06:56:42 +03:00
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2018-08-26 13:28:38 +03:00
import sys
2016-08-30 17:38:19 +03:00
import aiohttp
2016-06-24 06:56:42 +03:00
import logging
import asyncio
import socket
from . base_gns3_vm import BaseGNS3VM
from . gns3_vm_error import GNS3VMError
2020-01-26 12:23:17 +02:00
from gns3server . utils import parse_version
from gns3server . utils . asyncio import wait_run_in_executor
2016-06-24 06:56:42 +03:00
2016-08-24 12:36:32 +03:00
from . . . compute . virtualbox import (
2016-06-24 06:56:42 +03:00
VirtualBox ,
VirtualBoxError
)
log = logging . getLogger ( __name__ )
class VirtualBoxGNS3VM ( BaseGNS3VM ) :
2016-08-30 11:19:01 +03:00
def __init__ ( self , controller ) :
2016-06-24 06:56:42 +03:00
2016-07-12 02:01:18 +03:00
self . _engine = " virtualbox "
2016-08-30 17:38:19 +03:00
super ( ) . __init__ ( controller )
2020-01-26 12:23:17 +02:00
self . _system_properties = { }
2016-06-24 06:56:42 +03:00
self . _virtualbox_manager = VirtualBox ( )
2018-10-15 13:05:49 +03:00
async def _execute ( self , subcommand , args , timeout = 60 ) :
2016-06-24 06:56:42 +03:00
try :
2018-10-15 13:05:49 +03:00
result = await self . _virtualbox_manager . execute ( subcommand , args , timeout )
2016-08-30 17:38:19 +03:00
return ( " \n " . join ( result ) )
2016-06-24 06:56:42 +03:00
except VirtualBoxError as e :
raise GNS3VMError ( " Error while executing VBoxManage command: {} " . format ( e ) )
2018-10-15 13:05:49 +03:00
async def _get_state ( self ) :
2016-06-24 06:56:42 +03:00
"""
Returns the VM state ( e . g . running , paused etc . )
: returns : state ( string )
"""
2018-10-15 13:05:49 +03:00
result = await self . _execute ( " showvminfo " , [ self . _vmname , " --machinereadable " ] )
2016-06-24 06:56:42 +03:00
for info in result . splitlines ( ) :
if ' = ' in info :
name , value = info . split ( ' = ' , 1 )
if name == " VMState " :
return value . strip ( ' " ' )
return " unknown "
2020-01-26 12:23:17 +02:00
async def _get_system_properties ( self ) :
"""
Returns the VM state ( e . g . running , paused etc . )
: returns : state ( string )
"""
properties = await self . _execute ( " list " , [ " systemproperties " ] )
for prop in properties . splitlines ( ) :
try :
name , value = prop . split ( ' : ' , 1 )
except ValueError :
continue
self . _system_properties [ name . strip ( ) ] = value . strip ( )
async def _check_requirements ( self ) :
"""
Checks if the GNS3 VM can run on VirtualBox
"""
if not self . _system_properties :
await self . _get_system_properties ( )
if " API version " not in self . _system_properties :
raise VirtualBoxError ( " Can ' t access to VirtualBox API version: \n {} " . format ( self . _system_properties ) )
from cpuinfo import get_cpu_info
cpu_info = await wait_run_in_executor ( get_cpu_info )
2020-07-10 11:14:38 +03:00
vendor_id = cpu_info . get ( ' vendor_id_raw ' )
2020-01-26 12:23:17 +02:00
if vendor_id == " GenuineIntel " :
if parse_version ( self . _system_properties [ " API version " ] ) < parse_version ( " 6_1 " ) :
raise VirtualBoxError ( " VirtualBox version 6.1 or above is required to run the GNS3 VM with nested virtualization enabled on Intel processors " )
elif vendor_id == " AuthenticAMD " :
if parse_version ( self . _system_properties [ " API version " ] ) < parse_version ( " 6_0 " ) :
raise VirtualBoxError ( " VirtualBox version 6.0 or above is required to run the GNS3 VM with nested virtualization enabled on AMD processors " )
else :
log . warning ( " Could not determine CPU vendor: {} " . format ( vendor_id ) )
2018-10-15 13:05:49 +03:00
async def _look_for_interface ( self , network_backend ) :
2016-06-24 06:56:42 +03:00
"""
Look for an interface with a specific network backend .
: returns : interface number or - 1 if none is found
"""
2018-10-15 13:05:49 +03:00
result = await self . _execute ( " showvminfo " , [ self . _vmname , " --machinereadable " ] )
2016-06-24 06:56:42 +03:00
interface = - 1
for info in result . splitlines ( ) :
if ' = ' in info :
name , value = info . split ( ' = ' , 1 )
if name . startswith ( " nic " ) and value . strip ( ' " ' ) == network_backend :
try :
interface = int ( name [ 3 : ] )
break
except ValueError :
continue
return interface
2018-10-15 13:05:49 +03:00
async def _look_for_vboxnet ( self , interface_number ) :
2016-06-24 06:56:42 +03:00
"""
Look for the VirtualBox network name associated with a host only interface .
: returns : None or vboxnet name
"""
2018-10-15 13:05:49 +03:00
result = await self . _execute ( " showvminfo " , [ self . _vmname , " --machinereadable " ] )
2016-06-24 06:56:42 +03:00
for info in result . splitlines ( ) :
if ' = ' in info :
name , value = info . split ( ' = ' , 1 )
if name == " hostonlyadapter {} " . format ( interface_number ) :
return value . strip ( ' " ' )
return None
2018-10-15 13:05:49 +03:00
async def _check_dhcp_server ( self , vboxnet ) :
2016-06-24 06:56:42 +03:00
"""
Check if the DHCP server associated with a vboxnet is enabled .
: param vboxnet : vboxnet name
: returns : boolean
"""
2018-10-15 13:05:49 +03:00
properties = await self . _execute ( " list " , [ " dhcpservers " ] )
2016-06-24 06:56:42 +03:00
flag_dhcp_server_found = False
for prop in properties . splitlines ( ) :
try :
name , value = prop . split ( ' : ' , 1 )
except ValueError :
continue
if name . strip ( ) == " NetworkName " and value . strip ( ) . endswith ( vboxnet ) :
flag_dhcp_server_found = True
if flag_dhcp_server_found and name . strip ( ) == " Enabled " :
if value . strip ( ) == " Yes " :
return True
return False
2018-10-15 13:05:49 +03:00
async def _check_vboxnet_exists ( self , vboxnet ) :
2018-08-26 12:43:40 +03:00
"""
Check if the vboxnet interface exists
: param vboxnet : vboxnet name
: returns : boolean
"""
2018-10-15 13:05:49 +03:00
properties = await self . _execute ( " list " , [ " hostonlyifs " ] )
2018-08-26 12:43:40 +03:00
for prop in properties . splitlines ( ) :
try :
name , value = prop . split ( ' : ' , 1 )
except ValueError :
continue
if name . strip ( ) == " Name " and value . strip ( ) == vboxnet :
return True
return False
2018-10-15 13:05:49 +03:00
async def _find_first_available_vboxnet ( self ) :
2018-08-26 13:28:38 +03:00
"""
Find the first available vboxnet .
"""
2018-10-15 13:05:49 +03:00
properties = await self . _execute ( " list " , [ " hostonlyifs " ] )
2018-08-26 13:28:38 +03:00
for prop in properties . splitlines ( ) :
try :
name , value = prop . split ( ' : ' , 1 )
except ValueError :
continue
if name . strip ( ) == " Name " :
return value . strip ( )
return None
2018-10-15 13:05:49 +03:00
async def _check_vbox_port_forwarding ( self ) :
2016-06-24 06:56:42 +03:00
"""
Checks if the NAT port forwarding rule exists .
: returns : boolean
"""
2018-10-15 13:05:49 +03:00
result = await self . _execute ( " showvminfo " , [ self . _vmname , " --machinereadable " ] )
2016-06-24 06:56:42 +03:00
for info in result . splitlines ( ) :
if ' = ' in info :
name , value = info . split ( ' = ' , 1 )
if name . startswith ( " Forwarding " ) and value . strip ( ' " ' ) . startswith ( " GNS3VM " ) :
return True
return False
2018-10-15 13:05:49 +03:00
async def list ( self ) :
2016-07-12 02:01:18 +03:00
"""
List all VirtualBox VMs
"""
2020-05-20 12:12:50 +03:00
try :
await self . _check_requirements ( )
return await self . _virtualbox_manager . list_vms ( )
except VirtualBoxError as e :
raise GNS3VMError ( " Could not list VirtualBox VMs: {} " . format ( str ( e ) ) )
2016-07-12 02:01:18 +03:00
2018-10-15 13:05:49 +03:00
async def start ( self ) :
2016-06-24 06:56:42 +03:00
"""
Start the GNS3 VM .
"""
2020-01-26 12:23:17 +02:00
await self . _check_requirements ( )
2016-06-24 06:56:42 +03:00
# get a NAT interface number
2018-10-15 13:05:49 +03:00
nat_interface_number = await self . _look_for_interface ( " nat " )
2016-06-24 06:56:42 +03:00
if nat_interface_number < 0 :
2018-08-26 13:28:38 +03:00
raise GNS3VMError ( ' VM " {} " must have a NAT interface configured in order to start ' . format ( self . vmname ) )
2016-06-24 06:56:42 +03:00
2018-10-15 13:05:49 +03:00
hostonly_interface_number = await self . _look_for_interface ( " hostonly " )
2016-06-24 06:56:42 +03:00
if hostonly_interface_number < 0 :
2018-08-26 13:28:38 +03:00
raise GNS3VMError ( ' VM " {} " must have a host-only interface configured in order to start ' . format ( self . vmname ) )
2016-06-24 06:56:42 +03:00
2018-10-15 13:05:49 +03:00
vboxnet = await self . _look_for_vboxnet ( hostonly_interface_number )
2016-06-24 06:56:42 +03:00
if vboxnet is None :
2018-08-26 12:43:40 +03:00
raise GNS3VMError ( ' A VirtualBox host-only network could not be found on network adapter {} for " {} " ' . format ( hostonly_interface_number , self . _vmname ) )
2018-10-15 13:05:49 +03:00
if not ( await self . _check_vboxnet_exists ( vboxnet ) ) :
2018-08-26 13:28:38 +03:00
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.
2018-10-15 13:05:49 +03:00
first_available_vboxnet = await self . _find_first_available_vboxnet ( )
2018-08-26 13:28:38 +03:00
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 ) )
2018-10-15 13:05:49 +03:00
await self . set_hostonly_network ( hostonly_interface_number , first_available_vboxnet )
2018-08-26 13:28:38 +03:00
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 ) )
2016-06-24 06:56:42 +03:00
2018-10-15 13:05:49 +03:00
if not ( await self . _check_dhcp_server ( vboxnet ) ) :
2018-08-26 12:43:40 +03:00
raise GNS3VMError ( ' DHCP must be enabled on VirtualBox host-only network " {} " ' . format ( vboxnet ) )
2016-06-24 06:56:42 +03:00
2018-10-15 13:05:49 +03:00
vm_state = await self . _get_state ( )
2016-06-24 06:56:42 +03:00
log . info ( ' " {} " state is {} ' . format ( self . _vmname , vm_state ) )
2016-09-21 16:46:56 +03:00
if vm_state == " poweroff " :
2019-10-09 09:50:00 +03:00
log . info ( " Update GNS3 VM settings (CPU, RAM and Hardware Virtualization) " )
2018-10-15 13:05:49 +03:00
await self . set_vcpus ( self . vcpus )
await self . set_ram ( self . ram )
2019-10-09 09:50:00 +03:00
await self . enable_nested_hw_virt ( )
2016-09-21 16:46:56 +03:00
2016-06-24 06:56:42 +03:00
if vm_state in ( " poweroff " , " saved " ) :
# start the VM if it is not running
args = [ self . _vmname ]
if self . _headless :
args . extend ( [ " --type " , " headless " ] )
2018-10-15 13:05:49 +03:00
await self . _execute ( " startvm " , args )
2016-09-08 16:32:35 +03:00
elif vm_state == " paused " :
args = [ self . _vmname , " resume " ]
2018-10-15 13:05:49 +03:00
await self . _execute ( " controlvm " , args )
2016-06-24 06:56:42 +03:00
ip_address = " 127.0.0.1 "
try :
# get a random port on localhost
with socket . socket ( ) as s :
2018-04-16 11:36:36 +03:00
s . setsockopt ( socket . SOL_SOCKET , socket . SO_REUSEADDR , 1 )
2016-06-24 06:56:42 +03:00
s . bind ( ( ip_address , 0 ) )
2016-08-30 17:38:19 +03:00
api_port = s . getsockname ( ) [ 1 ]
2016-06-24 06:56:42 +03:00
except OSError as e :
raise GNS3VMError ( " Error while getting random port: {} " . format ( e ) )
2018-10-15 13:05:49 +03:00
if ( await self . _check_vbox_port_forwarding ( ) ) :
2016-06-24 06:56:42 +03:00
# delete the GNS3VM NAT port forwarding rule if it exists
log . info ( " Removing GNS3VM NAT port forwarding rule from interface {} " . format ( nat_interface_number ) )
2018-10-15 13:05:49 +03:00
await self . _execute ( " controlvm " , [ self . _vmname , " natpf {} " . format ( nat_interface_number ) , " delete " , " GNS3VM " ] )
2016-06-24 06:56:42 +03:00
2020-05-05 06:10:50 +03:00
# add a GNS3VM NAT port forwarding rule to redirect 127.0.0.1 with random port to the port in the VM
2016-08-30 17:38:19 +03:00
log . info ( " Adding GNS3VM NAT port forwarding rule with port {} to interface {} " . format ( api_port , nat_interface_number ) )
2018-10-15 13:05:49 +03:00
await self . _execute ( " controlvm " , [ self . _vmname , " natpf {} " . format ( nat_interface_number ) ,
2020-05-05 06:10:50 +03:00
" GNS3VM,tcp, {} , {} ,, {} " . format ( ip_address , api_port , self . port ) ] )
2016-08-30 17:38:19 +03:00
2018-10-15 13:05:49 +03:00
self . ip_address = await self . _get_ip ( hostonly_interface_number , api_port )
2016-08-30 17:38:19 +03:00
log . info ( " GNS3 VM has been started with IP {} " . format ( self . ip_address ) )
self . running = True
2018-10-15 13:05:49 +03:00
async def _get_ip ( self , hostonly_interface_number , api_port ) :
2016-08-30 17:38:19 +03:00
"""
Get the IP from VirtualBox .
Due to VirtualBox limitation the only way is to send request each
second to a GNS3 endpoint in order to get the list of the interfaces and
their IP and after that match it with VirtualBox host only .
"""
2017-02-23 17:19:20 +02:00
remaining_try = 300
2016-08-30 17:38:19 +03:00
while remaining_try > 0 :
2018-10-16 11:56:06 +03:00
async with aiohttp . ClientSession ( ) as session :
try :
async with session . get ( ' http://127.0.0.1: {} /v2/compute/network/interfaces ' . format ( api_port ) ) as resp :
if resp . status < 300 :
try :
json_data = await resp . json ( )
if json_data :
for interface in json_data :
if " name " in interface and interface [ " name " ] == " eth {} " . format (
hostonly_interface_number - 1 ) :
if " ip_address " in interface and len ( interface [ " ip_address " ] ) > 0 :
return interface [ " ip_address " ]
except ValueError :
pass
except ( OSError , aiohttp . ClientError , TimeoutError , asyncio . TimeoutError ) :
pass
2016-08-30 17:38:19 +03:00
remaining_try - = 1
2018-10-15 13:05:49 +03:00
await asyncio . sleep ( 1 )
2019-10-08 12:58:27 +03:00
raise GNS3VMError ( " Could not find guest IP address for {} " . format ( self . vmname ) )
2016-06-24 06:56:42 +03:00
2018-10-15 13:05:49 +03:00
async def suspend ( self ) :
2016-09-08 16:32:35 +03:00
"""
Suspend the GNS3 VM .
"""
2018-10-15 13:05:49 +03:00
await self . _execute ( " controlvm " , [ self . _vmname , " savestate " ] , timeout = 3 )
2016-09-08 16:32:35 +03:00
log . info ( " GNS3 VM has been suspend " )
self . running = False
2018-10-15 13:05:49 +03:00
async def stop ( self ) :
2016-06-24 06:56:42 +03:00
"""
Stops the GNS3 VM .
"""
2018-10-15 13:05:49 +03:00
vm_state = await self . _get_state ( )
2016-09-21 18:01:50 +03:00
if vm_state == " poweroff " :
self . running = False
return
2018-10-15 13:05:49 +03:00
await self . _execute ( " controlvm " , [ self . _vmname , " acpipowerbutton " ] , timeout = 3 )
2016-09-21 18:01:50 +03:00
trial = 120
while True :
try :
2018-10-15 13:05:49 +03:00
vm_state = await self . _get_state ( )
2016-09-21 18:01:50 +03:00
# During a small amount of time the command will fail
except GNS3VMError :
vm_state = " running "
if vm_state == " poweroff " :
break
trial - = 1
if trial == 0 :
2018-10-15 13:05:49 +03:00
await self . _execute ( " controlvm " , [ self . _vmname , " poweroff " ] , timeout = 3 )
2016-09-21 18:01:50 +03:00
break
2018-10-15 13:05:49 +03:00
await asyncio . sleep ( 1 )
2016-09-21 18:01:50 +03:00
2016-09-08 16:32:35 +03:00
log . info ( " GNS3 VM has been stopped " )
2016-06-24 06:56:42 +03:00
self . running = False
2018-10-15 13:05:49 +03:00
async def set_vcpus ( self , vcpus ) :
2016-06-24 06:56:42 +03:00
"""
Set the number of vCPU cores for the GNS3 VM .
: param vcpus : number of vCPU cores
"""
2018-10-15 13:05:49 +03:00
await self . _execute ( " modifyvm " , [ self . _vmname , " --cpus " , str ( vcpus ) ] , timeout = 3 )
2016-06-24 06:56:42 +03:00
log . info ( " GNS3 VM vCPU count set to {} " . format ( vcpus ) )
2018-10-15 13:05:49 +03:00
async def set_ram ( self , ram ) :
2016-06-24 06:56:42 +03:00
"""
Set the RAM amount for the GNS3 VM .
: param ram : amount of memory
"""
2018-10-15 13:05:49 +03:00
await self . _execute ( " modifyvm " , [ self . _vmname , " --memory " , str ( ram ) ] , timeout = 3 )
2016-06-24 06:56:42 +03:00
log . info ( " GNS3 VM RAM amount set to {} " . format ( ram ) )
2018-08-26 13:28:38 +03:00
2019-10-09 09:50:00 +03:00
async def enable_nested_hw_virt ( self ) :
"""
Enable nested hardware virtualization for the GNS3 VM .
"""
await self . _execute ( " modifyvm " , [ self . _vmname , " --nested-hw-virt " , " on " ] , timeout = 3 )
log . info ( " Nested hardware virtualization enabled " )
2018-10-15 13:05:49 +03:00
async def set_hostonly_network ( self , adapter_number , hostonly_network_name ) :
2018-08-26 13:28:38 +03:00
"""
Set a VirtualBox host - only network on a network adapter for the GNS3 VM .
: param adapter_number : network adapter number
: param hostonly_network_name : name of the VirtualBox host - only network
"""
2018-10-15 13:05:49 +03:00
await self . _execute ( " modifyvm " , [ self . _vmname , " --hostonlyadapter {} " . format ( adapter_number ) , hostonly_network_name ] , timeout = 3 )
2018-08-26 13:28:38 +03:00
log . info ( ' VirtualBox host-only network " {} " set on network adapter {} for " {} " ' . format ( hostonly_network_name ,
adapter_number ,
self . _vmname ) )