1 # Copyright (c) 2016 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
14 """QEMU utilities library."""
16 from time import time, sleep
20 from robot.api import logger
22 from resources.libraries.python.ssh import SSH
23 from resources.libraries.python.constants import Constants
24 from resources.libraries.python.topology import NodeType
27 class QemuUtils(object):
30 __QEMU_BIN = '/opt/qemu/bin/qemu-system-x86_64'
31 # QEMU Machine Protocol socket
32 __QMP_SOCK = '/tmp/qmp.sock'
33 # QEMU Guest Agent socket
34 __QGA_SOCK = '/tmp/qga.sock'
39 self._qemu_opt['smp'] = '-smp 1,sockets=1,cores=1,threads=1'
40 # Daemonize the QEMU process after initialization. Default one
41 # management interface.
42 self._qemu_opt['options'] = '-cpu host -daemonize -enable-kvm ' \
43 '-machine pc-1.0,accel=kvm,usb=off,mem-merge=off ' \
44 '-net nic,macaddr=52:54:00:00:02:01 -balloon none'
45 self._qemu_opt['ssh_fwd_port'] = 10022
46 # Default serial console port
47 self._qemu_opt['serial_port'] = 4556
48 # Default 512MB virtual RAM
49 self._qemu_opt['mem_size'] = 512
50 # Default huge page mount point, required for Vhost-user interfaces.
51 self._qemu_opt['huge_mnt'] = '/mnt/huge'
52 # Default image for CSIT virl setup
53 self._qemu_opt['disk_image'] = '/var/lib/vm/vhost-nested.img'
54 # Affinity of qemu processes
55 self._qemu_opt['affinity'] = False
67 self._socks = [self.__QMP_SOCK, self.__QGA_SOCK]
69 def qemu_set_smp(self, cpus, cores, threads, sockets):
70 """Set SMP option for QEMU
72 :param cpus: Number of CPUs.
73 :param cores: Number of CPU cores on one socket.
74 :param threads: Number of threads on one CPU core.
75 :param sockets: Number of discrete sockets in the system.
81 self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
82 cpus, cores, threads, sockets)
84 def qemu_set_ssh_fwd_port(self, fwd_port):
85 """Set host port for guest SSH forwarding.
87 :param fwd_port: Port number on host for guest SSH forwarding.
90 self._qemu_opt['ssh_fwd_port'] = fwd_port
91 self._vm_info['port'] = fwd_port
93 def qemu_set_serial_port(self, port):
94 """Set serial console port.
96 :param port: Serial console port.
99 self._qemu_opt['serial_port'] = port
101 def qemu_set_mem_size(self, mem_size):
102 """Set virtual RAM size.
104 :param mem_size: RAM size in Mega Bytes.
107 self._qemu_opt['mem_size'] = int(mem_size)
109 def qemu_set_huge_mnt(self, huge_mnt):
110 """Set hugefile mount point.
112 :param huge_mnt: System hugefile mount point.
115 self._qemu_opt['huge_mnt'] = huge_mnt
117 def qemu_set_disk_image(self, disk_image):
120 :param disk_image: Path of the disk image.
121 :type disk_image: str
123 self._qemu_opt['disk_image'] = disk_image
125 def qemu_set_affinity(self, mask):
126 """Set qemu affinity by taskset with cpu mask.
128 :param mask: Hex CPU mask.
131 self._qemu_opt['affinity'] = mask
133 def qemu_set_node(self, node):
134 """Set node to run QEMU on.
136 :param node: Node to run QEMU on.
141 self._ssh.connect(node)
142 self._vm_info['host'] = node['host']
144 def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
145 """Add Vhost-user interface.
147 :param socket: Path of the unix socket.
148 :param server: If True the socket shall be a listening socket.
149 :param mac: Vhost-user interface MAC address (optional, otherwise is
150 used autogenerated MAC 52:54:00:00:04:xx).
156 # Create unix socket character device.
157 chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
161 self._qemu_opt['options'] += chardev
162 # Create Vhost-user network backend.
163 netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0}'.format(
165 self._qemu_opt['options'] += netdev
166 # If MAC is not specified use autogenerated 52:54:00:00:04:<vhost_id>
167 # e.g. vhost1 MAC is 52:54:00:00:04:01
169 mac = '52:54:00:00:04:{0:02x}'.format(self._vhost_id)
170 extend_options = 'csum=off,gso=off,guest_tso4=off,guest_tso6=off,'\
171 'guest_ecn=off,mrg_rxbuf=off'
172 # Create Virtio network device.
173 device = ' -device virtio-net-pci,netdev=vhost{0},mac={1},{2}'.format(
174 self._vhost_id, mac, extend_options)
175 self._qemu_opt['options'] += device
176 # Add interface MAC and socket to the node dict
177 if_data = {'mac_address': mac, 'socket': socket}
178 if_name = 'vhost{}'.format(self._vhost_id)
179 self._vm_info['interfaces'][if_name] = if_data
180 # Add socket to the socket list
181 self._socks.append(socket)
183 def _qemu_qmp_exec(self, cmd):
184 """Execute QMP command.
186 QMP is JSON based protocol which allows to control QEMU instance.
188 :param cmd: QMP command to execute.
190 :return: Command output in python representation of JSON format. The
191 { "return": {} } response is QMP's success response. An error
192 response will contain the "error" keyword instead of "return".
194 # To enter command mode, the qmp_capabilities command must be issued.
195 qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' + \
196 '{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc -U ' + \
198 (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
199 if 0 != int(ret_code):
200 logger.debug('QMP execute failed {0}'.format(stderr))
201 raise RuntimeError('QMP execute "{0}" failed on {1}'.format(cmd,
204 # Skip capabilities negotiation messages.
205 out_list = stdout.splitlines()
206 if len(out_list) < 3:
207 raise RuntimeError('Invalid QMP output on {0}'.format(
209 return json.loads(out_list[2])
211 def _qemu_qga_flush(self):
212 """Flush the QGA parser state
214 qga_cmd = 'printf "\xFF" | sudo -S nc ' \
215 '-q 1 -U ' + self.__QGA_SOCK
216 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
217 if 0 != int(ret_code):
218 logger.debug('QGA execute failed {0}'.format(stderr))
219 raise RuntimeError('QGA execute "{0}" failed on {1}'.format(cmd,
224 return json.loads(stdout.split('\n', 1)[0])
226 def _qemu_qga_exec(self, cmd):
227 """Execute QGA command.
229 QGA provide access to a system-level agent via standard QMP commands.
231 :param cmd: QGA command to execute.
234 qga_cmd = 'echo "{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc ' \
235 '-q 1 -U ' + self.__QGA_SOCK
236 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
237 if 0 != int(ret_code):
238 logger.debug('QGA execute failed {0}'.format(stderr))
239 raise RuntimeError('QGA execute "{0}" failed on {1}'.format(cmd,
244 return json.loads(stdout.split('\n', 1)[0])
246 def _wait_until_vm_boot(self, timeout=300):
247 """Wait until QEMU VM is booted.
249 Ping QEMU guest agent each 5s until VM booted or timeout.
251 :param timeout: Waiting timeout in seconds (optional, default 300s).
256 if time() - start > timeout:
257 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
258 self._qemu_opt['disk_image'], self._node['host']))
259 self._qemu_qga_flush()
260 out = self._qemu_qga_exec('guest-ping')
261 # Empty output - VM not booted yet
264 # Non-error return - VM booted
265 elif out.get('return') is not None:
267 # Skip error and wait
268 elif out.get('error') is not None:
271 raise RuntimeError('QGA guest-ping unexpected output {}'.format(
273 logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
276 def _update_vm_interfaces(self):
277 """Update interface names in VM node dict."""
278 # Send guest-network-get-interfaces command via QGA, output example:
279 # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
280 # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
281 out = self._qemu_qga_exec('guest-network-get-interfaces')
282 interfaces = out.get('return')
285 raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
286 self._qemu_opt['disk_image'], self._node['host']))
287 # Create MAC-name dict
288 for interface in interfaces:
289 if 'hardware-address' not in interface:
291 mac_name[interface['hardware-address']] = interface['name']
292 # Match interface by MAC and save interface name
293 for interface in self._vm_info['interfaces'].values():
294 mac = interface.get('mac_address')
295 if_name = mac_name.get(mac)
297 logger.trace('Interface name for MAC {} not found'.format(mac))
299 interface['name'] = if_name
301 def _huge_page_check(self):
302 """Huge page check."""
303 huge_mnt = self._qemu_opt.get('huge_mnt')
304 mem_size = self._qemu_opt.get('mem_size')
305 # Check size of free huge pages
306 (_, output, _) = self._ssh.exec_command('grep Huge /proc/meminfo')
307 regex = re.compile(r'HugePages_Free:\s+(\d+)')
308 match = regex.search(output)
309 huge_free = int(match.group(1))
310 regex = re.compile(r'Hugepagesize:\s+(\d+)')
311 match = regex.search(output)
312 huge_size = int(match.group(1))
313 if (mem_size * 1024) > (huge_free * huge_size):
314 raise RuntimeError('Not enough free huge pages {0} kB, required '
315 '{1} MB'.format(huge_free * huge_size, mem_size))
316 # Check if huge pages mount point exist
318 (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
319 for line in output.splitlines():
320 # Try to find something like:
321 # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
323 if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
326 # If huge page mount point not exist create one
328 cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
330 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
331 if int(ret_code) != 0:
332 logger.debug('Mount huge pages failed {0}'.format(stderr))
333 raise RuntimeError('Mount huge pages failed on {0}'.format(
336 def qemu_start(self):
337 """Start QEMU and wait until VM boot.
339 :return: VM node info.
341 .. note:: First set at least node to run QEMU on.
342 .. warning:: Starts only one VM on the node.
345 ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
346 self._qemu_opt.get('ssh_fwd_port'))
347 # Memory and huge pages
348 mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
349 'share=on -m {0} -numa node,memdev=mem'.format(
350 self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
351 self._huge_page_check()
352 # Setup QMP via unix socket
353 qmp = '-qmp unix:{0},server,nowait'.format(self.__QMP_SOCK)
354 # Setup serial console
355 serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \
356 'nowait -device isa-serial,chardev=gnc0'.format(
357 self._qemu_opt.get('serial_port'))
358 # Setup QGA via chardev (unix socket) and isa-serial channel
359 qga = '-chardev socket,path=/tmp/qga.sock,server,nowait,id=qga0 ' \
360 '-device isa-serial,chardev=qga0'
362 graphic = '-monitor none -display none -vga none'
363 qbin = 'taskset {0} {1}'.format(self._qemu_opt.get('affinity'),
364 self.__QEMU_BIN) if self._qemu_opt.get(
365 'affinity') else self.__QEMU_BIN
367 cmd = '{0} {1} {2} {3} {4} -hda {5} {6} {7} {8} {9}'.format(
368 qbin, self._qemu_opt.get('smp'), mem, ssh_fwd,
369 self._qemu_opt.get('options'),
370 self._qemu_opt.get('disk_image'), qmp, serial, qga, graphic)
371 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300)
372 if int(ret_code) != 0:
373 logger.debug('QEMU start failed {0}'.format(stderr))
374 raise RuntimeError('QEMU start failed on {0}'.format(
376 logger.trace('QEMU running')
378 self._wait_until_vm_boot()
379 # Update interface names in VM node dict
380 self._update_vm_interfaces()
381 # Return VM node dict
385 """Quit the QEMU emulator."""
386 out = self._qemu_qmp_exec('quit')
387 err = out.get('error')
389 raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
390 self._node['host'], json.dumps(err)))
392 def qemu_system_powerdown(self):
393 """Power down the system (if supported)."""
394 out = self._qemu_qmp_exec('system_powerdown')
395 err = out.get('error')
397 raise RuntimeError('QEMU system powerdown failed on {0}, '
398 'error: {1}'.format(self._node['host'], json.dumps(err)))
400 def qemu_system_reset(self):
401 """Reset the system."""
402 out = self._qemu_qmp_exec('system_reset')
403 err = out.get('error')
405 raise RuntimeError('QEMU system reset failed on {0}, '
406 'error: {1}'.format(self._node['host'], json.dumps(err)))
409 """Kill qemu process."""
410 # TODO: add PID storage so that we can kill specific PID
411 # Note: in QEMU start phase there are 3 QEMU processes because we
413 self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
415 def qemu_clear_socks(self):
416 """Remove all sockets created by QEMU."""
417 # If serial console port still open kill process
418 cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port'))
419 self._ssh.exec_command_sudo(cmd)
420 # Delete all created sockets
421 for sock in self._socks:
422 cmd = 'rm -f {}'.format(sock)
423 self._ssh.exec_command_sudo(cmd)
425 def qemu_system_status(self):
426 """Return current VM status.
428 VM should be in following status:
430 - debug: QEMU running on a debugger
431 - finish-migrate: paused to finish the migration process
432 - inmigrate: waiting for an incoming migration
433 - internal-error: internal error has occurred
434 - io-error: the last IOP has failed
436 - postmigrate: paused following a successful migrate
437 - prelaunch: QEMU was started with -S and guest has not started
438 - restore-vm: paused to restore VM state
439 - running: actively running
440 - save-vm: paused to save the VM state
441 - shutdown: shut down (and -no-shutdown is in use)
442 - suspended: suspended (ACPI S3)
443 - watchdog: watchdog action has been triggered
444 - guest-panicked: panicked as a result of guest OS panic
449 out = self._qemu_qmp_exec('query-status')
450 ret = out.get('return')
452 return ret.get('status')
454 err = out.get('error')
455 raise RuntimeError('QEMU query-status failed on {0}, '
456 'error: {1}'.format(self._node['host'], json.dumps(err)))
459 def build_qemu(node):
460 """Build QEMU from sources.
462 :param node: Node to build QEMU on.
468 (ret_code, stdout, stderr) = \
469 ssh.exec_command('sudo -Sn bash {0}/{1}/qemu_build.sh'.format(
470 Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH), 1000)
472 if 0 != int(ret_code):
473 logger.debug('QEMU build failed {0}'.format(stderr))
474 raise RuntimeError('QEMU build failed on {0}'.format(node['host']))