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
26 class QemuUtils(object):
29 __QEMU_BIN = '/usr/bin/qemu-system-x86_64'
31 def __init__(self, qemu_id=1):
32 self._qemu_id = qemu_id
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)
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,accel=kvm,usb=off,mem-merge=off ' \
44 '-net nic,macaddr=52:54:00:00:00:{0:02x} -balloon none'\
45 .format(self._qemu_id)
46 self._qemu_opt['ssh_fwd_port'] = 10022
47 # Default serial console port
48 self._qemu_opt['serial_port'] = 4556
49 # Default 512MB virtual RAM
50 self._qemu_opt['mem_size'] = 512
51 # Default huge page mount point, required for Vhost-user interfaces.
52 self._qemu_opt['huge_mnt'] = '/mnt/huge'
53 # Default do not allocate huge pages.
54 self._qemu_opt['huge_allocate'] = False
55 # Default image for CSIT virl setup
56 self._qemu_opt['disk_image'] = '/var/lib/vm/vhost-nested.img'
60 'port': self._qemu_opt['ssh_fwd_port'],
66 self._qemu_opt['queues'] = 1
70 self._socks = [self._qmp_sock, self._qga_sock]
72 def qemu_set_smp(self, cpus, cores, threads, sockets):
73 """Set SMP option for QEMU
75 :param cpus: Number of CPUs.
76 :param cores: Number of CPU cores on one socket.
77 :param threads: Number of threads on one CPU core.
78 :param sockets: Number of discrete sockets in the system.
84 self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
85 cpus, cores, threads, sockets)
87 def qemu_set_ssh_fwd_port(self, fwd_port):
88 """Set host port for guest SSH forwarding.
90 :param fwd_port: Port number on host for guest SSH forwarding.
93 self._qemu_opt['ssh_fwd_port'] = fwd_port
94 self._vm_info['port'] = fwd_port
96 def qemu_set_serial_port(self, port):
97 """Set serial console port.
99 :param port: Serial console port.
102 self._qemu_opt['serial_port'] = port
104 def qemu_set_mem_size(self, mem_size):
105 """Set virtual RAM size.
107 :param mem_size: RAM size in Mega Bytes.
110 self._qemu_opt['mem_size'] = int(mem_size)
112 def qemu_set_huge_mnt(self, huge_mnt):
113 """Set hugefile mount point.
115 :param huge_mnt: System hugefile mount point.
118 self._qemu_opt['huge_mnt'] = huge_mnt
120 def qemu_set_huge_allocate(self):
121 """Set flag to allocate more huge pages if needed."""
122 self._qemu_opt['huge_allocate'] = True
124 def qemu_set_disk_image(self, disk_image):
127 :param disk_image: Path of the disk image.
128 :type disk_image: str
130 self._qemu_opt['disk_image'] = disk_image
132 def qemu_set_affinity(self, *host_cpus):
133 """Set qemu affinity by getting thread PIDs via QMP and taskset to list
136 :param host_cpus: List of CPU cores.
137 :type host_cpus: list
139 qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
141 if len(qemu_cpus) != len(host_cpus):
142 logger.debug('Host CPU count {0}, Qemu Thread count {1}'.format(
143 len(host_cpus), len(qemu_cpus)))
144 raise ValueError('Host CPU count must match Qemu Thread count')
146 for qemu_cpu, host_cpu in zip(qemu_cpus, host_cpus):
147 cmd = 'taskset -pc {0} {1}'.format(host_cpu, qemu_cpu['thread_id'])
148 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
149 if int(ret_code) != 0:
150 logger.debug('Set affinity failed {0}'.format(stderr))
151 raise RuntimeError('Set affinity failed on {0}'.format(
154 def qemu_set_scheduler_policy(self):
155 """Set scheduler policy to SCHED_RR with priority 1 for all Qemu CPU
158 :raises RuntimeError: Set scheduler policy failed.
160 qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
162 for qemu_cpu in qemu_cpus:
163 cmd = 'chrt -r -p 1 {0}'.format(qemu_cpu['thread_id'])
164 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
165 if int(ret_code) != 0:
166 logger.debug('Set SCHED_RR failed {0}'.format(stderr))
167 raise RuntimeError('Set SCHED_RR failed on {0}'.format(
170 def qemu_set_node(self, node):
171 """Set node to run QEMU on.
173 :param node: Node to run QEMU on.
178 self._ssh.connect(node)
179 self._vm_info['host'] = node['host']
181 def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
182 """Add Vhost-user interface.
184 :param socket: Path of the unix socket.
185 :param server: If True the socket shall be a listening socket.
186 :param mac: Vhost-user interface MAC address (optional, otherwise is
187 used auto-generated MAC 52:54:00:00:xx:yy).
193 # Create unix socket character device.
194 chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
198 self._qemu_opt['options'] += chardev
199 # Create Vhost-user network backend.
200 netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0},'\
201 'queues={1}'.format(self._vhost_id, self._qemu_opt['queues'])
202 self._qemu_opt['options'] += netdev
203 # If MAC is not specified use auto-generated MAC address based on
204 # template 52:54:00:00:<qemu_id>:<vhost_id>, e.g. vhost1 MAC of QEMU
205 # with ID 1 is 52:54:00:00:01:01
207 mac = '52:54:00:00:{0:02x}:{1:02x}'.\
208 format(self._qemu_id, self._vhost_id)
209 extend_options = 'mq=on,csum=off,gso=off,guest_tso4=off,'\
210 'guest_tso6=off,guest_ecn=off,mrg_rxbuf=off'
211 # Create Virtio network device.
212 device = ' -device virtio-net-pci,netdev=vhost{0},mac={1},{2}'.format(
213 self._vhost_id, mac, extend_options)
214 self._qemu_opt['options'] += device
215 # Add interface MAC and socket to the node dict
216 if_data = {'mac_address': mac, 'socket': socket}
217 if_name = 'vhost{}'.format(self._vhost_id)
218 self._vm_info['interfaces'][if_name] = if_data
219 # Add socket to the socket list
220 self._socks.append(socket)
222 def _qemu_qmp_exec(self, cmd):
223 """Execute QMP command.
225 QMP is JSON based protocol which allows to control QEMU instance.
227 :param cmd: QMP command to execute.
229 :return: Command output in python representation of JSON format. The
230 { "return": {} } response is QMP's success response. An error
231 response will contain the "error" keyword instead of "return".
233 # To enter command mode, the qmp_capabilities command must be issued.
234 qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' \
235 '{ \\"execute\\": \\"' + cmd + \
236 '\\" }" | sudo -S socat - UNIX-CONNECT:' + self._qmp_sock
238 (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
239 if int(ret_code) != 0:
240 logger.debug('QMP execute failed {0}'.format(stderr))
241 raise RuntimeError('QMP execute "{0}"'
242 ' failed on {1}'.format(cmd, self._node['host']))
244 # Skip capabilities negotiation messages.
245 out_list = stdout.splitlines()
246 if len(out_list) < 3:
247 raise RuntimeError('Invalid QMP output on {0}'.format(
249 return json.loads(out_list[2])
251 def _qemu_qga_flush(self):
252 """Flush the QGA parser state
254 qga_cmd = '(printf "\xFF"; sleep 1) | sudo -S socat - UNIX-CONNECT:' + \
256 #TODO: probably need something else
257 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
258 if int(ret_code) != 0:
259 logger.debug('QGA execute failed {0}'.format(stderr))
260 raise RuntimeError('QGA execute "{0}" '
261 'failed on {1}'.format(qga_cmd,
266 return json.loads(stdout.split('\n', 1)[0])
268 def _qemu_qga_exec(self, cmd):
269 """Execute QGA command.
271 QGA provide access to a system-level agent via standard QMP commands.
273 :param cmd: QGA command to execute.
276 qga_cmd = '(echo "{ \\"execute\\": \\"' + \
278 '\\" }"; sleep 1) | sudo -S socat - UNIX-CONNECT:' + \
280 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
281 if int(ret_code) != 0:
282 logger.debug('QGA execute failed {0}'.format(stderr))
283 raise RuntimeError('QGA execute "{0}"'
284 ' failed on {1}'.format(cmd, self._node['host']))
288 return json.loads(stdout.split('\n', 1)[0])
290 def _wait_until_vm_boot(self, timeout=60):
291 """Wait until QEMU VM is booted.
293 Ping QEMU guest agent each 5s until VM booted or timeout.
295 :param timeout: Waiting timeout in seconds (optional, default 60s).
300 if time() - start > timeout:
301 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
302 self._qemu_opt['disk_image'], self._node['host']))
305 self._qemu_qga_flush()
306 out = self._qemu_qga_exec('guest-ping')
308 logger.trace('QGA guest-ping unexpected output {}'.format(out))
309 # Empty output - VM not booted yet
312 # Non-error return - VM booted
313 elif out.get('return') is not None:
315 # Skip error and wait
316 elif out.get('error') is not None:
319 # If there is an unexpected output from QGA guest-info, try
320 # again until timeout.
321 logger.trace('QGA guest-ping unexpected output {}'.format(out))
323 logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
326 def _update_vm_interfaces(self):
327 """Update interface names in VM node dict."""
328 # Send guest-network-get-interfaces command via QGA, output example:
329 # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
330 # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
331 out = self._qemu_qga_exec('guest-network-get-interfaces')
332 interfaces = out.get('return')
335 raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
336 self._qemu_opt['disk_image'], self._node['host']))
337 # Create MAC-name dict
338 for interface in interfaces:
339 if 'hardware-address' not in interface:
341 mac_name[interface['hardware-address']] = interface['name']
342 # Match interface by MAC and save interface name
343 for interface in self._vm_info['interfaces'].values():
344 mac = interface.get('mac_address')
345 if_name = mac_name.get(mac)
347 logger.trace('Interface name for MAC {} not found'.format(mac))
349 interface['name'] = if_name
351 def _huge_page_check(self, allocate=False):
352 """Huge page check."""
353 huge_mnt = self._qemu_opt.get('huge_mnt')
354 mem_size = self._qemu_opt.get('mem_size')
356 # Get huge pages information
357 huge_size = self._get_huge_page_size()
358 huge_free = self._get_huge_page_free(huge_size)
359 huge_total = self._get_huge_page_total(huge_size)
361 # Check if memory reqested by qemu is available on host
362 if (mem_size * 1024) > (huge_free * huge_size):
363 # If we want to allocate hugepage dynamically
365 mem_needed = abs((huge_free * huge_size) - (mem_size * 1024))
366 huge_to_allocate = ((mem_needed / huge_size) * 2) + huge_total
367 max_map_count = huge_to_allocate*4
368 # Increase maximum number of memory map areas a process may have
369 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/max_map_count'.format(
371 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
372 # Increase hugepage count
373 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/nr_hugepages'.format(
375 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
376 if int(ret_code) != 0:
377 logger.debug('Mount huge pages failed {0}'.format(stderr))
378 raise RuntimeError('Mount huge pages failed on {0}'.format(
380 # If we do not want to allocate dynamicaly end with error
383 'Not enough free huge pages: {0}, '
384 '{1} MB'.format(huge_free, huge_free * huge_size)
386 # Check if huge pages mount point exist
388 (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
389 for line in output.splitlines():
390 # Try to find something like:
391 # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
393 if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
396 # If huge page mount point not exist create one
398 cmd = 'mkdir -p {0}'.format(huge_mnt)
399 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
400 if int(ret_code) != 0:
401 logger.debug('Create mount dir failed: {0}'.format(stderr))
402 raise RuntimeError('Create mount dir failed on {0}'.format(
404 cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
406 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
407 if int(ret_code) != 0:
408 logger.debug('Mount huge pages failed {0}'.format(stderr))
409 raise RuntimeError('Mount huge pages failed on {0}'.format(
412 def _get_huge_page_size(self):
413 """Get default size of huge pages in system.
415 :returns: Default size of free huge pages in system.
417 :raises: RuntimeError if reading failed for three times.
419 # TODO: remove to dedicated library
420 cmd_huge_size = "grep Hugepagesize /proc/meminfo | awk '{ print $2 }'"
422 (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_size)
427 logger.trace('Reading huge page size information failed')
431 raise RuntimeError('Getting huge page size information failed.')
434 def _get_huge_page_free(self, huge_size):
435 """Get total number of huge pages in system.
437 :param huge_size: Size of hugepages.
439 :returns: Number of free huge pages in system.
441 :raises: RuntimeError if reading failed for three times.
443 # TODO: add numa aware option
444 # TODO: remove to dedicated library
445 cmd_huge_free = 'cat /sys/kernel/mm/hugepages/hugepages-{0}kB/'\
446 'free_hugepages'.format(huge_size)
448 (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_free)
453 logger.trace('Reading free huge pages information failed')
457 raise RuntimeError('Getting free huge pages information failed.')
460 def _get_huge_page_total(self, huge_size):
461 """Get total number of huge pages in system.
463 :param huge_size: Size of hugepages.
465 :returns: Total number of huge pages in system.
467 :raises: RuntimeError if reading failed for three times.
469 # TODO: add numa aware option
470 # TODO: remove to dedicated library
471 cmd_huge_total = 'cat /sys/kernel/mm/hugepages/hugepages-{0}kB/'\
472 'nr_hugepages'.format(huge_size)
474 (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_total)
477 huge_total = int(out)
479 logger.trace('Reading total huge pages information failed')
483 raise RuntimeError('Getting total huge pages information failed.')
486 def qemu_start(self):
487 """Start QEMU and wait until VM boot.
489 :return: VM node info.
491 .. note:: First set at least node to run QEMU on.
492 .. warning:: Starts only one VM on the node.
495 ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
496 self._qemu_opt.get('ssh_fwd_port'))
497 # Memory and huge pages
498 mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
499 'share=on -m {0} -numa node,memdev=mem'.format(
500 self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
502 # By default check only if hugepages are available.
503 # If 'huge_allocate' is set to true try to allocate as well.
504 self._huge_page_check(allocate=self._qemu_opt.get('huge_allocate'))
507 drive = '-drive file={0},format=raw,cache=none,if=virtio'.format(
508 self._qemu_opt.get('disk_image'))
509 # Setup QMP via unix socket
510 qmp = '-qmp unix:{0},server,nowait'.format(self._qmp_sock)
511 # Setup serial console
512 serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \
513 'nowait -device isa-serial,chardev=gnc0'.format(
514 self._qemu_opt.get('serial_port'))
515 # Setup QGA via chardev (unix socket) and isa-serial channel
516 qga = '-chardev socket,path={0},server,nowait,id=qga0 ' \
517 '-device isa-serial,chardev=qga0'.format(self._qga_sock)
519 graphic = '-monitor none -display none -vga none'
522 cmd = '{0} {1} {2} {3} {4} {5} {6} {7} {8} {9}'.format(
523 self.__QEMU_BIN, self._qemu_opt.get('smp'), mem, ssh_fwd,
524 self._qemu_opt.get('options'),
525 drive, qmp, serial, qga, graphic)
526 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300)
527 if int(ret_code) != 0:
528 logger.debug('QEMU start failed {0}'.format(stderr))
529 raise RuntimeError('QEMU start failed on {0}'.format(
531 logger.trace('QEMU running')
534 self._wait_until_vm_boot()
535 except (RuntimeError, SSHTimeout):
537 self.qemu_clear_socks()
539 # Update interface names in VM node dict
540 self._update_vm_interfaces()
541 # Return VM node dict
545 """Quit the QEMU emulator."""
546 out = self._qemu_qmp_exec('quit')
547 err = out.get('error')
549 raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
550 self._node['host'], json.dumps(err)))
552 def qemu_system_powerdown(self):
553 """Power down the system (if supported)."""
554 out = self._qemu_qmp_exec('system_powerdown')
555 err = out.get('error')
558 'QEMU system powerdown failed on {0}, '
559 'error: {1}'.format(self._node['host'], json.dumps(err))
562 def qemu_system_reset(self):
563 """Reset the system."""
564 out = self._qemu_qmp_exec('system_reset')
565 err = out.get('error')
568 'QEMU system reset failed on {0}, '
569 'error: {1}'.format(self._node['host'], json.dumps(err)))
572 """Kill qemu process."""
573 # TODO: add PID storage so that we can kill specific PID
574 # Note: in QEMU start phase there are 3 QEMU processes because we
576 self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
578 def qemu_clear_socks(self):
579 """Remove all sockets created by QEMU."""
580 # If serial console port still open kill process
581 cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port'))
582 self._ssh.exec_command_sudo(cmd)
583 # Delete all created sockets
584 for sock in self._socks:
585 cmd = 'rm -f {}'.format(sock)
586 self._ssh.exec_command_sudo(cmd)
588 def qemu_system_status(self):
589 """Return current VM status.
591 VM should be in following status:
593 - debug: QEMU running on a debugger
594 - finish-migrate: paused to finish the migration process
595 - inmigrate: waiting for an incoming migration
596 - internal-error: internal error has occurred
597 - io-error: the last IOP has failed
599 - postmigrate: paused following a successful migrate
600 - prelaunch: QEMU was started with -S and guest has not started
601 - restore-vm: paused to restore VM state
602 - running: actively running
603 - save-vm: paused to save the VM state
604 - shutdown: shut down (and -no-shutdown is in use)
605 - suspended: suspended (ACPI S3)
606 - watchdog: watchdog action has been triggered
607 - guest-panicked: panicked as a result of guest OS panic
612 out = self._qemu_qmp_exec('query-status')
613 ret = out.get('return')
615 return ret.get('status')
617 err = out.get('error')
619 'QEMU query-status failed on {0}, '
620 'error: {1}'.format(self._node['host'], json.dumps(err)))
623 def build_qemu(node):
624 """Build QEMU from sources.
626 :param node: Node to build QEMU on.
632 (ret_code, stdout, stderr) = \
633 ssh.exec_command('sudo -Sn bash {0}/{1}/qemu_build.sh'.format(
634 Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH), 1000)
636 if int(ret_code) != 0:
637 logger.debug('QEMU build failed {0}'.format(stderr))
638 raise RuntimeError('QEMU build failed on {0}'.format(node['host']))