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
19 from robot.api import logger
21 from resources.libraries.python.ssh import SSH, SSHTimeout
22 from resources.libraries.python.constants import Constants
23 from resources.libraries.python.topology import NodeType, Topology
26 class QemuUtils(object):
29 def __init__(self, qemu_id=1):
30 self._qemu_id = qemu_id
31 # Path to QEMU binary. Use x86_64 by default
32 self._qemu_bin = '/usr/bin/qemu-system-x86_64'
33 # QEMU Machine Protocol socket
34 self._qmp_sock = '/tmp/qmp{0}.sock'.format(self._qemu_id)
35 # QEMU Guest Agent socket
36 self._qga_sock = '/tmp/qga{0}.sock'.format(self._qemu_id)
38 self._pid_file = '/tmp/qemu{0}.pid'.format(self._qemu_id)
41 self._qemu_opt['smp'] = '-smp 1,sockets=1,cores=1,threads=1'
42 # Daemonize the QEMU process after initialization. Default one
43 # management interface.
44 self._qemu_opt['options'] = '-cpu host -daemonize -enable-kvm ' \
45 '-machine pc,accel=kvm,usb=off,mem-merge=off ' \
46 '-net nic,macaddr=52:54:00:00:{0:02x}:ff -balloon none'\
47 .format(self._qemu_id)
48 self._qemu_opt['ssh_fwd_port'] = 10021 + qemu_id
49 # Default serial console port
50 self._qemu_opt['serial_port'] = 4555 + qemu_id
51 # Default 512MB virtual RAM
52 self._qemu_opt['mem_size'] = 512
53 # Default huge page mount point, required for Vhost-user interfaces.
54 self._qemu_opt['huge_mnt'] = '/mnt/huge'
55 # Default do not allocate huge pages.
56 self._qemu_opt['huge_allocate'] = False
57 # Default image for CSIT virl setup
58 self._qemu_opt['disk_image'] = '/var/lib/vm/vhost-nested.img'
62 'port': self._qemu_opt['ssh_fwd_port'],
68 self._qemu_opt['queues'] = 1
72 self._socks = [self._qmp_sock, self._qga_sock]
74 def qemu_set_bin(self, path):
75 """Set binary path for QEMU.
77 :param path: Absolute path in filesystem.
82 def qemu_set_smp(self, cpus, cores, threads, sockets):
83 """Set SMP option for QEMU.
85 :param cpus: Number of CPUs.
86 :param cores: Number of CPU cores on one socket.
87 :param threads: Number of threads on one CPU core.
88 :param sockets: Number of discrete sockets in the system.
94 self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
95 cpus, cores, threads, sockets)
97 def qemu_set_ssh_fwd_port(self, fwd_port):
98 """Set host port for guest SSH forwarding.
100 :param fwd_port: Port number on host for guest SSH forwarding.
103 self._qemu_opt['ssh_fwd_port'] = fwd_port
104 self._vm_info['port'] = fwd_port
106 def qemu_set_serial_port(self, port):
107 """Set serial console port.
109 :param port: Serial console port.
112 self._qemu_opt['serial_port'] = port
114 def qemu_set_mem_size(self, mem_size):
115 """Set virtual RAM size.
117 :param mem_size: RAM size in Mega Bytes.
120 self._qemu_opt['mem_size'] = int(mem_size)
122 def qemu_set_huge_mnt(self, huge_mnt):
123 """Set hugefile mount point.
125 :param huge_mnt: System hugefile mount point.
128 self._qemu_opt['huge_mnt'] = huge_mnt
130 def qemu_set_huge_allocate(self):
131 """Set flag to allocate more huge pages if needed."""
132 self._qemu_opt['huge_allocate'] = True
134 def qemu_set_disk_image(self, disk_image):
137 :param disk_image: Path of the disk image.
138 :type disk_image: str
140 self._qemu_opt['disk_image'] = disk_image
142 def qemu_set_affinity(self, *host_cpus):
143 """Set qemu affinity by getting thread PIDs via QMP and taskset to list
146 :param host_cpus: List of CPU cores.
147 :type host_cpus: list
149 qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
151 if len(qemu_cpus) != len(host_cpus):
152 logger.debug('Host CPU count {0}, Qemu Thread count {1}'.format(
153 len(host_cpus), len(qemu_cpus)))
154 raise ValueError('Host CPU count must match Qemu Thread count')
156 for qemu_cpu, host_cpu in zip(qemu_cpus, host_cpus):
157 cmd = 'taskset -pc {0} {1}'.format(host_cpu, qemu_cpu['thread_id'])
158 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
159 if int(ret_code) != 0:
160 logger.debug('Set affinity failed {0}'.format(stderr))
161 raise RuntimeError('Set affinity failed on {0}'.format(
164 def qemu_set_scheduler_policy(self):
165 """Set scheduler policy to SCHED_RR with priority 1 for all Qemu CPU
168 :raises RuntimeError: Set scheduler policy failed.
170 qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
172 for qemu_cpu in qemu_cpus:
173 cmd = 'chrt -r -p 1 {0}'.format(qemu_cpu['thread_id'])
174 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
175 if int(ret_code) != 0:
176 logger.debug('Set SCHED_RR failed {0}'.format(stderr))
177 raise RuntimeError('Set SCHED_RR failed on {0}'.format(
180 def qemu_set_node(self, node):
181 """Set node to run QEMU on.
183 :param node: Node to run QEMU on.
188 self._ssh.connect(node)
189 self._vm_info['host'] = node['host']
191 arch = Topology.get_node_arch(node)
192 self._qemu_bin = '/usr/bin/qemu-system-{0}'.format(arch)
194 def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
195 """Add Vhost-user interface.
197 :param socket: Path of the unix socket.
198 :param server: If True the socket shall be a listening socket.
199 :param mac: Vhost-user interface MAC address (optional, otherwise is
200 used auto-generated MAC 52:54:00:00:xx:yy).
206 # Create unix socket character device.
207 chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
211 self._qemu_opt['options'] += chardev
212 # Create Vhost-user network backend.
213 netdev = (' -netdev vhost-user,id=vhost{0},chardev=char{0},queues={1}'
214 .format(self._vhost_id, self._qemu_opt['queues']))
215 self._qemu_opt['options'] += netdev
216 # If MAC is not specified use auto-generated MAC address based on
217 # template 52:54:00:00:<qemu_id>:<vhost_id>, e.g. vhost1 MAC of QEMU
218 # with ID 1 is 52:54:00:00:01:01
220 mac = '52:54:00:00:{0:02x}:{1:02x}'.\
221 format(self._qemu_id, self._vhost_id)
222 extend_options = 'mq=on,csum=off,gso=off,guest_tso4=off,'\
223 'guest_tso6=off,guest_ecn=off,mrg_rxbuf=off'
224 # Create Virtio network device.
225 device = ' -device virtio-net-pci,netdev=vhost{0},mac={1},{2}'.format(
226 self._vhost_id, mac, extend_options)
227 self._qemu_opt['options'] += device
228 # Add interface MAC and socket to the node dict
229 if_data = {'mac_address': mac, 'socket': socket}
230 if_name = 'vhost{}'.format(self._vhost_id)
231 self._vm_info['interfaces'][if_name] = if_data
232 # Add socket to the socket list
233 self._socks.append(socket)
235 def _qemu_qmp_exec(self, cmd):
236 """Execute QMP command.
238 QMP is JSON based protocol which allows to control QEMU instance.
240 :param cmd: QMP command to execute.
242 :return: Command output in python representation of JSON format. The
243 { "return": {} } response is QMP's success response. An error
244 response will contain the "error" keyword instead of "return".
246 # To enter command mode, the qmp_capabilities command must be issued.
247 qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' \
248 '{ \\"execute\\": \\"' + cmd + \
249 '\\" }" | sudo -S socat - UNIX-CONNECT:' + self._qmp_sock
251 (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
252 if int(ret_code) != 0:
253 logger.debug('QMP execute failed {0}'.format(stderr))
254 raise RuntimeError('QMP execute "{0}"'
255 ' failed on {1}'.format(cmd, self._node['host']))
257 # Skip capabilities negotiation messages.
258 out_list = stdout.splitlines()
259 if len(out_list) < 3:
260 raise RuntimeError('Invalid QMP output on {0}'.format(
262 return json.loads(out_list[2])
264 def _qemu_qga_flush(self):
265 """Flush the QGA parser state
267 qga_cmd = '(printf "\xFF"; sleep 1) | sudo -S socat - UNIX-CONNECT:' + \
269 #TODO: probably need something else
270 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
271 if int(ret_code) != 0:
272 logger.debug('QGA execute failed {0}'.format(stderr))
273 raise RuntimeError('QGA execute "{0}" '
274 'failed on {1}'.format(qga_cmd,
279 return json.loads(stdout.split('\n', 1)[0])
281 def _qemu_qga_exec(self, cmd):
282 """Execute QGA command.
284 QGA provide access to a system-level agent via standard QMP commands.
286 :param cmd: QGA command to execute.
289 qga_cmd = '(echo "{ \\"execute\\": \\"' + \
291 '\\" }"; sleep 1) | sudo -S socat - UNIX-CONNECT:' + \
293 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
294 if int(ret_code) != 0:
295 logger.debug('QGA execute failed {0}'.format(stderr))
296 raise RuntimeError('QGA execute "{0}"'
297 ' failed on {1}'.format(cmd, self._node['host']))
301 return json.loads(stdout.split('\n', 1)[0])
303 def _wait_until_vm_boot(self, timeout=60):
304 """Wait until QEMU VM is booted.
306 First try to flush qga until there is output.
307 Then ping QEMU guest agent each 5s until VM booted or timeout.
309 :param timeout: Waiting timeout in seconds (optional, default 60s).
314 if time() - start > timeout:
315 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
316 self._qemu_opt['disk_image'], self._node['host']))
319 out = self._qemu_qga_flush()
321 logger.trace('QGA qga flush unexpected output {}'.format(out))
322 # Empty output - VM not booted yet
328 if time() - start > timeout:
329 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
330 self._qemu_opt['disk_image'], self._node['host']))
333 out = self._qemu_qga_exec('guest-ping')
335 logger.trace('QGA guest-ping unexpected output {}'.format(out))
336 # Empty output - VM not booted yet
339 # Non-error return - VM booted
340 elif out.get('return') is not None:
342 # Skip error and wait
343 elif out.get('error') is not None:
346 # If there is an unexpected output from QGA guest-info, try
347 # again until timeout.
348 logger.trace('QGA guest-ping unexpected output {}'.format(out))
350 logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
353 def _update_vm_interfaces(self):
354 """Update interface names in VM node dict."""
355 # Send guest-network-get-interfaces command via QGA, output example:
356 # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
357 # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
358 out = self._qemu_qga_exec('guest-network-get-interfaces')
359 interfaces = out.get('return')
362 raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
363 self._qemu_opt['disk_image'], self._node['host']))
364 # Create MAC-name dict
365 for interface in interfaces:
366 if 'hardware-address' not in interface:
368 mac_name[interface['hardware-address']] = interface['name']
369 # Match interface by MAC and save interface name
370 for interface in self._vm_info['interfaces'].values():
371 mac = interface.get('mac_address')
372 if_name = mac_name.get(mac)
374 logger.trace('Interface name for MAC {} not found'.format(mac))
376 interface['name'] = if_name
378 def _huge_page_check(self, allocate=False):
379 """Huge page check."""
380 huge_mnt = self._qemu_opt.get('huge_mnt')
381 mem_size = self._qemu_opt.get('mem_size')
383 # Get huge pages information
384 huge_size = self._get_huge_page_size()
385 huge_free = self._get_huge_page_free(huge_size)
386 huge_total = self._get_huge_page_total(huge_size)
388 # Check if memory reqested by qemu is available on host
389 if (mem_size * 1024) > (huge_free * huge_size):
390 # If we want to allocate hugepage dynamically
392 mem_needed = abs((huge_free * huge_size) - (mem_size * 1024))
393 huge_to_allocate = ((mem_needed / huge_size) * 2) + huge_total
394 max_map_count = huge_to_allocate*4
395 # Increase maximum number of memory map areas a process may have
396 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/max_map_count'.format(
398 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
399 # Increase hugepage count
400 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/nr_hugepages'.format(
402 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
403 if int(ret_code) != 0:
404 logger.debug('Mount huge pages failed {0}'.format(stderr))
405 raise RuntimeError('Mount huge pages failed on {0}'.format(
407 # If we do not want to allocate dynamicaly end with error
410 'Not enough free huge pages: {0}, '
411 '{1} MB'.format(huge_free, huge_free * huge_size)
413 # Check if huge pages mount point exist
415 (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
416 for line in output.splitlines():
417 # Try to find something like:
418 # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
420 if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
423 # If huge page mount point not exist create one
425 cmd = 'mkdir -p {0}'.format(huge_mnt)
426 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
427 if int(ret_code) != 0:
428 logger.debug('Create mount dir failed: {0}'.format(stderr))
429 raise RuntimeError('Create mount dir failed on {0}'.format(
431 cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
433 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
434 if int(ret_code) != 0:
435 logger.debug('Mount huge pages failed {0}'.format(stderr))
436 raise RuntimeError('Mount huge pages failed on {0}'.format(
439 def _get_huge_page_size(self):
440 """Get default size of huge pages in system.
442 :returns: Default size of free huge pages in system.
444 :raises: RuntimeError if reading failed for three times.
446 # TODO: remove to dedicated library
447 cmd_huge_size = "grep Hugepagesize /proc/meminfo | awk '{ print $2 }'"
449 (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_size)
454 logger.trace('Reading huge page size information failed')
458 raise RuntimeError('Getting huge page size information failed.')
461 def _get_huge_page_free(self, huge_size):
462 """Get total number of huge pages in system.
464 :param huge_size: Size of hugepages.
466 :returns: Number of free huge pages in system.
468 :raises: RuntimeError if reading failed for three times.
470 # TODO: add numa aware option
471 # TODO: remove to dedicated library
472 cmd_huge_free = 'cat /sys/kernel/mm/hugepages/hugepages-{0}kB/'\
473 'free_hugepages'.format(huge_size)
475 (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_free)
480 logger.trace('Reading free huge pages information failed')
484 raise RuntimeError('Getting free huge pages information failed.')
487 def _get_huge_page_total(self, huge_size):
488 """Get total number of huge pages in system.
490 :param huge_size: Size of hugepages.
492 :returns: Total number of huge pages in system.
494 :raises: RuntimeError if reading failed for three times.
496 # TODO: add numa aware option
497 # TODO: remove to dedicated library
498 cmd_huge_total = 'cat /sys/kernel/mm/hugepages/hugepages-{0}kB/'\
499 'nr_hugepages'.format(huge_size)
501 (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_total)
504 huge_total = int(out)
506 logger.trace('Reading total huge pages information failed')
510 raise RuntimeError('Getting total huge pages information failed.')
513 def qemu_start(self):
514 """Start QEMU and wait until VM boot.
516 :return: VM node info.
518 .. note:: First set at least node to run QEMU on.
519 .. warning:: Starts only one VM on the node.
522 ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
523 self._qemu_opt.get('ssh_fwd_port'))
524 # Memory and huge pages
525 mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
526 'share=on -m {0} -numa node,memdev=mem'.format(
527 self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
529 # By default check only if hugepages are available.
530 # If 'huge_allocate' is set to true try to allocate as well.
531 self._huge_page_check(allocate=self._qemu_opt.get('huge_allocate'))
534 drive = '-drive file={0},format=raw,cache=none,if=virtio'.format(
535 self._qemu_opt.get('disk_image'))
536 # Setup QMP via unix socket
537 qmp = '-qmp unix:{0},server,nowait'.format(self._qmp_sock)
538 # Setup serial console
539 serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \
540 'nowait -device isa-serial,chardev=gnc0'.format(
541 self._qemu_opt.get('serial_port'))
542 # Setup QGA via chardev (unix socket) and isa-serial channel
543 qga = '-chardev socket,path={0},server,nowait,id=qga0 ' \
544 '-device isa-serial,chardev=qga0'.format(self._qga_sock)
546 graphic = '-monitor none -display none -vga none'
548 pid = '-pidfile {}'.format(self._pid_file)
551 cmd = '{0} {1} {2} {3} {4} {5} {6} {7} {8} {9} {10}'.format(
552 self._qemu_bin, self._qemu_opt.get('smp'), mem, ssh_fwd,
553 self._qemu_opt.get('options'),
554 drive, qmp, serial, qga, graphic, pid)
556 (ret_code, _, _) = self._ssh.exec_command_sudo(cmd, timeout=300)
557 if int(ret_code) != 0:
558 raise RuntimeError('QEMU start failed on {0}'.format(
561 self._wait_until_vm_boot()
562 except (RuntimeError, SSHTimeout):
564 self.qemu_clear_socks()
566 logger.trace('QEMU started successfully.')
567 # Update interface names in VM node dict
568 self._update_vm_interfaces()
569 # Return VM node dict
573 """Quit the QEMU emulator."""
574 out = self._qemu_qmp_exec('quit')
575 err = out.get('error')
577 raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
578 self._node['host'], json.dumps(err)))
580 def qemu_system_powerdown(self):
581 """Power down the system (if supported)."""
582 out = self._qemu_qmp_exec('system_powerdown')
583 err = out.get('error')
586 'QEMU system powerdown failed on {0}, '
587 'error: {1}'.format(self._node['host'], json.dumps(err))
590 def qemu_system_reset(self):
591 """Reset the system."""
592 out = self._qemu_qmp_exec('system_reset')
593 err = out.get('error')
596 'QEMU system reset failed on {0}, '
597 'error: {1}'.format(self._node['host'], json.dumps(err)))
600 """Kill qemu process."""
601 # Note: in QEMU start phase there are 3 QEMU processes because we
603 self._ssh.exec_command_sudo('chmod +r {}'.format(self._pid_file))
604 self._ssh.exec_command_sudo('kill -SIGKILL $(cat {})'
605 .format(self._pid_file))
607 cmd = 'rm -f {}'.format(self._pid_file)
608 self._ssh.exec_command_sudo(cmd)
610 def qemu_kill_all(self, node=None):
611 """Kill all qemu processes on DUT node if specified.
613 :param node: Node to kill all QEMU processes on.
617 self.qemu_set_node(node)
618 self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
620 def qemu_clear_socks(self):
621 """Remove all sockets created by QEMU."""
622 # If serial console port still open kill process
623 cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port'))
624 self._ssh.exec_command_sudo(cmd)
625 # Delete all created sockets
626 for sock in self._socks:
627 cmd = 'rm -f {}'.format(sock)
628 self._ssh.exec_command_sudo(cmd)
630 def qemu_system_status(self):
631 """Return current VM status.
633 VM should be in following status:
635 - debug: QEMU running on a debugger
636 - finish-migrate: paused to finish the migration process
637 - inmigrate: waiting for an incoming migration
638 - internal-error: internal error has occurred
639 - io-error: the last IOP has failed
641 - postmigrate: paused following a successful migrate
642 - prelaunch: QEMU was started with -S and guest has not started
643 - restore-vm: paused to restore VM state
644 - running: actively running
645 - save-vm: paused to save the VM state
646 - shutdown: shut down (and -no-shutdown is in use)
647 - suspended: suspended (ACPI S3)
648 - watchdog: watchdog action has been triggered
649 - guest-panicked: panicked as a result of guest OS panic
654 out = self._qemu_qmp_exec('query-status')
655 ret = out.get('return')
657 return ret.get('status')
659 err = out.get('error')
661 'QEMU query-status failed on {0}, '
662 'error: {1}'.format(self._node['host'], json.dumps(err)))
665 def build_qemu(node, force_install=False, apply_patch=False):
666 """Build QEMU from sources.
668 :param node: Node to build QEMU on.
669 :param force_install: If True, then remove previous build.
670 :param apply_patch: If True, then apply patches from qemu_patches dir.
672 :type force_install: bool
673 :type apply_patch: bool
674 :raises: RuntimeError if building QEMU failed.
679 directory = ' --directory={0}'.format(Constants.QEMU_INSTALL_DIR)
680 version = ' --version={0}'.format(Constants.QEMU_INSTALL_VERSION)
681 force = ' --force' if force_install else ''
682 patch = ' --patch' if apply_patch else ''
683 arch = Topology.get_node_arch(node)
684 target_list = ' --target-list={0}-softmmu'.format(arch)
686 (ret_code, stdout, stderr) = \
688 "sudo -E sh -c '{0}/{1}/qemu_build.sh{2}{3}{4}{5}{6}'"\
689 .format(Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH,
690 version, directory, force, patch, target_list), 1000)
692 if int(ret_code) != 0:
693 logger.debug('QEMU build failed {0}'.format(stdout + stderr))
694 raise RuntimeError('QEMU build failed on {0}'.format(node['host']))