2015-02-19 12:33:25 +02:00
#
# Copyright (C) 2014 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/>.
"""
2015-04-08 20:17:34 +03:00
QEMU VM management ( creates command line , processes , files etc . ) in
order to run a QEMU VM .
2015-02-19 12:33:25 +02:00
"""
import sys
import os
2015-06-10 16:49:24 +03:00
import re
2019-10-09 12:47:22 +03:00
import shlex
2017-02-07 18:04:29 +02:00
import math
2015-02-19 12:33:25 +02:00
import shutil
2020-04-15 21:50:59 +03:00
import struct
2015-02-19 17:46:57 +02:00
import asyncio
2015-03-24 06:52:02 +02:00
import socket
2015-06-03 07:33:38 +03:00
import gns3server
2017-02-07 18:04:29 +02:00
import subprocess
2018-03-22 19:07:32 +02:00
import time
2018-03-30 15:28:22 +03:00
import json
2022-01-19 13:58:36 +02:00
import shlex
2015-02-19 12:33:25 +02:00
2022-01-19 13:58:36 +02:00
from gns3server . utils import parse_version
2018-01-29 15:20:48 +02:00
from gns3server . utils . asyncio import subprocess_check_output , cancellable_wait_run_in_executor
2015-02-19 12:33:25 +02:00
from . qemu_error import QemuError
2019-06-04 19:00:44 +03:00
from . utils . qcow2 import Qcow2 , Qcow2Error
2020-04-07 15:11:00 +03:00
from . utils . ziputils import pack_zip , unpack_zip
2015-02-19 12:33:25 +02:00
from . . adapters . ethernet_adapter import EthernetAdapter
2020-04-06 13:56:00 +03:00
from . . error import NodeError , ImageMissingError
2015-02-24 04:00:34 +02:00
from . . nios . nio_udp import NIOUDP
2015-04-27 23:38:15 +03:00
from . . nios . nio_tap import NIOTAP
2016-05-11 20:35:36 +03:00
from . . base_node import BaseNode
2015-03-04 17:01:56 +02:00
from . . . utils . asyncio import monitor_process
2015-06-17 18:11:25 +03:00
from . . . utils . images import md5sum
2016-04-25 17:36:20 +03:00
from . . . utils import macaddress_to_int , int_to_macaddress
2021-04-15 11:42:08 +03:00
from gns3server . schemas . compute . qemu_nodes import Qemu , QemuPlatform
2015-02-19 12:33:25 +02:00
import logging
2021-04-13 12:16:50 +03:00
2015-02-19 12:33:25 +02:00
log = logging . getLogger ( __name__ )
2016-05-11 20:35:36 +03:00
class QemuVM ( BaseNode ) :
2021-04-13 12:16:50 +03:00
module_name = " qemu "
2015-02-19 12:33:25 +02:00
"""
QEMU VM implementation .
2015-04-08 20:17:34 +03:00
: param name : Qemu VM name
2016-05-11 20:35:36 +03:00
: param node_id : Node identifier
2015-02-19 12:33:25 +02:00
: param project : Project instance
2015-04-08 20:17:34 +03:00
: param manager : Manager instance
2015-02-19 12:33:25 +02:00
: param console : TCP console port
2018-03-24 14:11:21 +03:00
: param console_type : Console type
2015-02-19 12:33:25 +02:00
: param qemu_path : path to the QEMU binary
2015-06-10 16:49:24 +03:00
: param platform : Platform to emulate
2015-02-19 12:33:25 +02:00
"""
2021-04-13 12:16:50 +03:00
def __init__ (
self ,
name ,
node_id ,
project ,
manager ,
linked_clone = True ,
qemu_path = None ,
console = None ,
console_type = " telnet " ,
aux = None ,
aux_type = " none " ,
platform = None ,
) :
super ( ) . __init__ (
name ,
node_id ,
project ,
manager ,
console = console ,
console_type = console_type ,
linked_clone = linked_clone ,
aux = aux ,
aux_type = aux_type ,
wrap_console = True ,
wrap_aux = True ,
)
2021-04-12 10:32:23 +03:00
self . _host = manager . config . settings . Server . host
self . _monitor_host = manager . config . settings . Qemu . monitor_host
2015-02-19 12:33:25 +02:00
self . _process = None
self . _cpulimit_process = None
2015-03-24 06:52:02 +02:00
self . _monitor = None
2015-02-19 12:33:25 +02:00
self . _stdout_file = " "
2017-11-17 13:13:34 +02:00
self . _qemu_img_stdout_file = " "
2015-08-27 17:06:11 +03:00
self . _execute_lock = asyncio . Lock ( )
2016-06-24 01:56:06 +03:00
self . _local_udp_tunnels = { }
2019-06-05 12:25:35 +03:00
self . _guest_cid = None
2020-09-12 16:37:39 +03:00
self . _command_line_changed = False
2015-02-19 12:33:25 +02:00
2015-04-08 20:17:34 +03:00
# QEMU VM settings
2015-06-10 16:49:24 +03:00
if qemu_path :
try :
self . qemu_path = qemu_path
2020-09-12 16:37:39 +03:00
except QemuError :
2016-05-18 12:23:45 +03:00
# If the binary is not found for topologies 1.4 and later
# search via the platform otherwise use the binary name
2015-06-10 16:49:24 +03:00
if platform :
self . platform = platform
else :
2016-01-28 17:14:26 +02:00
self . qemu_path = os . path . basename ( qemu_path )
2015-06-10 16:49:24 +03:00
else :
self . platform = platform
2015-02-19 12:33:25 +02:00
self . _hda_disk_image = " "
self . _hdb_disk_image = " "
2015-03-10 19:50:30 +02:00
self . _hdc_disk_image = " "
self . _hdd_disk_image = " "
2020-08-15 09:44:16 +03:00
self . _hda_disk_interface = " none "
self . _hdb_disk_interface = " none "
self . _hdc_disk_interface = " none "
self . _hdd_disk_interface = " none "
2015-08-03 08:02:02 +03:00
self . _cdrom_image = " "
2016-12-08 17:18:30 +02:00
self . _bios_image = " "
2015-08-03 08:02:02 +03:00
self . _boot_priority = " c "
2015-06-08 07:18:41 +03:00
self . _mac_address = " "
2015-02-19 12:33:25 +02:00
self . _options = " "
self . _ram = 256
2015-08-06 02:17:55 +03:00
self . _cpus = 1
2020-07-24 08:45:41 +03:00
self . _maxcpus = 1
2015-02-19 12:33:25 +02:00
self . _ethernet_adapters = [ ]
self . _adapter_type = " e1000 "
self . _initrd = " "
self . _kernel_image = " "
self . _kernel_command_line = " "
2020-06-02 12:15:22 +03:00
self . _replicate_network_connection_state = True
2020-08-14 11:27:24 +03:00
self . _create_config_disk = False
2018-03-30 17:18:44 +03:00
self . _on_close = " power_off "
2015-02-19 12:33:25 +02:00
self . _cpu_throttling = 0 # means no CPU throttling
self . _process_priority = " low "
2015-06-08 07:18:41 +03:00
self . mac_address = " " # this will generate a MAC address
2015-02-19 12:33:25 +02:00
self . adapters = 1 # creates 1 adapter by default
2020-04-06 13:56:00 +03:00
# config disk
2020-06-28 17:35:39 +03:00
self . config_disk_name = self . manager . config_disk
2020-04-16 12:07:56 +03:00
self . config_disk_image = " "
2020-08-13 10:40:31 +03:00
if self . config_disk_name :
if not shutil . which ( " mcopy " ) :
log . warning ( " Config disk: ' mtools ' are not installed. " )
2020-06-28 17:35:39 +03:00
self . config_disk_name = " "
2020-08-13 10:40:31 +03:00
else :
try :
self . config_disk_image = self . manager . get_abs_image_path ( self . config_disk_name )
2020-08-14 11:27:24 +03:00
except ( NodeError , ImageMissingError ) :
2021-04-13 12:07:58 +03:00
log . warning ( f " Config disk: image ' { self . config_disk_name } ' missing " )
2020-08-13 10:40:31 +03:00
self . config_disk_name = " "
2020-04-06 13:56:00 +03:00
2021-04-13 12:07:58 +03:00
log . info ( f ' QEMU VM " { self . _name } " [ { self . _id } ] has been created ' )
2015-02-19 12:33:25 +02:00
2019-06-04 19:00:44 +03:00
@property
def guest_cid ( self ) :
"""
2019-06-05 12:25:35 +03:00
Returns the CID ( console ID ) which is an unique identifier between 3 and 65535
2019-06-04 19:00:44 +03:00
: returns : integer between 3 and 65535
"""
return self . _guest_cid
2019-06-05 12:25:35 +03:00
@guest_cid.setter
def guest_cid ( self , guest_cid ) :
"""
Set the CID ( console ID ) which is an unique identifier between 3 and 65535
: returns : integer between 3 and 65535
"""
self . _guest_cid = guest_cid
2015-02-19 12:33:25 +02:00
@property
def monitor ( self ) :
"""
Returns the TCP monitor port .
: returns : monitor port ( integer )
"""
return self . _monitor
@property
def qemu_path ( self ) :
"""
Returns the QEMU binary path for this QEMU VM .
: returns : QEMU path
"""
return self . _qemu_path
@qemu_path.setter
def qemu_path ( self , qemu_path ) :
"""
Sets the QEMU binary path this QEMU VM .
: param qemu_path : QEMU path
"""
2015-02-19 17:46:57 +02:00
if qemu_path and os . pathsep not in qemu_path :
2015-08-27 19:27:17 +03:00
new_qemu_path = shutil . which ( qemu_path , path = os . pathsep . join ( self . _manager . paths_list ( ) ) )
if new_qemu_path is None :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " QEMU binary path { qemu_path } is not found in the path " )
2015-08-27 19:27:17 +03:00
qemu_path = new_qemu_path
2015-02-19 17:46:57 +02:00
2015-06-10 16:49:24 +03:00
self . _check_qemu_path ( qemu_path )
self . _qemu_path = qemu_path
self . _platform = os . path . basename ( qemu_path )
if self . _platform == " qemu-kvm " :
self . _platform = " x86_64 "
else :
2015-08-26 14:47:12 +03:00
qemu_bin = os . path . basename ( qemu_path )
2021-04-13 12:16:50 +03:00
qemu_bin = re . sub ( r " (w)? \ .(exe|EXE)$ " , " " , qemu_bin )
2016-01-14 18:40:58 +02:00
# Old version of GNS3 provide a binary named qemu.exe
if qemu_bin == " qemu " :
self . _platform = " i386 "
else :
2021-04-13 12:16:50 +03:00
self . _platform = re . sub ( r " ^qemu-system-(.*)$ " , r " \ 1 " , qemu_bin , re . IGNORECASE )
2020-10-31 07:32:21 +02:00
try :
QemuPlatform ( self . _platform . split ( " . " ) [ 0 ] )
except ValueError :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Platform { self . _platform } is unknown " )
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the QEMU path to {qemu_path} ' . format (
name = self . _name , id = self . _id , qemu_path = qemu_path
)
)
2015-06-10 16:49:24 +03:00
def _check_qemu_path ( self , qemu_path ) :
2018-03-15 09:17:39 +02:00
2015-02-19 17:46:57 +02:00
if qemu_path is None :
2015-08-27 19:27:17 +03:00
raise QemuError ( " QEMU binary path is not set " )
2015-02-19 17:46:57 +02:00
if not os . path . exists ( qemu_path ) :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " QEMU binary ' { qemu_path } ' is not accessible " )
2015-02-19 17:46:57 +02:00
if not os . access ( qemu_path , os . X_OK ) :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " QEMU binary ' { qemu_path } ' is not executable " )
2015-02-19 17:46:57 +02:00
2015-06-10 16:49:24 +03:00
@property
def platform ( self ) :
"""
Return the current platform
"""
return self . _platform
@platform.setter
def platform ( self , platform ) :
2018-03-15 09:17:39 +02:00
2015-06-10 16:49:24 +03:00
self . _platform = platform
2022-01-19 13:58:36 +02:00
self . qemu_path = f " qemu-system- { platform } "
2015-02-19 12:33:25 +02:00
2016-10-24 22:39:35 +03:00
def _disk_setter ( self , variable , value ) :
"""
Use by disk image setter for checking and apply modifications
: param variable : Variable name in the class
: param value : New disk value
"""
2018-03-15 09:17:39 +02:00
2018-11-19 10:53:43 +02:00
value = self . manager . get_abs_image_path ( value , self . project . path )
2016-10-24 22:39:35 +03:00
if not self . linked_clone :
for node in self . manager . nodes :
if node != self and getattr ( node , variable ) == value :
2021-04-13 12:16:50 +03:00
raise QemuError (
2022-03-13 06:20:02 +02:00
f " Sorry a node without the linked base setting enabled can only be used once on your server. { value } is already used by { node . name } in project { node . project . name } "
2021-04-13 12:16:50 +03:00
)
2016-10-24 22:39:35 +03:00
setattr ( self , " _ " + variable , value )
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the QEMU {variable} path to {disk_image} ' . format (
name = self . _name , variable = variable , id = self . _id , disk_image = value
)
)
2016-10-24 22:39:35 +03:00
2015-02-19 12:33:25 +02:00
@property
def hda_disk_image ( self ) :
"""
Returns the hda disk image path for this QEMU VM .
: returns : QEMU hda disk image path
"""
return self . _hda_disk_image
@hda_disk_image.setter
def hda_disk_image ( self , hda_disk_image ) :
"""
Sets the hda disk image for this QEMU VM .
: param hda_disk_image : QEMU hda disk image path
"""
2018-03-15 09:17:39 +02:00
2016-10-24 22:39:35 +03:00
self . _disk_setter ( " hda_disk_image " , hda_disk_image )
2015-02-19 12:33:25 +02:00
@property
def hdb_disk_image ( self ) :
"""
Returns the hdb disk image path for this QEMU VM .
: returns : QEMU hdb disk image path
"""
return self . _hdb_disk_image
@hdb_disk_image.setter
def hdb_disk_image ( self , hdb_disk_image ) :
"""
Sets the hdb disk image for this QEMU VM .
: param hdb_disk_image : QEMU hdb disk image path
"""
2016-10-24 22:39:35 +03:00
self . _disk_setter ( " hdb_disk_image " , hdb_disk_image )
2015-02-19 12:33:25 +02:00
2015-03-10 19:50:30 +02:00
@property
def hdc_disk_image ( self ) :
"""
Returns the hdc disk image path for this QEMU VM .
: returns : QEMU hdc disk image path
"""
return self . _hdc_disk_image
@hdc_disk_image.setter
def hdc_disk_image ( self , hdc_disk_image ) :
"""
Sets the hdc disk image for this QEMU VM .
: param hdc_disk_image : QEMU hdc disk image path
"""
2016-10-24 22:39:35 +03:00
self . _disk_setter ( " hdc_disk_image " , hdc_disk_image )
2015-03-10 19:50:30 +02:00
@property
def hdd_disk_image ( self ) :
"""
Returns the hdd disk image path for this QEMU VM .
: returns : QEMU hdd disk image path
"""
return self . _hdd_disk_image
@hdd_disk_image.setter
def hdd_disk_image ( self , hdd_disk_image ) :
"""
Sets the hdd disk image for this QEMU VM .
: param hdd_disk_image : QEMU hdd disk image path
"""
2016-10-24 22:39:35 +03:00
self . _disk_setter ( " hdd_disk_image " , hdd_disk_image )
2015-03-10 19:50:30 +02:00
2015-08-03 08:02:02 +03:00
@property
def hda_disk_interface ( self ) :
"""
Returns the hda disk interface this QEMU VM .
: returns : QEMU hda disk interface
"""
return self . _hda_disk_interface
@hda_disk_interface.setter
def hda_disk_interface ( self , hda_disk_interface ) :
"""
Sets the hda disk interface for this QEMU VM .
: param hda_disk_interface : QEMU hda disk interface
"""
self . _hda_disk_interface = hda_disk_interface
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the QEMU hda disk interface to {interface} ' . format (
name = self . _name , id = self . _id , interface = self . _hda_disk_interface
)
)
2015-08-03 08:02:02 +03:00
@property
def hdb_disk_interface ( self ) :
"""
Returns the hdb disk interface this QEMU VM .
: returns : QEMU hdb disk interface
"""
2016-01-01 02:44:23 +02:00
return self . _hdb_disk_interface
2015-08-03 08:02:02 +03:00
@hdb_disk_interface.setter
def hdb_disk_interface ( self , hdb_disk_interface ) :
"""
Sets the hda disk interface for this QEMU VM .
: param hdb_disk_interface : QEMU hdb disk interface
"""
self . _hdb_disk_interface = hdb_disk_interface
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the QEMU hdb disk interface to {interface} ' . format (
name = self . _name , id = self . _id , interface = self . _hdb_disk_interface
)
)
2015-08-03 08:02:02 +03:00
@property
def hdc_disk_interface ( self ) :
"""
Returns the hdc disk interface this QEMU VM .
: returns : QEMU hdc disk interface
"""
return self . _hdc_disk_interface
@hdc_disk_interface.setter
def hdc_disk_interface ( self , hdc_disk_interface ) :
"""
Sets the hdc disk interface for this QEMU VM .
: param hdc_disk_interface : QEMU hdc disk interface
"""
self . _hdc_disk_interface = hdc_disk_interface
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the QEMU hdc disk interface to {interface} ' . format (
name = self . _name , id = self . _id , interface = self . _hdc_disk_interface
)
)
2015-08-03 08:02:02 +03:00
@property
def hdd_disk_interface ( self ) :
"""
Returns the hda disk interface this QEMU VM .
: returns : QEMU hda disk interface
"""
return self . _hdd_disk_interface
@hdd_disk_interface.setter
def hdd_disk_interface ( self , hdd_disk_interface ) :
"""
Sets the hdd disk interface for this QEMU VM .
: param hdd_disk_interface : QEMU hdd disk interface
"""
self . _hdd_disk_interface = hdd_disk_interface
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the QEMU hdd disk interface to {interface} ' . format (
name = self . _name , id = self . _id , interface = self . _hdd_disk_interface
)
)
2015-08-03 08:02:02 +03:00
@property
def cdrom_image ( self ) :
"""
Returns the cdrom image path for this QEMU VM .
: returns : QEMU cdrom image path
"""
return self . _cdrom_image
@cdrom_image.setter
def cdrom_image ( self , cdrom_image ) :
"""
Sets the cdrom image for this QEMU VM .
: param cdrom_image : QEMU cdrom image path
"""
2018-11-19 10:53:43 +02:00
2019-06-12 15:23:03 +03:00
if cdrom_image :
self . _cdrom_image = self . manager . get_abs_image_path ( cdrom_image , self . project . path )
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the QEMU cdrom image path to {cdrom_image} ' . format (
name = self . _name , id = self . _id , cdrom_image = self . _cdrom_image
)
)
2019-06-12 15:23:03 +03:00
else :
self . _cdrom_image = " "
2020-10-06 09:38:51 +03:00
async def update_property ( self , name , value ) :
2020-09-12 16:37:39 +03:00
"""
Update Qemu VM properties .
"""
setattr ( self , name , value )
if name == " cdrom_image " :
# let the guest know about the new cdrom image
await self . _update_cdrom_image ( )
self . _command_line_changed = True
async def _update_cdrom_image ( self ) :
2019-06-12 15:23:03 +03:00
"""
Update the cdrom image path for the Qemu guest OS
"""
if self . is_running ( ) :
if self . _cdrom_image :
self . _cdrom_option ( ) # this will check the cdrom image is accessible
2019-06-15 16:20:21 +03:00
await self . _control_vm ( " eject -f ide1-cd0 " )
2021-04-13 12:07:58 +03:00
await self . _control_vm ( f " change ide1-cd0 { self . _cdrom_image } " )
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has changed the cdrom image path to {cdrom_image} ' . format (
name = self . _name , id = self . _id , cdrom_image = self . _cdrom_image
)
)
2019-06-12 15:23:03 +03:00
else :
2019-06-15 16:20:21 +03:00
await self . _control_vm ( " eject -f ide1-cd0 " )
2021-04-13 12:07:58 +03:00
log . info ( f ' QEMU VM " { self . _name } " [ { self . _id } ] has ejected the cdrom image ' )
2015-08-03 08:02:02 +03:00
2016-12-08 17:18:30 +02:00
@property
def bios_image ( self ) :
"""
Returns the bios image path for this QEMU VM .
: returns : QEMU bios image path
"""
return self . _bios_image
@bios_image.setter
def bios_image ( self , bios_image ) :
"""
Sets the bios image for this QEMU VM .
: param bios_image : QEMU bios image path
"""
2018-11-19 10:53:43 +02:00
self . _bios_image = self . manager . get_abs_image_path ( bios_image , self . project . path )
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the QEMU bios image path to {bios_image} ' . format (
name = self . _name , id = self . _id , bios_image = self . _bios_image
)
)
2016-12-08 17:18:30 +02:00
2015-08-03 08:02:02 +03:00
@property
def boot_priority ( self ) :
"""
Returns the boot priority for this QEMU VM .
: returns : QEMU boot priority
"""
return self . _boot_priority
@boot_priority.setter
def boot_priority ( self , boot_priority ) :
"""
Sets the boot priority for this QEMU VM .
: param boot_priority : QEMU boot priority
"""
self . _boot_priority = boot_priority
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the boot priority to {boot_priority} ' . format (
name = self . _name , id = self . _id , boot_priority = self . _boot_priority
)
)
2015-08-03 08:02:02 +03:00
2017-07-12 15:50:33 +03:00
@property
def ethernet_adapters ( self ) :
"""
Return the list of ethernet adapters of the node
"""
return self . _ethernet_adapters
2015-02-19 12:33:25 +02:00
@property
def adapters ( self ) :
"""
2015-04-08 20:17:34 +03:00
Returns the number of Ethernet adapters for this QEMU VM .
2015-02-19 12:33:25 +02:00
: returns : number of adapters
"""
return len ( self . _ethernet_adapters )
@adapters.setter
def adapters ( self , adapters ) :
"""
2015-04-08 20:17:34 +03:00
Sets the number of Ethernet adapters for this QEMU VM .
2015-02-19 12:33:25 +02:00
: param adapters : number of adapters
"""
self . _ethernet_adapters . clear ( )
2015-04-08 20:17:34 +03:00
for adapter_number in range ( 0 , adapters ) :
2015-02-19 12:33:25 +02:00
self . _ethernet_adapters . append ( EthernetAdapter ( ) )
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ]: number of Ethernet adapters changed to {adapters} ' . format (
name = self . _name , id = self . _id , adapters = adapters
)
)
2015-02-19 12:33:25 +02:00
@property
def adapter_type ( self ) :
"""
2015-04-08 20:17:34 +03:00
Returns the adapter type for this QEMU VM .
2015-02-19 12:33:25 +02:00
: returns : adapter type ( string )
"""
return self . _adapter_type
@adapter_type.setter
def adapter_type ( self , adapter_type ) :
"""
2015-04-08 20:17:34 +03:00
Sets the adapter type for this QEMU VM .
2015-02-19 12:33:25 +02:00
: param adapter_type : adapter type ( string )
"""
self . _adapter_type = adapter_type
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ]: adapter type changed to {adapter_type} ' . format (
name = self . _name , id = self . _id , adapter_type = adapter_type
)
)
2015-02-19 12:33:25 +02:00
2015-06-03 23:52:49 +03:00
@property
def mac_address ( self ) :
"""
Returns the MAC address for this QEMU VM .
: returns : adapter type ( string )
"""
return self . _mac_address
@mac_address.setter
def mac_address ( self , mac_address ) :
"""
Sets the MAC address for this QEMU VM .
: param mac_address : MAC address
"""
2015-06-08 07:18:41 +03:00
if not mac_address :
2016-05-30 20:52:08 +03:00
# use the node UUID to generate a random MAC address
2021-08-10 09:08:49 +03:00
self . _mac_address = f " 0c: { self . id [ 2 : 4 ] } : { self . id [ 4 : 6 ] } : { self . id [ 6 : 8 ] } :00:00 "
2015-06-08 07:18:41 +03:00
else :
2016-05-30 20:52:08 +03:00
self . _mac_address = mac_address
2015-06-03 23:52:49 +03:00
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ]: MAC address changed to {mac_addr} ' . format (
name = self . _name , id = self . _id , mac_addr = self . _mac_address
)
)
2015-06-03 23:52:49 +03:00
2020-06-02 12:15:22 +03:00
@property
def replicate_network_connection_state ( self ) :
"""
Returns whether the network connection state for links is replicated in QEMU .
: returns : boolean
"""
return self . _replicate_network_connection_state
@replicate_network_connection_state.setter
def replicate_network_connection_state ( self , replicate_network_connection_state ) :
"""
Sets whether the network connection state for links is replicated in QEMU
: param replicate_network_connection_state : boolean
"""
if replicate_network_connection_state :
2021-04-13 12:07:58 +03:00
log . info ( f ' QEMU VM " { self . _name } " [ { self . _id } ] has enabled network connection state replication ' )
2020-06-02 12:15:22 +03:00
else :
2021-04-13 12:07:58 +03:00
log . info ( f ' QEMU VM " { self . _name } " [ { self . _id } ] has disabled network connection state replication ' )
2020-06-02 12:15:22 +03:00
self . _replicate_network_connection_state = replicate_network_connection_state
2020-08-14 11:27:24 +03:00
@property
def create_config_disk ( self ) :
"""
Returns whether a config disk is automatically created on HDD disk interface ( secondary slave )
: returns : boolean
"""
return self . _create_config_disk
@create_config_disk.setter
def create_config_disk ( self , create_config_disk ) :
"""
Sets whether a config disk is automatically created on HDD disk interface ( secondary slave )
: param replicate_network_connection_state : boolean
"""
if create_config_disk :
2021-04-13 12:07:58 +03:00
log . info ( f ' QEMU VM " { self . _name } " [ { self . _id } ] has enabled the config disk creation feature ' )
2020-08-14 11:27:24 +03:00
else :
2021-04-13 12:07:58 +03:00
log . info ( f ' QEMU VM " { self . _name } " [ { self . _id } ] has disabled the config disk creation feature ' )
2020-08-14 11:27:24 +03:00
self . _create_config_disk = create_config_disk
2015-06-03 07:33:38 +03:00
@property
2018-03-30 17:18:44 +03:00
def on_close ( self ) :
2015-06-03 07:33:38 +03:00
"""
2018-03-30 17:18:44 +03:00
Returns the action to execute when the VM is stopped / closed
2015-06-03 07:33:38 +03:00
2018-03-30 17:18:44 +03:00
: returns : string
2018-03-30 15:28:22 +03:00
"""
2018-03-30 17:18:44 +03:00
return self . _on_close
2018-03-30 15:28:22 +03:00
2018-03-30 17:18:44 +03:00
@on_close.setter
def on_close ( self , on_close ) :
2018-03-30 15:28:22 +03:00
"""
2018-03-30 17:18:44 +03:00
Sets the action to execute when the VM is stopped / closed
2018-03-30 15:28:22 +03:00
2018-03-30 17:18:44 +03:00
: param on_close : string
2018-03-30 15:28:22 +03:00
"""
2021-04-13 12:07:58 +03:00
log . info ( f ' QEMU VM " { self . _name } " [ { self . _id } ] set the close action to " { on_close } " ' )
2018-03-30 17:18:44 +03:00
self . _on_close = on_close
2018-03-30 15:28:22 +03:00
2015-02-19 12:33:25 +02:00
@property
def cpu_throttling ( self ) :
"""
Returns the percentage of CPU allowed .
: returns : integer
"""
return self . _cpu_throttling
@cpu_throttling.setter
def cpu_throttling ( self , cpu_throttling ) :
"""
Sets the percentage of CPU allowed .
: param cpu_throttling : integer
"""
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the percentage of CPU allowed to {cpu} ' . format (
name = self . _name , id = self . _id , cpu = cpu_throttling
)
)
2015-02-19 12:33:25 +02:00
self . _cpu_throttling = cpu_throttling
self . _stop_cpulimit ( )
if cpu_throttling :
self . _set_cpu_throttling ( )
@property
def process_priority ( self ) :
"""
Returns the process priority .
: returns : string
"""
return self . _process_priority
@process_priority.setter
def process_priority ( self , process_priority ) :
"""
Sets the process priority .
: param process_priority : string
"""
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the process priority to {priority} ' . format (
name = self . _name , id = self . _id , priority = process_priority
)
)
2015-02-19 12:33:25 +02:00
self . _process_priority = process_priority
@property
def ram ( self ) :
"""
Returns the RAM amount for this QEMU VM .
: returns : RAM amount in MB
"""
return self . _ram
@ram.setter
def ram ( self , ram ) :
"""
Sets the amount of RAM for this QEMU VM .
: param ram : RAM amount in MB
"""
2021-04-13 12:07:58 +03:00
log . info ( f ' QEMU VM " { self . _name } " [ { self . _id } ] has set the RAM to { ram } ' )
2015-02-19 12:33:25 +02:00
self . _ram = ram
2015-08-06 02:17:55 +03:00
@property
def cpus ( self ) :
"""
Returns the number of vCPUs this QEMU VM .
: returns : number of vCPUs .
"""
return self . _cpus
@cpus.setter
def cpus ( self , cpus ) :
"""
Sets the number of vCPUs this QEMU VM .
: param cpus : number of vCPUs .
"""
2021-04-13 12:07:58 +03:00
log . info ( f ' QEMU VM " { self . _name } " [ { self . _id } ] has set the number of vCPUs to { cpus } ' )
2015-08-06 02:17:55 +03:00
self . _cpus = cpus
2020-07-24 08:45:41 +03:00
@property
def maxcpus ( self ) :
"""
Returns the maximum number of hotpluggable vCPUs for this QEMU VM .
: returns : maximum number of hotpluggable vCPUs .
"""
return self . _maxcpus
@maxcpus.setter
def maxcpus ( self , maxcpus ) :
"""
Sets the maximum number of hotpluggable vCPUs for this QEMU VM .
: param maxcpus : maximum number of hotpluggable vCPUs
"""
2021-04-13 12:07:58 +03:00
log . info ( f ' QEMU VM " { self . _name } " [ { self . _id } ] has set maximum number of hotpluggable vCPUs to { maxcpus } ' )
2020-07-24 08:45:41 +03:00
self . _maxcpus = maxcpus
2015-02-19 12:33:25 +02:00
@property
def options ( self ) :
"""
Returns the options for this QEMU VM .
: returns : QEMU options
"""
return self . _options
@options.setter
def options ( self , options ) :
"""
Sets the options for this QEMU VM .
: param options : QEMU options
"""
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the QEMU options to {options} ' . format (
name = self . _name , id = self . _id , options = options
)
)
2015-06-12 10:40:38 +03:00
2021-07-27 10:04:51 +03:00
# "-no-kvm" and "-no-hax' are deprecated since Qemu v5.2
if " -no-kvm " in options :
options = options . replace ( " -no-kvm " , " -machine accel=tcg " )
if " -no-hax " in options :
options = options . replace ( " -no-hax " , " -machine accel=tcg " )
if " -enable-kvm " in options :
if not sys . platform . startswith ( " linux " ) :
# KVM can only be enabled on Linux
2015-07-28 00:31:42 +03:00
options = options . replace ( " -enable-kvm " , " " )
2021-07-27 10:04:51 +03:00
else :
options = options . replace ( " -enable-kvm " , " -machine accel=kvm " )
if " -enable-hax " in options :
2022-01-19 13:58:36 +02:00
if not sys . platform . startswith ( " darwin " ) :
# HAXM is only available on macOS
2018-03-21 11:41:25 +02:00
options = options . replace ( " -enable-hax " , " " )
2021-07-27 10:04:51 +03:00
else :
options = options . replace ( " -enable-hax " , " -machine accel=hax " )
2015-06-12 10:40:38 +03:00
self . _options = options . strip ( )
2015-02-19 12:33:25 +02:00
@property
def initrd ( self ) :
"""
Returns the initrd path for this QEMU VM .
: returns : QEMU initrd path
"""
return self . _initrd
@initrd.setter
def initrd ( self , initrd ) :
"""
Sets the initrd path for this QEMU VM .
: param initrd : QEMU initrd path
"""
2018-11-19 10:53:43 +02:00
initrd = self . manager . get_abs_image_path ( initrd , self . project . path )
2015-03-10 05:57:21 +02:00
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the QEMU initrd path to {initrd} ' . format (
name = self . _name , id = self . _id , initrd = initrd
)
)
2019-04-15 13:05:20 +03:00
if " asa " in initrd and self . _initrd != initrd :
2021-04-13 12:16:50 +03:00
self . project . emit (
" log.warning " ,
{
" message " : " Warning ASA 8 is not supported by GNS3 and Cisco, please use ASAv instead. Depending of your hardware and OS this could not work or you could be limited to one instance. If ASA 8 is not booting their is no GNS3 solution, you must to upgrade to ASAv. "
} ,
)
2015-02-19 12:33:25 +02:00
self . _initrd = initrd
@property
def kernel_image ( self ) :
"""
Returns the kernel image path for this QEMU VM .
: returns : QEMU kernel image path
"""
return self . _kernel_image
@kernel_image.setter
def kernel_image ( self , kernel_image ) :
"""
Sets the kernel image path for this QEMU VM .
: param kernel_image : QEMU kernel image path
"""
2018-11-19 10:53:43 +02:00
kernel_image = self . manager . get_abs_image_path ( kernel_image , self . project . path )
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the QEMU kernel image path to {kernel_image} ' . format (
name = self . _name , id = self . _id , kernel_image = kernel_image
)
)
2015-02-19 12:33:25 +02:00
self . _kernel_image = kernel_image
@property
def kernel_command_line ( self ) :
"""
Returns the kernel command line for this QEMU VM .
: returns : QEMU kernel command line
"""
return self . _kernel_command_line
@kernel_command_line.setter
def kernel_command_line ( self , kernel_command_line ) :
"""
Sets the kernel command line for this QEMU VM .
: param kernel_command_line : QEMU kernel command line
"""
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] has set the QEMU kernel command line to {kernel_command_line} ' . format (
name = self . _name , id = self . _id , kernel_command_line = kernel_command_line
)
)
2015-02-19 12:33:25 +02:00
self . _kernel_command_line = kernel_command_line
2018-10-15 13:05:49 +03:00
async def _set_process_priority ( self ) :
2015-02-19 12:33:25 +02:00
"""
Changes the process priority
"""
2015-12-22 14:15:28 +02:00
if self . _process_priority == " normal " :
return
2022-01-19 13:58:36 +02:00
if self . _process_priority == " realtime " :
priority = - 20
elif self . _process_priority == " very high " :
priority = - 15
elif self . _process_priority == " high " :
priority = - 5
elif self . _process_priority == " low " :
priority = 5
elif self . _process_priority == " very low " :
priority = 19
2015-02-19 12:33:25 +02:00
else :
2022-01-19 13:58:36 +02:00
priority = 0
try :
process = await asyncio . create_subprocess_exec (
" renice " , " -n " , str ( priority ) , " -p " , str ( self . _process . pid )
)
await process . wait ( )
except ( OSError , subprocess . SubprocessError ) as e :
log . error ( f ' Could not change process priority for QEMU VM " { self . _name } " : { e } ' )
2015-02-19 12:33:25 +02:00
def _stop_cpulimit ( self ) :
"""
Stops the cpulimit process .
"""
2015-02-26 11:18:01 +02:00
if self . _cpulimit_process and self . _cpulimit_process . returncode is None :
2015-02-19 12:33:25 +02:00
self . _cpulimit_process . kill ( )
try :
self . _process . wait ( 3 )
except subprocess . TimeoutExpired :
2021-04-13 12:07:58 +03:00
log . error ( f " Could not kill cpulimit process { self . _cpulimit_process . pid } " )
2015-02-19 12:33:25 +02:00
def _set_cpu_throttling ( self ) :
"""
Limits the CPU usage for current QEMU process .
"""
if not self . is_running ( ) :
return
try :
2021-04-13 12:16:50 +03:00
subprocess . Popen (
2022-01-19 13:58:36 +02:00
[ " cpulimit " , " --lazy " , f " --pid= { self . _process . pid } " , f " --limit= { self . _cpu_throttling } " ] ,
2021-04-13 12:16:50 +03:00
cwd = self . working_dir ,
)
2021-04-13 12:07:58 +03:00
log . info ( f " CPU throttled to { self . _cpu_throttling } % " )
2015-02-19 12:33:25 +02:00
except FileNotFoundError :
raise QemuError ( " cpulimit could not be found, please install it or deactivate CPU throttling " )
except ( OSError , subprocess . SubprocessError ) as e :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Could not throttle CPU: { e } " )
2015-02-19 12:33:25 +02:00
2018-10-15 13:05:49 +03:00
async def create ( self ) :
2018-01-29 15:20:48 +02:00
"""
Creates QEMU VM and sets proper MD5 hashes
"""
# In case user upload image manually we don't have md5 sums.
# We need generate hashes at this point, otherwise they will be generated
2021-04-17 17:04:28 +03:00
# at asdict but not on separate thread.
2018-10-15 13:05:49 +03:00
await cancellable_wait_run_in_executor ( md5sum , self . _hda_disk_image )
await cancellable_wait_run_in_executor ( md5sum , self . _hdb_disk_image )
await cancellable_wait_run_in_executor ( md5sum , self . _hdc_disk_image )
await cancellable_wait_run_in_executor ( md5sum , self . _hdd_disk_image )
2018-01-29 15:20:48 +02:00
2021-04-13 12:07:58 +03:00
super ( ) . create ( )
2018-01-29 15:20:48 +02:00
2018-10-15 13:05:49 +03:00
async def start ( self ) :
2015-02-19 12:33:25 +02:00
"""
Starts this QEMU VM .
"""
2018-10-15 13:05:49 +03:00
async with self . _execute_lock :
2015-08-27 17:06:11 +03:00
if self . is_running ( ) :
# resume the VM if it is paused
2018-10-15 13:05:49 +03:00
await self . resume ( )
2015-08-27 17:06:11 +03:00
return
2015-02-19 12:33:25 +02:00
2021-04-12 10:32:23 +03:00
if self . _manager . config . settings . Qemu . enable_monitor :
2015-03-24 06:52:02 +02:00
try :
2021-04-13 12:16:50 +03:00
info = socket . getaddrinfo (
self . _monitor_host , 0 , socket . AF_UNSPEC , socket . SOCK_STREAM , 0 , socket . AI_PASSIVE
)
2016-11-28 14:18:04 +02:00
if not info :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " getaddrinfo returns an empty list on { self . _monitor_host } " )
2016-11-28 14:18:04 +02:00
for res in info :
af , socktype , proto , _ , sa = res
# let the OS find an unused port for the Qemu monitor
with socket . socket ( af , socktype , proto ) as sock :
2018-04-16 11:36:36 +03:00
sock . setsockopt ( socket . SOL_SOCKET , socket . SO_REUSEADDR , 1 )
2016-11-28 14:18:04 +02:00
sock . bind ( sa )
self . _monitor = sock . getsockname ( ) [ 1 ]
except OSError as e :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Could not find free port for the Qemu monitor: { e } " )
2016-11-28 14:18:04 +02:00
# check if there is enough RAM to run
self . check_available_ram ( self . ram )
2018-10-15 13:05:49 +03:00
command = await self . _build_command ( )
2022-01-19 13:58:36 +02:00
command_string = " " . join ( shlex . quote ( s ) for s in command )
2016-11-28 14:18:04 +02:00
try :
2021-04-13 12:07:58 +03:00
log . info ( f " Starting QEMU with: { command_string } " )
2016-11-28 14:18:04 +02:00
self . _stdout_file = os . path . join ( self . working_dir , " qemu.log " )
2021-04-13 12:07:58 +03:00
log . info ( f " logging to { self . _stdout_file } " )
2016-11-28 14:18:04 +02:00
with open ( self . _stdout_file , " w " , encoding = " utf-8 " ) as fd :
2021-04-13 12:07:58 +03:00
fd . write ( f " Start QEMU with { command_string } \n \n Execution log: \n " )
2021-04-13 12:16:50 +03:00
self . command_line = " " . join ( command )
self . _process = await asyncio . create_subprocess_exec (
* command , stdout = fd , stderr = subprocess . STDOUT , cwd = self . working_dir
)
2021-04-13 12:07:58 +03:00
log . info ( f ' QEMU VM " { self . _name } " started PID= { self . _process . pid } ' )
2020-09-12 16:37:39 +03:00
self . _command_line_changed = False
2016-11-28 14:18:04 +02:00
self . status = " started "
monitor_process ( self . _process , self . _termination_callback )
except ( OSError , subprocess . SubprocessError , UnicodeEncodeError ) as e :
stdout = self . read_stdout ( )
2021-04-13 12:07:58 +03:00
log . error ( f " Could not start QEMU { self . qemu_path } : { e } \n { stdout } " )
raise QemuError ( f " Could not start QEMU { self . qemu_path } : { e } \n { stdout } " )
2016-11-28 14:18:04 +02:00
2018-10-15 13:05:49 +03:00
await self . _set_process_priority ( )
2016-11-28 14:18:04 +02:00
if self . _cpu_throttling :
self . _set_cpu_throttling ( )
2018-03-21 11:41:25 +02:00
if " -enable-kvm " in command_string or " -enable-hax " in command_string :
2016-11-28 14:18:04 +02:00
self . _hw_virtualization = True
2018-03-19 09:22:10 +02:00
2018-10-15 13:05:49 +03:00
await self . _start_ubridge ( )
2018-03-22 19:07:32 +02:00
set_link_commands = [ ]
2018-03-19 09:22:10 +02:00
for adapter_number , adapter in enumerate ( self . _ethernet_adapters ) :
nio = adapter . get_nio ( 0 )
if nio :
2021-04-13 12:16:50 +03:00
await self . add_ubridge_udp_connection (
f " QEMU- { self . _id } - { adapter_number } " , self . _local_udp_tunnels [ adapter_number ] [ 1 ] , nio
)
2020-06-02 12:15:22 +03:00
if nio . suspend and self . _replicate_network_connection_state :
2021-04-13 12:07:58 +03:00
set_link_commands . append ( f " set_link gns3- { adapter_number } off " )
2020-06-02 12:15:22 +03:00
elif self . _replicate_network_connection_state :
2021-04-13 12:07:58 +03:00
set_link_commands . append ( f " set_link gns3- { adapter_number } off " )
2019-05-19 13:59:00 +03:00
2020-06-02 12:15:22 +03:00
if " -loadvm " not in command_string and self . _replicate_network_connection_state :
2019-05-19 13:59:00 +03:00
# only set the link statuses if not restoring a previous VM state
await self . _control_vm_commands ( set_link_commands )
2018-03-19 09:22:10 +02:00
2016-11-28 14:18:04 +02:00
try :
2019-05-25 11:31:35 +03:00
if self . is_running ( ) :
await self . start_wrap_console ( )
2016-11-28 14:18:04 +02:00
except OSError as e :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Could not start Telnet QEMU console { e } \n " )
2015-07-22 07:58:28 +03:00
2018-10-15 13:05:49 +03:00
async def _termination_callback ( self , returncode ) :
2015-03-04 17:01:56 +02:00
"""
2015-05-13 22:53:42 +03:00
Called when the process has stopped .
2015-03-04 17:01:56 +02:00
: param returncode : Process returncode
"""
2015-05-13 22:53:42 +03:00
2015-03-04 17:01:56 +02:00
if self . started :
2015-05-13 22:53:42 +03:00
log . info ( " QEMU process has stopped, return code: %d " , returncode )
2018-10-15 13:05:49 +03:00
await self . stop ( )
2022-01-19 13:58:36 +02:00
if returncode != 0 :
2021-04-13 12:16:50 +03:00
self . project . emit (
" log.error " ,
{ " message " : f " QEMU process has stopped, return code: { returncode } \n { self . read_stdout ( ) } " } ,
)
2015-03-04 17:01:56 +02:00
2018-10-15 13:05:49 +03:00
async def stop ( self ) :
2015-02-19 12:33:25 +02:00
"""
Stops this QEMU VM .
"""
2018-10-15 13:05:49 +03:00
await self . _stop_ubridge ( )
async with self . _execute_lock :
2015-08-27 17:06:11 +03:00
# stop the QEMU process
self . _hw_virtualization = False
if self . is_running ( ) :
2021-04-13 12:07:58 +03:00
log . info ( f ' Stopping QEMU VM " { self . _name } " PID= { self . _process . pid } ' )
2015-08-27 17:06:11 +03:00
try :
2018-03-30 15:28:22 +03:00
2018-03-30 17:18:44 +03:00
if self . on_close == " save_vm_state " :
2018-10-15 13:05:49 +03:00
await self . _control_vm ( " stop " )
await self . _control_vm ( " savevm GNS3_SAVED_STATE " )
2018-03-30 15:28:22 +03:00
wait_for_savevm = 120
while wait_for_savevm :
2018-10-15 13:05:49 +03:00
await asyncio . sleep ( 1 )
status = await self . _saved_state_option ( )
2018-03-30 15:28:22 +03:00
wait_for_savevm - = 1
if status != [ ] :
break
2018-03-30 17:18:44 +03:00
if self . on_close == " shutdown_signal " :
2018-10-15 13:05:49 +03:00
await self . _control_vm ( " system_powerdown " )
2019-03-01 12:23:49 +02:00
await gns3server . utils . asyncio . wait_for_process_termination ( self . _process , timeout = 120 )
2015-08-27 17:06:11 +03:00
else :
self . _process . terminate ( )
2018-10-15 13:05:49 +03:00
await gns3server . utils . asyncio . wait_for_process_termination ( self . _process , timeout = 3 )
2015-12-07 11:48:57 +02:00
except ProcessLookupError :
pass
2015-08-27 17:06:11 +03:00
except asyncio . TimeoutError :
2016-12-12 10:16:29 +02:00
if self . _process :
try :
self . _process . kill ( )
except ProcessLookupError :
pass
if self . _process . returncode is None :
2021-04-13 12:07:58 +03:00
log . warning ( f ' QEMU VM " { self . _name } " PID= { self . _process . pid } is still running ' )
2015-08-27 17:06:11 +03:00
self . _process = None
self . _stop_cpulimit ( )
2018-03-30 17:18:44 +03:00
if self . on_close != " save_vm_state " :
2018-10-15 13:05:49 +03:00
await self . _clear_save_vm_stated ( )
2020-04-06 13:56:00 +03:00
await self . _export_config ( )
2018-10-15 13:05:49 +03:00
await super ( ) . stop ( )
2015-02-19 12:33:25 +02:00
2018-10-15 13:05:49 +03:00
async def _open_qemu_monitor_connection_vm ( self , timeout = 10 ) :
2018-03-22 19:07:32 +02:00
"""
Opens a connection to the QEMU monitor .
: param timeout : timeout to connect to the monitor TCP server
: returns : The reader returned is a StreamReader instance ; the writer is a StreamWriter instance
"""
begin = time . time ( )
connection_success = False
last_exception = None
reader = writer = None
while time . time ( ) - begin < timeout :
2018-10-15 13:05:49 +03:00
await asyncio . sleep ( 0.01 )
2018-03-22 19:07:32 +02:00
try :
2021-04-13 12:07:58 +03:00
log . debug ( f " Connecting to Qemu monitor on { self . _monitor_host } : { self . _monitor } " )
2018-10-15 13:05:49 +03:00
reader , writer = await asyncio . open_connection ( self . _monitor_host , self . _monitor )
2018-03-22 19:07:32 +02:00
except ( asyncio . TimeoutError , OSError ) as e :
last_exception = e
continue
connection_success = True
break
if not connection_success :
2021-04-13 12:16:50 +03:00
log . warning (
" Could not connect to QEMU monitor on {} : {} : {} " . format (
self . _monitor_host , self . _monitor , last_exception
)
)
2018-03-22 19:07:32 +02:00
else :
2021-04-13 12:16:50 +03:00
log . info (
f " Connected to QEMU monitor on { self . _monitor_host } : { self . _monitor } after { time . time ( ) - begin : .4f } seconds "
)
2018-03-22 19:07:32 +02:00
return reader , writer
2018-10-15 13:05:49 +03:00
async def _control_vm ( self , command , expected = None ) :
2015-02-19 12:33:25 +02:00
"""
Executes a command with QEMU monitor when this VM is running .
: param command : QEMU monitor command ( e . g . info status , stop etc . )
2015-04-08 20:17:34 +03:00
: param expected : An array of expected strings
2015-02-19 12:33:25 +02:00
2015-04-08 20:17:34 +03:00
: returns : result of the command ( matched object or None )
2015-02-19 12:33:25 +02:00
"""
result = None
if self . is_running ( ) and self . _monitor :
2021-04-13 12:07:58 +03:00
log . info ( f " Execute QEMU monitor command: { command } " )
2018-10-15 13:05:49 +03:00
reader , writer = await self . _open_qemu_monitor_connection_vm ( )
2018-03-22 19:07:32 +02:00
if reader is None and writer is None :
2015-02-19 12:33:25 +02:00
return result
2018-03-22 19:07:32 +02:00
2015-02-19 12:33:25 +02:00
try :
2021-04-13 12:16:50 +03:00
cmd_byte = command . encode ( " ascii " )
2019-05-18 15:31:41 +03:00
writer . write ( cmd_byte + b " \n " )
if not expected :
while True :
line = await asyncio . wait_for ( reader . readline ( ) , timeout = 3 ) # echo of the command
if not line or cmd_byte in line :
break
except asyncio . TimeoutError :
2021-04-13 12:07:58 +03:00
log . warning ( f " Missing echo of command ' { command } ' " )
2015-02-19 12:33:25 +02:00
except OSError as e :
2021-04-13 12:07:58 +03:00
log . warning ( f " Could not write to QEMU monitor: { e } " )
2015-02-21 01:15:56 +02:00
writer . close ( )
2015-02-19 12:33:25 +02:00
return result
if expected :
try :
2015-02-21 01:15:56 +02:00
while result is None :
2019-05-18 15:31:41 +03:00
line = await asyncio . wait_for ( reader . readline ( ) , timeout = 3 )
2015-02-21 01:15:56 +02:00
if not line :
break
for expect in expected :
if expect in line :
2015-04-27 06:19:39 +03:00
result = line . decode ( " utf-8 " ) . strip ( )
2015-02-21 01:15:56 +02:00
break
2019-05-18 15:31:41 +03:00
except asyncio . TimeoutError :
2021-04-13 12:07:58 +03:00
log . warning ( f " Timeout while waiting for result of command ' { command } ' " )
2018-09-06 10:49:12 +03:00
except ( ConnectionError , EOFError ) as e :
2021-04-13 12:07:58 +03:00
log . warning ( f " Could not read from QEMU monitor: { e } " )
2015-02-21 01:15:56 +02:00
writer . close ( )
2015-02-19 12:33:25 +02:00
return result
2018-10-15 13:05:49 +03:00
async def _control_vm_commands ( self , commands ) :
2018-03-22 19:07:32 +02:00
"""
Executes commands with QEMU monitor when this VM is running .
: param commands : a list of QEMU monitor commands ( e . g . info status , stop etc . )
"""
if self . is_running ( ) and self . _monitor :
2018-10-15 13:05:49 +03:00
reader , writer = await self . _open_qemu_monitor_connection_vm ( )
2018-03-22 19:07:32 +02:00
if reader is None and writer is None :
return
for command in commands :
2021-04-13 12:07:58 +03:00
log . info ( f " Execute QEMU monitor command: { command } " )
2018-03-22 19:07:32 +02:00
try :
2021-04-13 12:16:50 +03:00
cmd_byte = command . encode ( " ascii " )
2019-10-15 18:42:42 +03:00
writer . write ( cmd_byte + b " \n " )
while True :
line = await asyncio . wait_for ( reader . readline ( ) , timeout = 3 ) # echo of the command
if not line or cmd_byte in line :
break
2019-05-18 15:31:41 +03:00
except asyncio . TimeoutError :
2021-04-13 12:07:58 +03:00
log . warning ( f " Missing echo of command ' { command } ' " )
2018-03-22 19:07:32 +02:00
except OSError as e :
2021-04-13 12:07:58 +03:00
log . warning ( f " Could not write to QEMU monitor: { e } " )
2018-03-22 19:07:32 +02:00
writer . close ( )
2018-10-15 13:05:49 +03:00
async def close ( self ) :
2015-04-08 20:17:34 +03:00
"""
Closes this QEMU VM .
"""
2015-02-19 17:46:57 +02:00
2018-10-15 13:05:49 +03:00
if not ( await super ( ) . close ( ) ) :
2016-02-29 11:38:30 +02:00
return False
2021-04-13 12:16:50 +03:00
# FIXME: Don't wait for ACPI shutdown when closing the project, should we?
2019-05-19 13:59:00 +03:00
if self . on_close == " shutdown_signal " :
self . on_close = " power_off "
2018-10-15 13:05:49 +03:00
await self . stop ( )
2015-10-08 05:48:36 +03:00
for adapter in self . _ethernet_adapters :
if adapter is not None :
for nio in adapter . ports . values ( ) :
if nio and isinstance ( nio , NIOUDP ) :
self . manager . port_manager . release_udp_port ( nio . lport , self . _project )
2016-06-24 01:56:06 +03:00
for udp_tunnel in self . _local_udp_tunnels . values ( ) :
self . manager . port_manager . release_udp_port ( udp_tunnel [ 0 ] . lport , self . _project )
self . manager . port_manager . release_udp_port ( udp_tunnel [ 1 ] . lport , self . _project )
self . _local_udp_tunnels = { }
2018-10-15 13:05:49 +03:00
async def _get_vm_status ( self ) :
2015-02-19 12:33:25 +02:00
"""
2015-04-08 20:17:34 +03:00
Returns this VM suspend status .
2015-02-19 12:33:25 +02:00
2015-03-17 12:02:14 +02:00
Status are extracted from :
https : / / github . com / qemu / qemu / blob / master / qapi - schema . json #L152
2015-02-19 12:33:25 +02:00
: returns : status ( string )
"""
2021-04-13 12:16:50 +03:00
result = await self . _control_vm (
" info status " ,
[
b " debug " ,
b " inmigrate " ,
b " internal-error " ,
b " io-error " ,
b " paused " ,
b " postmigrate " ,
b " prelaunch " ,
b " finish-migrate " ,
b " restore-vm " ,
b " running " ,
b " save-vm " ,
b " shutdown " ,
b " suspended " ,
b " watchdog " ,
b " guest-panicked " ,
] ,
)
2015-03-23 04:40:19 +02:00
if result is None :
return result
2021-04-13 12:16:50 +03:00
status = result . rsplit ( " " , 1 ) [ 1 ]
2017-01-17 17:03:12 +02:00
if status == " running " or status == " prelaunch " :
self . status = " started "
elif status == " suspended " :
self . status = " suspended "
elif status == " shutdown " :
self . status = " stopped "
return status
2015-02-19 12:33:25 +02:00
2018-10-15 13:05:49 +03:00
async def suspend ( self ) :
2015-02-19 12:33:25 +02:00
"""
Suspends this QEMU VM .
"""
2015-03-24 06:52:02 +02:00
if self . is_running ( ) :
2018-10-15 13:05:49 +03:00
vm_status = await self . _get_vm_status ( )
2015-03-24 06:52:02 +02:00
if vm_status is None :
raise QemuError ( " Suspending a QEMU VM is not supported " )
2017-01-17 17:03:12 +02:00
elif vm_status == " running " or vm_status == " prelaunch " :
2018-10-15 13:05:49 +03:00
await self . _control_vm ( " stop " )
2016-05-14 05:41:58 +03:00
self . status = " suspended "
2015-03-24 06:52:02 +02:00
log . debug ( " QEMU VM has been suspended " )
else :
2021-04-13 12:07:58 +03:00
log . info ( f " QEMU VM is not running to be suspended, current status is { vm_status } " )
2015-02-19 12:33:25 +02:00
2018-10-15 13:05:49 +03:00
async def reload ( self ) :
2015-02-19 12:33:25 +02:00
"""
Reloads this QEMU VM .
"""
2020-09-12 16:37:39 +03:00
if self . _command_line_changed :
await self . stop ( )
await self . start ( )
else :
await self . _control_vm ( " system_reset " )
2015-02-19 12:33:25 +02:00
log . debug ( " QEMU VM has been reset " )
2018-10-15 13:05:49 +03:00
async def resume ( self ) :
2015-02-19 12:33:25 +02:00
"""
Resumes this QEMU VM .
"""
2018-10-15 13:05:49 +03:00
vm_status = await self . _get_vm_status ( )
2015-03-23 04:40:19 +02:00
if vm_status is None :
raise QemuError ( " Resuming a QEMU VM is not supported " )
elif vm_status == " paused " :
2018-10-15 13:05:49 +03:00
await self . _control_vm ( " cont " )
2020-09-04 11:32:21 +03:00
self . status = " started "
2015-02-19 12:33:25 +02:00
log . debug ( " QEMU VM has been resumed " )
else :
2021-04-13 12:07:58 +03:00
log . info ( f " QEMU VM is not paused to be resumed, current status is { vm_status } " )
2015-02-19 12:33:25 +02:00
2018-10-15 13:05:49 +03:00
async def adapter_add_nio_binding ( self , adapter_number , nio ) :
2015-02-19 12:33:25 +02:00
"""
2018-10-27 10:47:17 +03:00
Adds an adapter NIO binding .
2015-02-19 12:33:25 +02:00
2015-04-08 20:17:34 +03:00
: param adapter_number : adapter number
2015-03-07 05:08:00 +02:00
: param nio : NIO instance to add to the adapter
2015-02-19 12:33:25 +02:00
"""
try :
2015-04-08 20:17:34 +03:00
adapter = self . _ethernet_adapters [ adapter_number ]
2015-02-19 12:33:25 +02:00
except IndexError :
2021-04-13 12:16:50 +03:00
raise QemuError (
' Adapter {adapter_number} does not exist on QEMU VM " {name} " ' . format (
name = self . _name , adapter_number = adapter_number
)
)
2015-02-19 12:33:25 +02:00
2017-07-12 11:07:21 +03:00
if self . is_running ( ) :
try :
2021-04-13 12:16:50 +03:00
await self . add_ubridge_udp_connection (
f " QEMU- { self . _id } - { adapter_number } " , self . _local_udp_tunnels [ adapter_number ] [ 1 ] , nio
)
2020-06-02 12:15:22 +03:00
if self . _replicate_network_connection_state :
2021-04-13 12:07:58 +03:00
await self . _control_vm ( f " set_link gns3- { adapter_number } on " )
2017-12-21 09:55:49 +02:00
except ( IndexError , KeyError ) :
2021-04-13 12:16:50 +03:00
raise QemuError (
' Adapter {adapter_number} does not exist on QEMU VM " {name} " ' . format (
name = self . _name , adapter_number = adapter_number
)
)
2015-02-19 12:33:25 +02:00
adapter . add_nio ( 0 , nio )
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ]: {nio} added to adapter {adapter_number} ' . format (
name = self . _name , id = self . _id , nio = nio , adapter_number = adapter_number
)
)
2017-07-12 15:50:33 +03:00
2018-10-15 13:05:49 +03:00
async def adapter_update_nio_binding ( self , adapter_number , nio ) :
2017-07-12 15:50:33 +03:00
"""
2018-10-27 10:47:17 +03:00
Update an adapter NIO binding .
2017-07-12 15:50:33 +03:00
: param adapter_number : adapter number
2018-10-27 10:47:17 +03:00
: param nio : NIO instance to update the adapter
2017-07-12 15:50:33 +03:00
"""
if self . is_running ( ) :
try :
2021-04-13 12:16:50 +03:00
await self . update_ubridge_udp_connection (
f " QEMU- { self . _id } - { adapter_number } " , self . _local_udp_tunnels [ adapter_number ] [ 1 ] , nio
)
2020-06-02 12:15:22 +03:00
if self . _replicate_network_connection_state :
if nio . suspend :
2021-04-13 12:07:58 +03:00
await self . _control_vm ( f " set_link gns3- { adapter_number } off " )
2020-06-02 12:15:22 +03:00
else :
2021-04-13 12:07:58 +03:00
await self . _control_vm ( f " set_link gns3- { adapter_number } on " )
2017-07-12 15:50:33 +03:00
except IndexError :
2021-04-13 12:16:50 +03:00
raise QemuError (
' Adapter {adapter_number} does not exist on QEMU VM " {name} " ' . format (
name = self . _name , adapter_number = adapter_number
)
)
2015-02-19 12:33:25 +02:00
2018-10-15 13:05:49 +03:00
async def adapter_remove_nio_binding ( self , adapter_number ) :
2015-02-19 12:33:25 +02:00
"""
2018-10-27 10:47:17 +03:00
Removes an adapter NIO binding .
2015-02-19 12:33:25 +02:00
2015-04-08 20:17:34 +03:00
: param adapter_number : adapter number
2015-02-19 12:33:25 +02:00
: returns : NIO instance
"""
try :
2015-04-08 20:17:34 +03:00
adapter = self . _ethernet_adapters [ adapter_number ]
2015-02-19 12:33:25 +02:00
except IndexError :
2021-04-13 12:16:50 +03:00
raise QemuError (
' Adapter {adapter_number} does not exist on QEMU VM " {name} " ' . format (
name = self . _name , adapter_number = adapter_number
)
)
2015-02-19 12:33:25 +02:00
2019-04-01 15:47:31 +03:00
await self . stop_capture ( adapter_number )
2017-07-12 11:07:21 +03:00
if self . is_running ( ) :
2020-06-02 12:15:22 +03:00
if self . _replicate_network_connection_state :
2021-04-13 12:07:58 +03:00
await self . _control_vm ( f " set_link gns3- { adapter_number } off " )
await self . _ubridge_send ( " bridge delete {name} " . format ( name = f " QEMU- { self . _id } - { adapter_number } " ) )
2015-02-19 12:33:25 +02:00
nio = adapter . get_nio ( 0 )
2015-03-07 05:08:00 +02:00
if isinstance ( nio , NIOUDP ) :
2015-03-22 01:19:12 +02:00
self . manager . port_manager . release_udp_port ( nio . lport , self . _project )
2015-02-19 12:33:25 +02:00
adapter . remove_nio ( 0 )
2016-06-24 01:56:06 +03:00
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ]: {nio} removed from adapter {adapter_number} ' . format (
name = self . _name , id = self . _id , nio = nio , adapter_number = adapter_number
)
)
2015-02-19 12:33:25 +02:00
return nio
2018-10-27 10:47:17 +03:00
def get_nio ( self , adapter_number ) :
2016-06-24 01:56:06 +03:00
"""
2018-10-27 10:47:17 +03:00
Gets an adapter NIO binding .
2016-06-24 01:56:06 +03:00
: param adapter_number : adapter number
2018-10-27 10:47:17 +03:00
: returns : NIO instance
2016-06-24 01:56:06 +03:00
"""
try :
adapter = self . _ethernet_adapters [ adapter_number ]
except IndexError :
2021-04-13 12:16:50 +03:00
raise QemuError (
' Adapter {adapter_number} does not exist on QEMU VM " {name} " ' . format (
name = self . _name , adapter_number = adapter_number
)
)
2016-06-24 01:56:06 +03:00
nio = adapter . get_nio ( 0 )
if not nio :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Adapter { adapter_number } is not connected " )
2016-06-24 01:56:06 +03:00
2018-10-27 10:47:17 +03:00
return nio
async def start_capture ( self , adapter_number , output_file ) :
"""
Starts a packet capture .
: param adapter_number : adapter number
: param output_file : PCAP destination file for the capture
"""
nio = self . get_nio ( adapter_number )
2016-06-24 01:56:06 +03:00
if nio . capturing :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Packet capture is already activated on adapter { adapter_number } " )
2016-06-24 01:56:06 +03:00
2019-04-01 16:58:18 +03:00
nio . start_packet_capture ( output_file )
2016-12-14 13:01:34 +02:00
if self . ubridge :
2021-04-13 12:16:50 +03:00
await self . _ubridge_send (
' bridge start_capture {name} " {output_file} " ' . format (
name = f " QEMU- { self . _id } - { adapter_number } " , output_file = output_file
)
)
log . info (
" QEMU VM ' {name} ' [ {id} ]: starting packet capture on adapter {adapter_number} " . format (
name = self . name , id = self . id , adapter_number = adapter_number
)
)
2016-06-24 01:56:06 +03:00
2018-10-15 13:05:49 +03:00
async def stop_capture ( self , adapter_number ) :
2016-06-24 01:56:06 +03:00
"""
Stops a packet capture .
: param adapter_number : adapter number
"""
2018-10-27 10:47:17 +03:00
nio = self . get_nio ( adapter_number )
2019-04-01 15:47:31 +03:00
if not nio . capturing :
return
2016-06-24 01:56:06 +03:00
2019-04-01 16:58:18 +03:00
nio . stop_packet_capture ( )
2016-12-14 13:01:34 +02:00
if self . ubridge :
2021-04-13 12:16:50 +03:00
await self . _ubridge_send ( " bridge stop_capture {name} " . format ( name = f " QEMU- { self . _id } - { adapter_number } " ) )
2016-06-24 01:56:06 +03:00
2021-04-13 12:16:50 +03:00
log . info (
" QEMU VM ' {name} ' [ {id} ]: stopping packet capture on adapter {adapter_number} " . format (
name = self . name , id = self . id , adapter_number = adapter_number
)
)
2016-08-26 11:40:11 +03:00
2015-02-19 12:33:25 +02:00
@property
def started ( self ) :
"""
Returns either this QEMU VM has been started or not .
: returns : boolean
"""
2015-03-04 17:01:56 +02:00
return self . status == " started "
2015-02-19 12:33:25 +02:00
def read_stdout ( self ) :
"""
Reads the standard output of the QEMU process .
Only use when the process has been stopped or has crashed .
"""
output = " "
if self . _stdout_file :
try :
2015-04-25 20:58:34 +03:00
with open ( self . _stdout_file , " rb " ) as file :
output = file . read ( ) . decode ( " utf-8 " , errors = " replace " )
2015-02-19 12:33:25 +02:00
except OSError as e :
2021-04-13 12:07:58 +03:00
log . warning ( f " Could not read { self . _stdout_file } : { e } " )
2017-11-17 13:13:34 +02:00
return output
def read_qemu_img_stdout ( self ) :
"""
Reads the standard output of the QEMU - IMG process .
"""
output = " "
if self . _qemu_img_stdout_file :
try :
with open ( self . _qemu_img_stdout_file , " rb " ) as file :
output = file . read ( ) . decode ( " utf-8 " , errors = " replace " )
except OSError as e :
2021-04-13 12:07:58 +03:00
log . warning ( f " Could not read { self . _qemu_img_stdout_file } : { e } " )
2015-02-19 12:33:25 +02:00
return output
def is_running ( self ) :
"""
Checks if the QEMU process is running
: returns : True or False
"""
2015-02-19 17:46:57 +02:00
if self . _process :
2015-02-25 16:42:01 +02:00
if self . _process . returncode is None :
return True
else :
self . _process = None
2015-02-19 12:33:25 +02:00
return False
def command ( self ) :
"""
Returns the QEMU command line .
: returns : QEMU command line ( string )
"""
return " " . join ( self . _build_command ( ) )
2018-03-24 14:11:21 +03:00
@BaseNode.console_type.setter
def console_type ( self , new_console_type ) :
"""
Sets the console type for this QEMU VM .
: param new_console_type : console type ( string )
"""
if self . is_running ( ) and self . console_type != new_console_type :
2021-04-13 12:07:58 +03:00
raise QemuError ( f ' " { self . _name } " must be stopped to change the console type to { new_console_type } ' )
2018-03-24 14:11:21 +03:00
super ( QemuVM , QemuVM ) . console_type . __set__ ( self , new_console_type )
2020-07-29 09:53:51 +03:00
def _serial_options ( self , internal_console_port , external_console_port ) :
2015-02-19 12:33:25 +02:00
2020-07-29 09:53:51 +03:00
if external_console_port :
2021-04-13 12:07:58 +03:00
return [ " -serial " , f " telnet:127.0.0.1: { internal_console_port } ,server,nowait " ]
2015-02-19 12:33:25 +02:00
else :
return [ ]
2020-07-29 09:53:51 +03:00
def _vnc_options ( self , port ) :
2015-06-25 04:09:17 +03:00
2020-07-29 09:53:51 +03:00
if port :
vnc_port = port - 5900 # subtract by 5900 to get the display number
2021-04-13 12:07:58 +03:00
return [ " -vnc " , f " { self . _manager . port_manager . console_host } : { vnc_port } " ]
2015-06-25 04:09:17 +03:00
else :
return [ ]
2020-07-29 09:53:51 +03:00
def _spice_options ( self , port ) :
2017-06-16 11:03:33 +03:00
2020-07-29 09:53:51 +03:00
if port :
2017-12-07 20:02:41 +02:00
console_host = self . _manager . port_manager . console_host
2020-06-09 08:36:41 +03:00
if console_host == " 0.0.0.0 " :
if socket . has_ipv6 :
# to fix an issue with Qemu when IPv4 is not enabled
# see https://github.com/GNS3/gns3-gui/issues/2352
# FIXME: consider making this more global (not just for Qemu + SPICE)
console_host = " :: "
else :
raise QemuError ( " IPv6 must be enabled in order to use the SPICE console " )
2021-04-13 12:16:50 +03:00
return [ " -spice " , f " addr= { console_host } ,port= { port } ,disable-ticketing " , " -vga " , " qxl " ]
2017-06-16 11:03:33 +03:00
else :
return [ ]
2020-07-29 09:53:51 +03:00
def _spice_with_agent_options ( self , port ) :
2018-03-25 10:36:14 +03:00
2020-07-29 09:53:51 +03:00
spice_options = self . _spice_options ( port )
if spice_options :
2018-03-25 10:36:14 +03:00
# agent options (mouse/screen)
2021-04-13 12:16:50 +03:00
agent_options = [
" -device " ,
" virtio-serial " ,
" -chardev " ,
" spicevmc,id=vdagent,debug=0,name=vdagent " ,
" -device " ,
" virtserialport,chardev=vdagent,name=com.redhat.spice.0 " ,
]
2018-03-25 10:36:14 +03:00
spice_options . extend ( agent_options )
# folder sharing options
2021-04-13 12:16:50 +03:00
folder_sharing_options = [
" -chardev " ,
" spiceport,name=org.spice-space.webdav.0,id=charchannel0 " ,
" -device " ,
" virtserialport,chardev=charchannel0,id=channel0,name=org.spice-space.webdav.0 " ,
]
2018-03-25 10:36:14 +03:00
spice_options . extend ( folder_sharing_options )
return spice_options
2020-07-29 09:53:51 +03:00
def _console_options ( self ) :
if self . _console_type == " telnet " and self . _wrap_console :
return self . _serial_options ( self . _internal_console_port , self . console )
elif self . _console_type == " vnc " :
return self . _vnc_options ( self . console )
elif self . _console_type == " spice " :
return self . _spice_options ( self . console )
elif self . _console_type == " spice+agent " :
return self . _spice_with_agent_options ( self . console )
elif self . _console_type != " none " :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Console type { self . _console_type } is unknown " )
2020-07-29 09:53:51 +03:00
def _aux_options ( self ) :
if self . _aux_type != " none " and self . _aux_type == self . _console_type :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Auxiliary console type { self . _aux_type } cannot be the same as console type " )
2020-07-29 09:53:51 +03:00
if self . _aux_type == " telnet " and self . _wrap_aux :
return self . _serial_options ( self . _internal_aux_port , self . aux )
elif self . _aux_type == " vnc " :
return self . _vnc_options ( self . aux )
elif self . _aux_type == " spice " :
return self . _spice_options ( self . aux )
elif self . _aux_type == " spice+agent " :
return self . _spice_with_agent_options ( self . aux )
elif self . _aux_type != " none " :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Auxiliary console type { self . _aux_type } is unknown " )
2020-07-29 09:53:51 +03:00
return [ ]
2015-02-19 12:33:25 +02:00
def _monitor_options ( self ) :
if self . _monitor :
2021-04-13 12:07:58 +03:00
return [ " -monitor " , f " tcp: { self . _monitor_host } : { self . _monitor } ,server,nowait " ]
2015-02-19 12:33:25 +02:00
else :
return [ ]
2015-07-27 17:19:15 +03:00
def _get_qemu_img ( self ) :
"""
Search the qemu - img binary in the same binary of the qemu binary
2021-09-05 12:04:37 +03:00
to avoid version incompatibility .
2015-07-27 17:19:15 +03:00
: returns : qemu - img path or raise an error
"""
2015-02-19 12:33:25 +02:00
2021-09-05 12:04:37 +03:00
qemu_path_dir = os . path . dirname ( self . qemu_path )
qemu_image_path = shutil . which ( " qemu-img " , path = qemu_path_dir )
if qemu_image_path :
return qemu_image_path
2021-09-09 10:06:17 +03:00
raise QemuError ( f " Could not find qemu-img in { qemu_path_dir } " )
2015-07-27 17:19:15 +03:00
2018-10-15 13:05:49 +03:00
async def _qemu_img_exec ( self , command ) :
2017-11-17 13:13:34 +02:00
self . _qemu_img_stdout_file = os . path . join ( self . working_dir , " qemu-img.log " )
2021-04-13 12:07:58 +03:00
log . info ( f " logging to { self . _qemu_img_stdout_file } " )
2022-01-19 13:58:36 +02:00
command_string = " " . join ( shlex . quote ( s ) for s in command )
2021-04-13 12:07:58 +03:00
log . info ( f " Executing qemu-img with: { command_string } " )
2017-11-17 13:13:34 +02:00
with open ( self . _qemu_img_stdout_file , " w " , encoding = " utf-8 " ) as fd :
2021-04-13 12:16:50 +03:00
process = await asyncio . create_subprocess_exec (
* command , stdout = fd , stderr = subprocess . STDOUT , cwd = self . working_dir
)
2018-10-15 13:05:49 +03:00
retcode = await process . wait ( )
2021-04-13 12:07:58 +03:00
log . info ( f " { self . _get_qemu_img ( ) } returned with { retcode } " )
2017-11-16 09:54:37 +02:00
return retcode
2021-09-05 12:04:37 +03:00
async def _find_disk_file_format ( self , disk ) :
qemu_img_path = self . _get_qemu_img ( )
try :
output = await subprocess_check_output ( qemu_img_path , " info " , " --output=json " , disk )
except subprocess . SubprocessError as e :
2021-09-09 10:06:17 +03:00
raise QemuError ( f " Error received while checking Qemu disk format: { e } " )
2021-09-05 12:04:37 +03:00
if output :
try :
json_data = json . loads ( output )
except ValueError as e :
2021-09-09 10:06:17 +03:00
raise QemuError ( f " Invalid JSON data returned by qemu-img: { e } " )
2021-09-05 12:04:37 +03:00
return json_data . get ( " format " )
2020-12-30 22:36:38 +02:00
async def _create_linked_clone ( self , disk_name , disk_image , disk ) :
2021-09-05 12:04:37 +03:00
2020-12-30 22:36:38 +02:00
try :
qemu_img_path = self . _get_qemu_img ( )
2021-09-05 12:04:37 +03:00
backing_file_format = await self . _find_disk_file_format ( disk_image )
if not backing_file_format :
2021-09-09 10:06:17 +03:00
raise QemuError ( f " Could not detect format for disk image: { disk_image } " )
2021-09-04 14:11:50 +03:00
backing_options , base_qcow2 = Qcow2 . backing_options ( disk_image )
if base_qcow2 and base_qcow2 . crypt_method :
# Workaround for https://gitlab.com/qemu-project/qemu/-/issues/441
# (we have to pass -u and the size). Also embed secret name.
command = [ qemu_img_path , " create " , " -b " , backing_options ,
2021-09-05 15:23:36 +03:00
" -F " , backing_file_format , " -f " , " qcow2 " , " -u " , disk , str ( base_qcow2 . size ) ]
2021-09-04 14:11:50 +03:00
else :
2021-09-05 15:23:36 +03:00
command = [ qemu_img_path , " create " , " -o " , " backing_file= {} " . format ( disk_image ) ,
" -F " , backing_file_format , " -f " , " qcow2 " , disk ]
2021-07-08 19:43:30 +03:00
2020-12-30 22:36:38 +02:00
retcode = await self . _qemu_img_exec ( command )
if retcode :
stdout = self . read_qemu_img_stdout ( )
2021-04-13 12:16:50 +03:00
raise QemuError (
" Could not create ' {} ' disk image: qemu-img returned with {} \n {} " . format ( disk_name , retcode , stdout )
)
2020-12-30 22:36:38 +02:00
except ( OSError , subprocess . SubprocessError ) as e :
stdout = self . read_qemu_img_stdout ( )
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Could not create ' { disk_name } ' disk image: { e } \n { stdout } " )
2020-12-30 22:36:38 +02:00
2020-04-15 21:50:59 +03:00
async def _mcopy ( self , image , * args ) :
2020-04-06 13:56:00 +03:00
try :
2020-04-15 21:50:59 +03:00
# read offset of first partition from MBR
with open ( image , " rb " ) as img_file :
mbr = img_file . read ( 512 )
part_type , offset , signature = struct . unpack ( " <450xB3xL52xH " , mbr )
if signature != 0xAA55 :
2021-04-13 12:07:58 +03:00
raise OSError ( f " mcopy failure: { image } : invalid MBR " )
2020-04-15 21:50:59 +03:00
if part_type not in ( 1 , 4 , 6 , 11 , 12 , 14 ) :
2021-04-13 12:16:50 +03:00
raise OSError ( " mcopy failure: {} : invalid partition type {:02X} " . format ( image , part_type ) )
2021-04-13 12:07:58 +03:00
part_image = image + f " @@ { offset } S "
2020-04-15 21:50:59 +03:00
process = await asyncio . create_subprocess_exec (
2021-04-13 12:16:50 +03:00
" mcopy " ,
" -i " ,
part_image ,
* args ,
2020-04-15 21:50:59 +03:00
stdin = subprocess . DEVNULL ,
2021-04-13 12:16:50 +03:00
stdout = subprocess . PIPE ,
stderr = subprocess . STDOUT ,
cwd = self . working_dir ,
)
2020-04-06 13:56:00 +03:00
( stdout , _ ) = await process . communicate ( )
retcode = process . returncode
except ( OSError , subprocess . SubprocessError ) as e :
2021-04-13 12:07:58 +03:00
raise OSError ( f " mcopy failure: { e } " )
2020-04-06 13:56:00 +03:00
if retcode != 0 :
stdout = stdout . decode ( " utf-8 " ) . rstrip ( )
if stdout :
2021-04-13 12:07:58 +03:00
raise OSError ( f " mcopy failure: { stdout } " )
2020-04-06 13:56:00 +03:00
else :
2021-04-13 12:07:58 +03:00
raise OSError ( f " mcopy failure: return code { retcode } " )
2020-04-06 13:56:00 +03:00
async def _export_config ( self ) :
disk_name = getattr ( self , " config_disk_name " )
2020-04-15 21:50:59 +03:00
if not disk_name :
return
disk = os . path . join ( self . working_dir , disk_name )
if not os . path . exists ( disk ) :
2020-04-06 13:56:00 +03:00
return
config_dir = os . path . join ( self . working_dir , " configs " )
zip_file = os . path . join ( self . working_dir , " config.zip " )
try :
os . mkdir ( config_dir )
2020-07-03 12:31:17 +03:00
await self . _mcopy ( disk , " -s " , " -m " , " -n " , " -- " , " ::/ " , config_dir )
2020-04-06 13:56:00 +03:00
if os . path . exists ( zip_file ) :
os . remove ( zip_file )
2020-06-28 10:21:57 +03:00
pack_zip ( zip_file , config_dir )
2020-04-06 13:56:00 +03:00
except OSError as e :
2021-04-13 12:07:58 +03:00
log . warning ( f " Can ' t export config: { e } " )
self . project . emit ( " log.warning " , { " message " : f " { self . _name } : Can ' t export config: { e } " } )
2020-07-03 12:31:17 +03:00
shutil . rmtree ( config_dir , ignore_errors = True )
2020-04-06 13:56:00 +03:00
async def _import_config ( self ) :
disk_name = getattr ( self , " config_disk_name " )
zip_file = os . path . join ( self . working_dir , " config.zip " )
if not disk_name or not os . path . exists ( zip_file ) :
return
config_dir = os . path . join ( self . working_dir , " configs " )
disk = os . path . join ( self . working_dir , disk_name )
2020-07-03 12:31:17 +03:00
disk_tmp = disk + " .tmp "
2020-04-06 13:56:00 +03:00
try :
os . mkdir ( config_dir )
2020-07-03 12:31:17 +03:00
shutil . copyfile ( getattr ( self , " config_disk_image " ) , disk_tmp )
2020-04-07 15:11:00 +03:00
unpack_zip ( zip_file , config_dir )
2021-04-13 12:16:50 +03:00
config_files = [ os . path . join ( config_dir , fname ) for fname in os . listdir ( config_dir ) ]
2020-04-06 13:56:00 +03:00
if config_files :
2020-07-03 12:31:17 +03:00
await self . _mcopy ( disk_tmp , " -s " , " -m " , " -o " , " -- " , * config_files , " ::/ " )
os . replace ( disk_tmp , disk )
2020-04-06 13:56:00 +03:00
except OSError as e :
2021-04-13 12:07:58 +03:00
log . warning ( f " Can ' t import config: { e } " )
self . project . emit ( " log.warning " , { " message " : f " { self . _name } : Can ' t import config: { e } " } )
2020-07-03 12:31:17 +03:00
if os . path . exists ( disk_tmp ) :
os . remove ( disk_tmp )
os . remove ( zip_file )
shutil . rmtree ( config_dir , ignore_errors = True )
2020-04-06 13:56:00 +03:00
2021-02-18 09:44:35 +02:00
async def _disk_interface_options ( self , disk , disk_index , interface , format = None ) :
2020-04-06 13:56:00 +03:00
options = [ ]
extra_drive_options = " "
if format :
2021-04-13 12:07:58 +03:00
extra_drive_options + = f " ,format= { format } "
2020-04-06 13:56:00 +03:00
2020-10-12 10:34:49 +03:00
# From Qemu man page: if the filename contains comma, you must double it
# (for instance, "file=my,,file" to use file "my,file").
disk = disk . replace ( " , " , " ,, " )
2020-04-06 13:56:00 +03:00
if interface == " sata " :
# special case, sata controller doesn't exist in Qemu
2021-04-13 12:16:50 +03:00
options . extend ( [ " -device " , f " ahci,id=ahci { disk_index } " ] )
options . extend (
[
" -drive " ,
f " file= { disk } ,if=none,id=drive { disk_index } ,index= { disk_index } ,media=disk { extra_drive_options } " ,
]
)
2021-02-14 06:09:02 +02:00
qemu_version = await self . manager . get_qemu_version ( self . qemu_path )
if qemu_version and parse_version ( qemu_version ) > = parse_version ( " 4.2.0 " ) :
# The ‘ ide-drive’ device is deprecated since version 4.2.0
# https://qemu.readthedocs.io/en/latest/system/deprecated.html#ide-drive-since-4-2
2021-04-13 12:16:50 +03:00
options . extend (
[ " -device " , f " ide-hd,drive=drive { disk_index } ,bus=ahci { disk_index } .0,id=drive { disk_index } " ]
)
2021-02-14 06:09:02 +02:00
else :
2021-04-13 12:16:50 +03:00
options . extend (
[ " -device " , f " ide-drive,drive=drive { disk_index } ,bus=ahci { disk_index } .0,id=drive { disk_index } " ]
)
2020-04-06 13:56:00 +03:00
elif interface == " nvme " :
2021-04-13 12:16:50 +03:00
options . extend (
[
" -drive " ,
f " file= { disk } ,if=none,id=drive { disk_index } ,index= { disk_index } ,media=disk { extra_drive_options } " ,
]
)
options . extend ( [ " -device " , f " nvme,drive=drive { disk_index } ,serial= { disk_index } " ] )
2020-04-06 13:56:00 +03:00
elif interface == " scsi " :
2021-04-13 12:16:50 +03:00
options . extend ( [ " -device " , f " virtio-scsi-pci,id=scsi { disk_index } " ] )
options . extend (
[
" -drive " ,
f " file= { disk } ,if=none,id=drive { disk_index } ,index= { disk_index } ,media=disk { extra_drive_options } " ,
]
)
options . extend ( [ " -device " , f " scsi-hd,drive=drive { disk_index } " ] )
# elif interface == "sd":
2020-04-06 13:56:00 +03:00
# options.extend(["-drive", 'file={},id=drive{},index={}{}'.format(disk, disk_index, disk_index, extra_drive_options)])
# options.extend(["-device", 'sd-card,drive=drive{},id=drive{}'.format(disk_index, disk_index, disk_index)])
else :
2021-04-13 12:16:50 +03:00
options . extend (
[
" -drive " ,
f " file= { disk } ,if= { interface } ,index= { disk_index } ,media=disk,id=drive { disk_index } { extra_drive_options } " ,
]
)
2020-04-06 13:56:00 +03:00
return options
2018-10-15 13:05:49 +03:00
async def _disk_options ( self ) :
2015-07-27 17:19:15 +03:00
options = [ ]
qemu_img_path = self . _get_qemu_img ( )
2016-04-05 13:35:07 +03:00
drives = [ " a " , " b " , " c " , " d " ]
2015-03-10 19:50:30 +02:00
2016-04-05 13:35:07 +03:00
for disk_index , drive in enumerate ( drives ) :
2020-08-18 04:24:11 +03:00
# prioritize config disk over harddisk d
2021-04-13 12:16:50 +03:00
if drive == " d " and self . _create_config_disk :
2020-08-18 04:24:11 +03:00
continue
2021-04-13 12:07:58 +03:00
disk_image = getattr ( self , f " _hd { drive } _disk_image " )
2016-04-05 13:35:07 +03:00
if not disk_image :
continue
2021-04-13 12:07:58 +03:00
interface = getattr ( self , f " hd { drive } _disk_interface " )
2020-08-15 09:44:16 +03:00
# fail-safe: use "ide" if there is a disk image and no interface type has been explicitly configured
if interface == " none " :
2020-08-15 10:05:31 +03:00
interface = " ide "
2021-04-13 12:07:58 +03:00
setattr ( self , f " hd { drive } _disk_interface " , interface )
2015-03-10 19:50:30 +02:00
2020-08-15 09:44:16 +03:00
disk_name = " hd " + drive
2016-04-05 13:35:07 +03:00
if not os . path . isfile ( disk_image ) or not os . path . exists ( disk_image ) :
if os . path . islink ( disk_image ) :
2021-04-13 12:16:50 +03:00
raise QemuError (
f " { disk_name } disk image ' { disk_image } ' linked to ' { os . path . realpath ( disk_image ) } ' is not accessible "
)
2015-03-10 19:50:30 +02:00
else :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " { disk_name } disk image ' { disk_image } ' is not accessible " )
2017-11-16 09:54:37 +02:00
else :
try :
# check for corrupt disk image
2018-10-15 13:05:49 +03:00
retcode = await self . _qemu_img_exec ( [ qemu_img_path , " check " , disk_image ] )
2017-11-16 09:54:37 +02:00
if retcode == 3 :
# image has leaked clusters, but is not corrupted, let's try to fix it
2021-04-13 12:07:58 +03:00
log . warning ( f " Qemu image { disk_image } has leaked clusters " )
2021-09-05 15:48:46 +03:00
if await self . _qemu_img_exec ( [ qemu_img_path , " check " , " -r " , " leaks " , " {} " . format ( disk_image ) ] ) == 3 :
2017-11-16 09:54:37 +02:00
self . project . emit ( " log.warning " , { " message " : " Qemu image ' {} ' has leaked clusters and could not be fixed " . format ( disk_image ) } )
elif retcode == 2 :
# image is corrupted, let's try to fix it
2021-04-13 12:07:58 +03:00
log . warning ( f " Qemu image { disk_image } is corrupted " )
2021-09-05 15:48:46 +03:00
if await self . _qemu_img_exec ( [ qemu_img_path , " check " , " -r " , " all " , " {} " . format ( disk_image ) ] ) == 2 :
2017-11-16 09:54:37 +02:00
self . project . emit ( " log.warning " , { " message " : " Qemu image ' {} ' is corrupted and could not be fixed " . format ( disk_image ) } )
2021-07-08 19:43:30 +03:00
# ignore retcode == 1. One reason is that the image is encrypted and there is no encrypt.key-secret available
2017-11-16 09:54:37 +02:00
except ( OSError , subprocess . SubprocessError ) as e :
2017-11-17 13:13:34 +02:00
stdout = self . read_qemu_img_stdout ( )
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Could not check ' { disk_name } ' disk image: { e } \n { stdout } " )
2017-11-16 09:54:37 +02:00
2016-10-24 22:39:35 +03:00
if self . linked_clone :
2021-04-13 12:07:58 +03:00
disk = os . path . join ( self . working_dir , f " { disk_name } _disk.qcow2 " )
2016-04-05 13:35:07 +03:00
if not os . path . exists ( disk ) :
# create the disk
2020-12-30 22:36:38 +02:00
await self . _create_linked_clone ( disk_name , disk_image , disk )
2016-04-05 13:41:26 +03:00
else :
2021-09-05 15:48:46 +03:00
backing_file_format = await self . _find_disk_file_format ( disk_image )
if not backing_file_format :
raise QemuError ( " Could not detect format for disk image: {} " . format ( disk_image ) )
2021-09-04 14:11:50 +03:00
# Rebase the image. This is in case the base image moved to a different directory,
# which will be the case if we imported a portable project. This uses
# get_abs_image_path(hdX_disk_image) and ignores the old base path embedded
# in the qcow2 file itself.
2016-04-05 13:41:26 +03:00
try :
qcow2 = Qcow2 ( disk )
2021-09-05 15:48:46 +03:00
await qcow2 . rebase ( qemu_img_path , disk_image , backing_file_format )
2016-04-05 13:41:26 +03:00
except ( Qcow2Error , OSError ) as e :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Could not use qcow2 disk image ' { disk_image } ' for { disk_name } { e } " )
2016-04-05 13:41:26 +03:00
2015-10-19 04:19:27 +03:00
else :
2016-04-05 13:35:07 +03:00
disk = disk_image
2017-01-10 15:11:40 +02:00
2021-02-14 06:09:02 +02:00
options . extend ( await self . _disk_interface_options ( disk , disk_index , interface ) )
2020-04-06 13:56:00 +03:00
# config disk
disk_image = getattr ( self , " config_disk_image " )
2020-08-14 11:27:24 +03:00
if disk_image and self . _create_config_disk :
2020-08-18 04:24:11 +03:00
disk_name = getattr ( self , " config_disk_name " )
disk = os . path . join ( self . working_dir , disk_name )
if self . hdd_disk_interface == " none " :
# use the HDA interface type if none has been configured for HDD
self . hdd_disk_interface = getattr ( self , " hda_disk_interface " , " none " )
await self . _import_config ( )
disk_exists = os . path . exists ( disk )
if not disk_exists :
try :
shutil . copyfile ( disk_image , disk )
disk_exists = True
except OSError as e :
2021-04-13 12:07:58 +03:00
log . warning ( f " Could not create ' { disk_name } ' disk image: { e } " )
2020-08-18 04:24:11 +03:00
if disk_exists :
2021-02-14 06:09:02 +02:00
options . extend ( await self . _disk_interface_options ( disk , 3 , self . hdd_disk_interface , " raw " ) )
2015-03-10 19:50:30 +02:00
2015-02-19 12:33:25 +02:00
return options
2018-10-15 13:05:49 +03:00
async def resize_disk ( self , drive_name , extend ) :
2018-03-26 14:05:49 +03:00
if self . is_running ( ) :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Cannot resize { drive_name } while the VM is running " )
2018-03-26 14:05:49 +03:00
if self . linked_clone :
2021-04-13 12:07:58 +03:00
disk_image_path = os . path . join ( self . working_dir , f " { drive_name } _disk.qcow2 " )
2020-12-30 22:36:38 +02:00
if not os . path . exists ( disk_image_path ) :
2021-04-13 12:07:58 +03:00
disk_image = getattr ( self , f " _ { drive_name } _disk_image " )
2020-12-30 22:36:38 +02:00
await self . _create_linked_clone ( drive_name , disk_image , disk_image_path )
2018-03-26 14:05:49 +03:00
else :
2021-04-13 12:07:58 +03:00
disk_image_path = getattr ( self , f " { drive_name } _disk_image " )
2018-03-26 14:05:49 +03:00
if not os . path . exists ( disk_image_path ) :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Disk path ' { disk_image_path } ' does not exist " )
2018-03-26 14:05:49 +03:00
qemu_img_path = self . _get_qemu_img ( )
2018-10-15 13:05:49 +03:00
await self . manager . resize_disk ( qemu_img_path , disk_image_path , extend )
2018-03-26 14:05:49 +03:00
2015-08-03 08:02:02 +03:00
def _cdrom_option ( self ) :
options = [ ]
if self . _cdrom_image :
if not os . path . isfile ( self . _cdrom_image ) or not os . path . exists ( self . _cdrom_image ) :
if os . path . islink ( self . _cdrom_image ) :
2021-04-13 12:16:50 +03:00
raise QemuError (
f " cdrom image ' { self . _cdrom_image } ' linked to ' { os . path . realpath ( self . _cdrom_image ) } ' is not accessible "
)
2015-08-03 08:02:02 +03:00
else :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " cdrom image ' { self . _cdrom_image } ' is not accessible " )
2015-08-03 08:02:02 +03:00
if self . _hdc_disk_image :
raise QemuError ( " You cannot use a disk image on hdc disk and a CDROM image at the same time " )
2020-10-11 06:32:22 +03:00
options . extend ( [ " -cdrom " , self . _cdrom_image . replace ( " , " , " ,, " ) ] )
2015-08-03 08:02:02 +03:00
return options
2016-12-08 17:18:30 +02:00
def _bios_option ( self ) :
options = [ ]
if self . _bios_image :
if not os . path . isfile ( self . _bios_image ) or not os . path . exists ( self . _bios_image ) :
if os . path . islink ( self . _bios_image ) :
2021-04-13 12:16:50 +03:00
raise QemuError (
f " bios image ' { self . _bios_image } ' linked to ' { os . path . realpath ( self . _bios_image ) } ' is not accessible "
)
2016-12-08 17:18:30 +02:00
else :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " bios image ' { self . _bios_image } ' is not accessible " )
2020-10-11 06:32:22 +03:00
options . extend ( [ " -bios " , self . _bios_image . replace ( " , " , " ,, " ) ] )
2016-12-08 17:18:30 +02:00
return options
2015-02-19 12:33:25 +02:00
def _linux_boot_options ( self ) :
options = [ ]
if self . _initrd :
if not os . path . isfile ( self . _initrd ) or not os . path . exists ( self . _initrd ) :
if os . path . islink ( self . _initrd ) :
2021-04-13 12:16:50 +03:00
raise QemuError (
f " initrd file ' { self . _initrd } ' linked to ' { os . path . realpath ( self . _initrd ) } ' is not accessible "
)
2015-02-19 12:33:25 +02:00
else :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " initrd file ' { self . _initrd } ' is not accessible " )
2020-10-11 06:32:22 +03:00
options . extend ( [ " -initrd " , self . _initrd . replace ( " , " , " ,, " ) ] )
2015-02-19 12:33:25 +02:00
if self . _kernel_image :
if not os . path . isfile ( self . _kernel_image ) or not os . path . exists ( self . _kernel_image ) :
if os . path . islink ( self . _kernel_image ) :
2021-04-13 12:16:50 +03:00
raise QemuError (
f " kernel image ' { self . _kernel_image } ' linked to ' { os . path . realpath ( self . _kernel_image ) } ' is not accessible "
)
2015-02-19 12:33:25 +02:00
else :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " kernel image ' { self . _kernel_image } ' is not accessible " )
2020-10-11 06:32:22 +03:00
options . extend ( [ " -kernel " , self . _kernel_image . replace ( " , " , " ,, " ) ] )
2015-02-19 12:33:25 +02:00
if self . _kernel_command_line :
options . extend ( [ " -append " , self . _kernel_command_line ] )
return options
2018-10-15 13:05:49 +03:00
async def _network_options ( self ) :
2015-02-19 12:33:25 +02:00
network_options = [ ]
2021-04-13 12:16:50 +03:00
network_options . extend (
[ " -net " , " none " ]
) # we do not want any user networking back-end if no adapter is connected.
2015-10-04 15:41:39 +03:00
2017-02-07 18:04:29 +02:00
# Each 32 PCI device we need to add a PCI bridge with max 9 bridges
pci_devices = 4 + len ( self . _ethernet_adapters ) # 4 PCI devices are use by default by qemu
2019-11-03 11:56:52 +02:00
pci_bridges = math . floor ( pci_devices / 32 )
pci_bridges_created = 0
if pci_bridges > = 1 :
2018-10-15 13:05:49 +03:00
qemu_version = await self . manager . get_qemu_version ( self . qemu_path )
2017-02-17 10:55:50 +02:00
if qemu_version and parse_version ( qemu_version ) < parse_version ( " 2.4.0 " ) :
2021-04-13 12:16:50 +03:00
raise QemuError (
" Qemu version 2.4 or later is required to run this VM with a large number of network adapters "
)
2017-02-17 10:55:50 +02:00
2019-11-03 11:56:52 +02:00
pci_device_id = 4 + pci_bridges # Bridge consume PCI ports
2015-05-06 23:59:01 +03:00
for adapter_number , adapter in enumerate ( self . _ethernet_adapters ) :
2016-04-25 17:36:20 +03:00
mac = int_to_macaddress ( macaddress_to_int ( self . _mac_address ) + adapter_number )
2016-06-24 01:56:06 +03:00
2017-07-11 14:42:47 +03:00
# use a local UDP tunnel to connect to uBridge instead
if adapter_number not in self . _local_udp_tunnels :
self . _local_udp_tunnels [ adapter_number ] = self . _create_local_udp_tunnel ( )
nio = self . _local_udp_tunnels [ adapter_number ] [ 0 ]
2018-04-02 18:27:12 +03:00
custom_adapter = self . _get_custom_adapter_settings ( adapter_number )
adapter_type = custom_adapter . get ( " adapter_type " , self . _adapter_type )
custom_mac_address = custom_adapter . get ( " mac_address " )
if custom_mac_address :
mac = int_to_macaddress ( macaddress_to_int ( custom_mac_address ) )
2021-10-19 08:02:27 +03:00
device_string = f " { adapter_type } ,mac= { mac } "
bridge_id = math . floor ( pci_device_id / 32 )
if bridge_id > 0 :
if pci_bridges_created < bridge_id :
network_options . extend ( [ " -device " , f " i82801b11-bridge,id=dmi_pci_bridge { bridge_id } " ] )
network_options . extend (
[
" -device " ,
" pci-bridge,id=pci-bridge {bridge_id} ,bus=dmi_pci_bridge {bridge_id} ,chassis_nr=0x1,addr=0x {bridge_id} ,shpc=off " . format (
bridge_id = bridge_id
) ,
]
)
pci_bridges_created + = 1
addr = pci_device_id % 32
device_string = f " { device_string } ,bus=pci-bridge { bridge_id } ,addr=0x { addr : 02x } "
pci_device_id + = 1
if nio :
network_options . extend ( [ " -device " , f " { device_string } ,netdev=gns3- { adapter_number } " ] )
if isinstance ( nio , NIOUDP ) :
network_options . extend (
[
" -netdev " ,
" socket,id=gns3- {} ,udp= {} : {} ,localaddr= {} : {} " . format (
adapter_number , nio . rhost , nio . rport , " 127.0.0.1 " , nio . lport
) ,
]
)
elif isinstance ( nio , NIOTAP ) :
network_options . extend (
[ " -netdev " , f " tap,id=gns3- { adapter_number } ,ifname= { nio . tap_device } ,script=no,downscript=no " ]
)
2015-05-06 23:59:01 +03:00
else :
2021-10-19 08:02:27 +03:00
network_options . extend ( [ " -device " , device_string ] )
2015-02-19 12:33:25 +02:00
return network_options
2019-02-22 13:04:49 +02:00
async def _disable_graphics ( self ) :
2015-02-23 21:21:00 +02:00
"""
2019-02-17 13:03:36 +02:00
Disable graphics depending of the QEMU version
2015-02-23 21:21:00 +02:00
"""
2015-04-08 20:17:34 +03:00
2019-02-17 13:03:36 +02:00
if any ( opt in self . _options for opt in [ " -display " , " -nographic " , " -curses " , " -sdl " " -spice " , " -vnc " ] ) :
2015-02-23 21:21:00 +02:00
return [ ]
2019-02-22 13:04:49 +02:00
version = await self . manager . get_qemu_version ( self . qemu_path )
2019-02-17 13:03:36 +02:00
if version and parse_version ( version ) > = parse_version ( " 3.0 " ) :
return [ " -display " , " none " ]
else :
2017-11-18 11:22:29 +02:00
return [ " -nographic " ]
2015-02-23 21:21:00 +02:00
2018-10-15 13:05:49 +03:00
async def _run_with_hardware_acceleration ( self , qemu_path , options ) :
2015-08-07 17:49:45 +03:00
"""
2018-03-21 11:41:25 +02:00
Check if we can run Qemu with hardware acceleration
2015-08-07 17:49:45 +03:00
: param qemu_path : Path to qemu
: param options : String of qemu user options
2018-03-21 11:41:25 +02:00
: returns : Boolean True if we need to enable hardware acceleration
"""
2021-04-12 10:32:23 +03:00
enable_hardware_accel = self . manager . config . settings . Qemu . enable_hardware_acceleration
require_hardware_accel = self . manager . config . settings . Qemu . require_hardware_acceleration
2021-07-27 10:04:51 +03:00
if enable_hardware_accel and " -machine accel=tcg " not in options :
2018-03-21 11:41:25 +02:00
# Turn OFF hardware acceleration for non x86 architectures
2022-01-19 13:58:36 +02:00
supported_binaries = [ " qemu-system-x86_64 " , " qemu-system-i386 " , " qemu-kvm " ]
2018-03-22 10:45:41 +02:00
if os . path . basename ( qemu_path ) not in supported_binaries :
2018-03-21 11:41:25 +02:00
if require_hardware_accel :
2021-04-13 12:16:50 +03:00
raise QemuError (
" Hardware acceleration can only be used with the following Qemu executables: {} " . format (
" , " . join ( supported_binaries )
)
)
2018-03-21 11:41:25 +02:00
else :
return False
2015-08-07 17:49:45 +03:00
2018-03-21 11:41:25 +02:00
if sys . platform . startswith ( " linux " ) and not os . path . exists ( " /dev/kvm " ) :
if require_hardware_accel :
2021-04-13 12:16:50 +03:00
raise QemuError (
" KVM acceleration cannot be used (/dev/kvm doesn ' t exist). It is possible to turn off KVM support in the gns3_server.conf by adding enable_hardware_acceleration = false to the [Qemu] section. "
)
2018-03-21 11:41:25 +02:00
else :
return False
elif sys . platform . startswith ( " darwin " ) :
2018-10-15 13:05:49 +03:00
process = await asyncio . create_subprocess_shell ( " kextstat | grep com.intel.kext.intelhaxm " )
await process . wait ( )
2018-03-22 17:37:09 +02:00
if process . returncode != 0 :
if require_hardware_accel :
2021-04-13 12:16:50 +03:00
raise QemuError (
" HAXM acceleration support is not installed on this host (com.intel.kext.intelhaxm extension not loaded) "
)
2018-03-22 17:37:09 +02:00
else :
return False
2015-08-07 17:49:45 +03:00
return True
return False
2018-10-15 13:05:49 +03:00
async def _clear_save_vm_stated ( self , snapshot_name = " GNS3_SAVED_STATE " ) :
2018-03-30 15:28:22 +03:00
drives = [ " a " , " b " , " c " , " d " ]
qemu_img_path = self . _get_qemu_img ( )
for disk_index , drive in enumerate ( drives ) :
2021-04-13 12:07:58 +03:00
disk_image = getattr ( self , f " _hd { drive } _disk_image " )
2018-03-30 15:28:22 +03:00
if not disk_image :
continue
try :
if self . linked_clone :
2021-04-13 12:07:58 +03:00
disk = os . path . join ( self . working_dir , f " hd { drive } _disk.qcow2 " )
2018-03-30 15:28:22 +03:00
else :
disk = disk_image
2018-04-06 09:19:54 +03:00
if not os . path . exists ( disk ) :
continue
2019-01-12 12:04:37 +02:00
output = await subprocess_check_output ( qemu_img_path , " info " , " --output=json " , disk )
2019-05-19 13:59:00 +03:00
if output :
try :
json_data = json . loads ( output )
except ValueError as e :
2021-04-13 12:16:50 +03:00
raise QemuError (
f " Invalid JSON data returned by qemu-img while looking for the Qemu VM saved state snapshot: { e } "
)
2019-05-19 13:59:00 +03:00
if " snapshots " in json_data :
for snapshot in json_data [ " snapshots " ] :
if snapshot [ " name " ] == snapshot_name :
# delete the snapshot
command = [ qemu_img_path , " snapshot " , " -d " , snapshot_name , disk ]
retcode = await self . _qemu_img_exec ( command )
if retcode :
stdout = self . read_qemu_img_stdout ( )
2021-04-13 12:07:58 +03:00
log . warning ( f " Could not delete saved VM state from disk { disk } : { stdout } " )
2019-05-19 13:59:00 +03:00
else :
2021-04-13 12:07:58 +03:00
log . info ( f " Deleted saved VM state from disk { disk } " )
2018-03-30 15:28:22 +03:00
except subprocess . SubprocessError as e :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Error while looking for the Qemu VM saved state snapshot: { e } " )
2018-03-30 15:28:22 +03:00
2018-10-15 13:05:49 +03:00
async def _saved_state_option ( self , snapshot_name = " GNS3_SAVED_STATE " ) :
2018-03-30 15:28:22 +03:00
drives = [ " a " , " b " , " c " , " d " ]
qemu_img_path = self . _get_qemu_img ( )
for disk_index , drive in enumerate ( drives ) :
2021-04-13 12:07:58 +03:00
disk_image = getattr ( self , f " _hd { drive } _disk_image " )
2018-03-30 15:28:22 +03:00
if not disk_image :
continue
try :
if self . linked_clone :
2021-04-13 12:07:58 +03:00
disk = os . path . join ( self . working_dir , f " hd { drive } _disk.qcow2 " )
2018-03-30 15:28:22 +03:00
else :
disk = disk_image
2018-04-06 09:19:54 +03:00
if not os . path . exists ( disk ) :
continue
2018-10-15 13:05:49 +03:00
output = await subprocess_check_output ( qemu_img_path , " info " , " --output=json " , disk )
2019-05-19 13:59:00 +03:00
if output :
try :
json_data = json . loads ( output )
except ValueError as e :
2021-04-13 12:16:50 +03:00
raise QemuError (
f " Invalid JSON data returned by qemu-img while looking for the Qemu VM saved state snapshot: { e } "
)
2019-05-19 13:59:00 +03:00
if " snapshots " in json_data :
for snapshot in json_data [ " snapshots " ] :
if snapshot [ " name " ] == snapshot_name :
2021-04-13 12:16:50 +03:00
log . info (
' QEMU VM " {name} " [ {id} ] VM saved state detected (snapshot name: {snapshot} ) ' . format (
name = self . _name , id = self . id , snapshot = snapshot_name
)
)
2020-10-11 06:32:22 +03:00
return [ " -loadvm " , snapshot_name . replace ( " , " , " ,, " ) ]
2019-05-19 13:59:00 +03:00
2018-03-30 15:28:22 +03:00
except subprocess . SubprocessError as e :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Error while looking for the Qemu VM saved state snapshot: { e } " )
2018-03-30 15:28:22 +03:00
return [ ]
2018-10-15 13:05:49 +03:00
async def _build_command ( self ) :
2015-02-19 12:33:25 +02:00
"""
Command to start the QEMU process .
( to be passed to subprocess . Popen ( ) )
"""
2020-10-11 06:32:22 +03:00
vm_name = self . _name . replace ( " , " , " ,, " )
project_path = self . project . path . replace ( " , " , " ,, " )
2016-10-03 16:54:20 +03:00
additional_options = self . _options . strip ( )
2020-10-11 06:32:22 +03:00
additional_options = additional_options . replace ( " % vm-name % " , ' " ' + vm_name . replace ( ' " ' , ' \\ " ' ) + ' " ' )
2017-11-18 12:36:11 +02:00
additional_options = additional_options . replace ( " % vm-id % " , self . _id )
additional_options = additional_options . replace ( " % project-id % " , self . project . id )
2020-10-11 06:32:22 +03:00
additional_options = additional_options . replace ( " % project-path % " , ' " ' + project_path . replace ( ' " ' , ' \\ " ' ) + ' " ' )
2019-06-04 19:00:44 +03:00
additional_options = additional_options . replace ( " %g uest-cid % " , str ( self . _guest_cid ) )
if self . _console_type != " none " and self . _console :
2019-05-18 10:31:21 +03:00
additional_options = additional_options . replace ( " %c onsole-port % " , str ( self . _console ) )
2015-02-19 17:46:57 +02:00
command = [ self . qemu_path ]
2020-10-11 06:32:22 +03:00
command . extend ( [ " -name " , vm_name ] )
2021-04-13 12:07:58 +03:00
command . extend ( [ " -m " , f " { self . _ram } M " ] )
2020-07-24 08:45:41 +03:00
# set the maximum number of the hotpluggable CPUs to match the number of CPUs to avoid issues.
maxcpus = self . _maxcpus
if self . _cpus > maxcpus :
maxcpus = self . _cpus
2021-04-13 12:07:58 +03:00
command . extend ( [ " -smp " , f " cpus= { self . _cpus } ,maxcpus= { maxcpus } ,sockets=1 " ] )
2021-04-13 12:16:50 +03:00
if await self . _run_with_hardware_acceleration ( self . qemu_path , self . _options ) :
2018-03-21 11:41:25 +02:00
if sys . platform . startswith ( " linux " ) :
command . extend ( [ " -enable-kvm " ] )
2018-10-15 13:05:49 +03:00
version = await self . manager . get_qemu_version ( self . qemu_path )
2018-03-21 11:41:25 +02:00
# Issue on some combo Intel CPU + KVM + Qemu 2.4.0
# https://github.com/GNS3/gns3-server/issues/685
if version and parse_version ( version ) > = parse_version ( " 2.4.0 " ) and self . platform == " x86_64 " :
command . extend ( [ " -machine " , " smm=off " ] )
2022-01-19 13:58:36 +02:00
elif sys . platform . startswith ( " darwin " ) :
2018-03-21 11:41:25 +02:00
command . extend ( [ " -enable-hax " ] )
2021-04-13 12:07:58 +03:00
command . extend ( [ " -boot " , f " order= { self . _boot_priority } " ] )
2016-12-08 17:18:30 +02:00
command . extend ( self . _bios_option ( ) )
command . extend ( self . _cdrom_option ( ) )
2021-04-13 12:07:58 +03:00
command . extend ( await self . _disk_options ( ) )
2015-02-19 12:33:25 +02:00
command . extend ( self . _linux_boot_options ( ) )
2016-10-03 16:54:20 +03:00
if " -uuid " not in additional_options :
command . extend ( [ " -uuid " , self . _id ] )
2020-07-29 09:53:51 +03:00
command . extend ( self . _console_options ( ) )
command . extend ( self . _aux_options ( ) )
2015-02-19 12:33:25 +02:00
command . extend ( self . _monitor_options ( ) )
2021-04-13 12:07:58 +03:00
command . extend ( await self . _network_options ( ) )
2018-03-30 17:18:44 +03:00
if self . on_close != " save_vm_state " :
2018-10-15 13:05:49 +03:00
await self . _clear_save_vm_stated ( )
2018-03-30 15:28:22 +03:00
else :
2021-04-13 12:07:58 +03:00
command . extend ( await self . _saved_state_option ( ) )
2019-02-17 13:03:36 +02:00
if self . _console_type == " telnet " :
2021-04-13 12:07:58 +03:00
command . extend ( await self . _disable_graphics ( ) )
2015-02-19 12:33:25 +02:00
if additional_options :
2015-03-31 23:14:08 +03:00
try :
command . extend ( shlex . split ( additional_options ) )
except ValueError as e :
2021-04-13 12:07:58 +03:00
raise QemuError ( f " Invalid additional options: { additional_options } error { e } " )
2015-02-19 12:33:25 +02:00
return command
2015-02-19 20:43:45 +02:00
2021-04-17 17:04:28 +03:00
def asdict ( self ) :
2021-04-13 12:16:50 +03:00
answer = { " project_id " : self . project . id , " node_id " : self . id , " node_directory " : self . working_path }
2015-03-10 19:50:30 +02:00
# Qemu has a long list of options. The JSON schema is the single source of information
2020-10-31 07:32:21 +02:00
for field in Qemu . schema ( ) [ " properties " ] :
2015-02-19 21:22:30 +02:00
if field not in answer :
2015-06-17 18:11:25 +03:00
try :
answer [ field ] = getattr ( self , field )
except AttributeError :
pass
2018-11-19 10:53:43 +02:00
answer [ " hda_disk_image " ] = self . manager . get_relative_image_path ( self . _hda_disk_image , self . project . path )
2015-06-17 18:11:25 +03:00
answer [ " hda_disk_image_md5sum " ] = md5sum ( self . _hda_disk_image )
2018-11-19 10:53:43 +02:00
answer [ " hdb_disk_image " ] = self . manager . get_relative_image_path ( self . _hdb_disk_image , self . project . path )
2015-06-17 18:11:25 +03:00
answer [ " hdb_disk_image_md5sum " ] = md5sum ( self . _hdb_disk_image )
2018-11-19 10:53:43 +02:00
answer [ " hdc_disk_image " ] = self . manager . get_relative_image_path ( self . _hdc_disk_image , self . project . path )
2015-06-17 18:11:25 +03:00
answer [ " hdc_disk_image_md5sum " ] = md5sum ( self . _hdc_disk_image )
2018-11-19 10:53:43 +02:00
answer [ " hdd_disk_image " ] = self . manager . get_relative_image_path ( self . _hdd_disk_image , self . project . path )
2015-06-17 18:11:25 +03:00
answer [ " hdd_disk_image_md5sum " ] = md5sum ( self . _hdd_disk_image )
2018-11-19 10:53:43 +02:00
answer [ " cdrom_image " ] = self . manager . get_relative_image_path ( self . _cdrom_image , self . project . path )
2015-08-03 08:02:02 +03:00
answer [ " cdrom_image_md5sum " ] = md5sum ( self . _cdrom_image )
2018-11-19 10:53:43 +02:00
answer [ " bios_image " ] = self . manager . get_relative_image_path ( self . _bios_image , self . project . path )
2016-12-08 17:18:30 +02:00
answer [ " bios_image_md5sum " ] = md5sum ( self . _bios_image )
2018-11-19 10:53:43 +02:00
answer [ " initrd " ] = self . manager . get_relative_image_path ( self . _initrd , self . project . path )
2015-06-17 18:11:25 +03:00
answer [ " initrd_md5sum " ] = md5sum ( self . _initrd )
2018-11-19 10:53:43 +02:00
answer [ " kernel_image " ] = self . manager . get_relative_image_path ( self . _kernel_image , self . project . path )
2015-06-17 18:11:25 +03:00
answer [ " kernel_image_md5sum " ] = md5sum ( self . _kernel_image )
2015-02-19 21:22:30 +02:00
return answer