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
2020-10-22 08:49:44 +03:00
from gns3server . utils . http_client import HTTPClient
2020-01-26 12:23:17 +02:00
from gns3server . utils . asyncio import wait_run_in_executor
2016-06-24 06:56:42 +03:00
2021-04-13 12:16:50 +03:00
from . . . compute . virtualbox import VirtualBox , VirtualBoxError
2016-06-24 06:56:42 +03:00
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 )
2021-04-13 12:16:50 +03:00
return " \n " . join ( result )
2016-06-24 06:56:42 +03:00
except VirtualBoxError as e :
2021-04-13 12:07:58 +03:00
raise GNS3VMError ( f " Error while executing VBoxManage command: { e } " )
2016-06-24 06:56:42 +03:00
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 ( ) :
2021-04-13 12:16:50 +03:00
if " = " in info :
name , value = info . split ( " = " , 1 )
2016-06-24 06:56:42 +03:00
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 :
2021-04-13 12:16:50 +03:00
name , value = prop . split ( " : " , 1 )
2020-01-26 12:23:17 +02:00
except ValueError :
continue
self . _system_properties [ name . strip ( ) ] = value . strip ( )
2023-08-09 13:51:53 +03:00
if " API Version " in self . _system_properties :
# API version is not consistent between VirtualBox versions, the key is named "API Version" in VirtualBox 7
self . _system_properties [ " API version " ] = self . _system_properties . pop ( " API Version " )
2020-01-26 12:23:17 +02:00
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 :
2021-04-13 12:07:58 +03:00
raise VirtualBoxError ( f " Can ' t access to VirtualBox API version: \n { self . _system_properties } " )
2020-01-26 12:23:17 +02:00
from cpuinfo import get_cpu_info
2021-04-13 12:16:50 +03:00
2020-01-26 12:23:17 +02:00
cpu_info = await wait_run_in_executor ( get_cpu_info )
2021-04-13 12:16:50 +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 " ) :
2021-04-13 12:16:50 +03:00
raise VirtualBoxError (
" VirtualBox version 6.1 or above is required to run the GNS3 VM with nested virtualization enabled on Intel processors "
)
2020-01-26 12:23:17 +02:00
elif vendor_id == " AuthenticAMD " :
if parse_version ( self . _system_properties [ " API version " ] ) < parse_version ( " 6_0 " ) :
2021-04-13 12:16:50 +03:00
raise VirtualBoxError (
" VirtualBox version 6.0 or above is required to run the GNS3 VM with nested virtualization enabled on AMD processors "
)
2020-01-26 12:23:17 +02:00
else :
2021-04-13 12:07:58 +03:00
log . warning ( f " Could not determine CPU vendor: { vendor_id } " )
2020-01-26 12:23:17 +02:00
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 ( ) :
2021-04-13 12:16:50 +03:00
if " = " in info :
name , value = info . split ( " = " , 1 )
2016-06-24 06:56:42 +03:00
if name . startswith ( " nic " ) and value . strip ( ' " ' ) == network_backend :
try :
interface = int ( name [ 3 : ] )
break
except ValueError :
continue
return interface
2023-05-30 14:52:57 +03:00
async def _look_for_vboxnet ( self , backend_type , interface_number ) :
2016-06-24 06:56:42 +03:00
"""
2023-05-30 14:52:57 +03:00
Look for the VirtualBox network name associated with an interface .
2016-06-24 06:56:42 +03:00
: 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 )
2023-05-30 14:52:57 +03:00
if name == " {} {} " . format ( backend_type , interface_number ) :
2016-06-24 06:56:42 +03:00
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 :
2021-04-13 12:16:50 +03:00
name , value = prop . split ( " : " , 1 )
2016-06-24 06:56:42 +03:00
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
2023-05-31 13:49:13 +03:00
async def _check_vboxnet_exists ( self , vboxnet , vboxnet_type ) :
2018-08-26 12:43:40 +03:00
"""
Check if the vboxnet interface exists
: param vboxnet : vboxnet name
: returns : boolean
"""
2023-05-31 13:49:13 +03:00
properties = await self . _execute ( " list " , [ " {} " . format ( vboxnet_type ) ] )
2018-08-26 12:43:40 +03:00
for prop in properties . splitlines ( ) :
try :
2021-04-13 12:16:50 +03:00
name , value = prop . split ( " : " , 1 )
2018-08-26 12:43:40 +03:00
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 :
2021-04-13 12:16:50 +03:00
name , value = prop . split ( " : " , 1 )
2018-08-26 13:28:38 +03:00
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 ( ) :
2021-04-13 12:16:50 +03:00
if " = " in info :
name , value = info . split ( " = " , 1 )
2016-06-24 06:56:42 +03:00
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 :
2021-04-13 12:07:58 +03:00
raise GNS3VMError ( f " Could not list VirtualBox VMs: { 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 :
2021-04-13 12:07:58 +03:00
raise GNS3VMError ( f ' VM " { self . vmname } " must have a NAT interface configured in order to start ' )
2016-06-24 06:56:42 +03:00
2023-05-30 14:52:57 +03:00
if sys . platform . startswith ( " darwin " ) and parse_version ( self . _system_properties [ " API version " ] ) > = parse_version ( " 7_0 " ) :
2023-05-31 14:09:25 +03:00
# VirtualBox 7.0+ on macOS requires a host-only network interface
2023-05-30 14:52:57 +03:00
backend_type = " hostonly-network "
backend_description = " host-only network "
2023-05-31 14:09:25 +03:00
vboxnet_type = " hostonlynets "
2023-05-30 14:52:57 +03:00
interface_number = await self . _look_for_interface ( " hostonlynetwork " )
if interface_number < 0 :
raise GNS3VMError ( ' VM " {} " must have a network adapter attached to a host-only network in order to start ' . format ( self . vmname ) )
else :
backend_type = " hostonlyadapter "
backend_description = " host-only adapter "
2023-05-31 14:09:25 +03:00
vboxnet_type = " hostonlyifs "
2023-05-30 14:52:57 +03:00
interface_number = await self . _look_for_interface ( " hostonly " )
2016-06-24 06:56:42 +03:00
2023-05-30 14:52:57 +03:00
if interface_number < 0 :
raise GNS3VMError ( ' VM " {} " must have a network adapter attached to a {} in order to start ' . format ( self . vmname , backend_description ) )
2016-06-24 06:56:42 +03:00
2023-05-30 14:52:57 +03:00
vboxnet = await self . _look_for_vboxnet ( backend_type , interface_number )
2016-06-24 06:56:42 +03:00
if vboxnet is None :
2023-05-30 14:52:57 +03:00
raise GNS3VMError ( ' A VirtualBox host-only network could not be found on network adapter {} for " {} " ' . format ( interface_number , self . _vmname ) )
2018-08-26 12:43:40 +03:00
2023-05-31 13:49:13 +03:00
if not ( await self . _check_vboxnet_exists ( vboxnet , vboxnet_type ) ) :
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 :
2023-05-30 14:52:57 +03:00
raise GNS3VMError ( ' Please add a VirtualBox host-only network with DHCP enabled and attached it to network adapter {} for " {} " ' . format ( interface_number , self . _vmname ) )
await self . set_hostonly_network ( 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 ,
2023-05-30 14:52:57 +03:00
interface_number ,
2018-08-26 13:28:38 +03:00
self . _vmname ) )
2016-06-24 06:56:42 +03:00
2023-05-31 14:09:25 +03:00
if backend_type == " hostonlyadapter " and 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 ( )
2021-04-13 12:07:58 +03:00
log . info ( f ' " { self . _vmname } " state is { vm_state } ' )
2016-09-21 16:46:56 +03:00
if vm_state == " poweroff " :
2020-11-05 02:43:57 +02:00
if self . allocate_vcpus_ram :
log . info ( " Update GNS3 VM vCPUs and RAM settings " )
await self . set_vcpus ( self . vcpus )
await self . set_ram ( self . ram )
log . info ( " Update GNS3 VM Hardware Virtualization setting " )
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 :
2021-04-13 12:07:58 +03:00
raise GNS3VMError ( f " Error while getting random port: { e } " )
2016-06-24 06:56:42 +03:00
2021-04-13 12:16:50 +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
2021-04-13 12:07:58 +03:00
log . info ( f " Removing GNS3VM NAT port forwarding rule from interface { nat_interface_number } " )
await self . _execute ( " controlvm " , [ self . _vmname , f " natpf { 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
2021-04-13 12:07:58 +03:00
log . info ( f " Adding GNS3VM NAT port forwarding rule with port { api_port } to interface { nat_interface_number } " )
2021-04-13 12:16:50 +03:00
await self . _execute (
" controlvm " ,
[ self . _vmname , f " natpf { nat_interface_number } " , f " GNS3VM,tcp, { ip_address } , { api_port } ,, { self . port } " ] ,
)
2016-08-30 17:38:19 +03:00
2023-05-30 14:52:57 +03:00
self . ip_address = await self . _get_ip ( 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 .
"""
2020-10-22 08:49:44 +03:00
2017-02-23 17:19:20 +02:00
remaining_try = 300
2016-08-30 17:38:19 +03:00
while remaining_try > 0 :
2020-10-22 08:49:44 +03:00
try :
2020-11-02 03:35:32 +02:00
async with HTTPClient . get ( f " http://127.0.0.1: { api_port } /v3/compute/network/interfaces " ) as resp :
2020-10-22 08:49:44 +03:00
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 (
2021-04-13 12:16:50 +03:00
hostonly_interface_number - 1
) :
2020-10-22 08:49:44 +03:00
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 )
2021-04-13 12:07:58 +03:00
raise GNS3VMError ( f " Could not find guest IP address for { 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 )
2021-04-13 12:07:58 +03:00
log . info ( f " GNS3 VM vCPU count set to { vcpus } " )
2016-06-24 06:56:42 +03:00
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 )
2021-04-13 12:07:58 +03:00
log . info ( f " GNS3 VM RAM amount set to { 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
"""
2021-04-13 12:16:50 +03:00
await self . _execute (
" modifyvm " , [ self . _vmname , f " --hostonlyadapter { 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
)
)