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'
30 # QEMU Machine Protocol socket
31 __QMP_SOCK = '/tmp/qmp.sock'
32 # QEMU Guest Agent socket
33 __QGA_SOCK = '/tmp/qga.sock'
38 self._qemu_opt['smp'] = '-smp 1,sockets=1,cores=1,threads=1'
39 # Daemonize the QEMU process after initialization. Default one
40 # management interface.
41 self._qemu_opt['options'] = '-cpu host -daemonize -enable-kvm ' \
42 '-machine pc,accel=kvm,usb=off,mem-merge=off ' \
43 '-net nic,macaddr=52:54:00:00:02:01 -balloon none'
44 self._qemu_opt['ssh_fwd_port'] = 10022
45 # Default serial console port
46 self._qemu_opt['serial_port'] = 4556
47 # Default 512MB virtual RAM
48 self._qemu_opt['mem_size'] = 512
49 # Default huge page mount point, required for Vhost-user interfaces.
50 self._qemu_opt['huge_mnt'] = '/mnt/huge'
51 # Default do not allocate huge pages.
52 self._qemu_opt['huge_allocate'] = False
53 # Default image for CSIT virl setup
54 self._qemu_opt['disk_image'] = '/var/lib/vm/vhost-nested.img'
64 self._qemu_opt['queues'] = 1
68 self._socks = [self.__QMP_SOCK, self.__QGA_SOCK]
70 def qemu_set_smp(self, cpus, cores, threads, sockets):
71 """Set SMP option for QEMU
73 :param cpus: Number of CPUs.
74 :param cores: Number of CPU cores on one socket.
75 :param threads: Number of threads on one CPU core.
76 :param sockets: Number of discrete sockets in the system.
82 self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
83 cpus, cores, threads, sockets)
85 def qemu_set_ssh_fwd_port(self, fwd_port):
86 """Set host port for guest SSH forwarding.
88 :param fwd_port: Port number on host for guest SSH forwarding.
91 self._qemu_opt['ssh_fwd_port'] = fwd_port
92 self._vm_info['port'] = fwd_port
94 def qemu_set_serial_port(self, port):
95 """Set serial console port.
97 :param port: Serial console port.
100 self._qemu_opt['serial_port'] = port
102 def qemu_set_mem_size(self, mem_size):
103 """Set virtual RAM size.
105 :param mem_size: RAM size in Mega Bytes.
108 self._qemu_opt['mem_size'] = int(mem_size)
110 def qemu_set_huge_mnt(self, huge_mnt):
111 """Set hugefile mount point.
113 :param huge_mnt: System hugefile mount point.
116 self._qemu_opt['huge_mnt'] = huge_mnt
118 def qemu_set_huge_allocate(self):
119 """Set flag to allocate more huge pages if needed."""
120 self._qemu_opt['huge_allocate'] = True
122 def qemu_set_disk_image(self, disk_image):
125 :param disk_image: Path of the disk image.
126 :type disk_image: str
128 self._qemu_opt['disk_image'] = disk_image
130 def qemu_set_affinity(self, *host_cpus):
131 """Set qemu affinity by getting thread PIDs via QMP and taskset to list
134 :param host_cpus: List of CPU cores.
135 :type host_cpus: list
137 qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
139 if len(qemu_cpus) != len(host_cpus):
140 logger.debug('Host CPU count {0}, Qemu Thread count {1}'.format(
141 len(host_cpus), len(qemu_cpus)))
142 raise ValueError('Host CPU count must match Qemu Thread count')
144 for qemu_cpu, host_cpu in zip(qemu_cpus, host_cpus):
145 cmd = 'taskset -pc {0} {1}'.format(host_cpu, qemu_cpu['thread_id'])
146 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
147 if int(ret_code) != 0:
148 logger.debug('Set affinity failed {0}'.format(stderr))
149 raise RuntimeError('Set affinity failed on {0}'.format(
152 def qemu_set_scheduler_policy(self):
153 """Set scheduler policy to SCHED_RR with priority 1 for all Qemu CPU
156 :raises RuntimeError: Set scheduler policy failed.
158 qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
160 for qemu_cpu in qemu_cpus:
161 cmd = 'chrt -r -p 1 {0}'.format(qemu_cpu['thread_id'])
162 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
163 if int(ret_code) != 0:
164 logger.debug('Set SCHED_RR failed {0}'.format(stderr))
165 raise RuntimeError('Set SCHED_RR failed on {0}'.format(
168 def qemu_set_node(self, node):
169 """Set node to run QEMU on.
171 :param node: Node to run QEMU on.
176 self._ssh.connect(node)
177 self._vm_info['host'] = node['host']
179 def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
180 """Add Vhost-user interface.
182 :param socket: Path of the unix socket.
183 :param server: If True the socket shall be a listening socket.
184 :param mac: Vhost-user interface MAC address (optional, otherwise is
185 used autogenerated MAC 52:54:00:00:04:xx).
191 # Create unix socket character device.
192 chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
196 self._qemu_opt['options'] += chardev
197 # Create Vhost-user network backend.
198 netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0},'\
199 'queues={1}'.format(self._vhost_id, self._qemu_opt['queues'])
200 self._qemu_opt['options'] += netdev
201 # If MAC is not specified use autogenerated 52:54:00:00:04:<vhost_id>
202 # e.g. vhost1 MAC is 52:54:00:00:04:01
204 mac = '52:54:00:00:04:{0:02x}'.format(self._vhost_id)
205 extend_options = 'mq=on,csum=off,gso=off,guest_tso4=off,'\
206 'guest_tso6=off,guest_ecn=off,mrg_rxbuf=off'
207 # Create Virtio network device.
208 device = ' -device virtio-net-pci,netdev=vhost{0},mac={1},{2}'.format(
209 self._vhost_id, mac, extend_options)
210 self._qemu_opt['options'] += device
211 # Add interface MAC and socket to the node dict
212 if_data = {'mac_address': mac, 'socket': socket}
213 if_name = 'vhost{}'.format(self._vhost_id)
214 self._vm_info['interfaces'][if_name] = if_data
215 # Add socket to the socket list
216 self._socks.append(socket)
218 def _qemu_qmp_exec(self, cmd):
219 """Execute QMP command.
221 QMP is JSON based protocol which allows to control QEMU instance.
223 :param cmd: QMP command to execute.
225 :return: Command output in python representation of JSON format. The
226 { "return": {} } response is QMP's success response. An error
227 response will contain the "error" keyword instead of "return".
229 # To enter command mode, the qmp_capabilities command must be issued.
230 qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' \
231 '{ \\"execute\\": \\"' + cmd + \
232 '\\" }" | sudo -S socat - UNIX-CONNECT:' + self.__QMP_SOCK
234 (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
235 if int(ret_code) != 0:
236 logger.debug('QMP execute failed {0}'.format(stderr))
237 raise RuntimeError('QMP execute "{0}"'
238 ' failed on {1}'.format(cmd, self._node['host']))
240 # Skip capabilities negotiation messages.
241 out_list = stdout.splitlines()
242 if len(out_list) < 3:
243 raise RuntimeError('Invalid QMP output on {0}'.format(
245 return json.loads(out_list[2])
247 def _qemu_qga_flush(self):
248 """Flush the QGA parser state
250 qga_cmd = '(printf "\xFF"; sleep 1) | sudo -S socat - UNIX-CONNECT:' + \
252 #TODO: probably need something else
253 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
254 if int(ret_code) != 0:
255 logger.debug('QGA execute failed {0}'.format(stderr))
256 raise RuntimeError('QGA execute "{0}" '
257 'failed on {1}'.format(qga_cmd,
262 return json.loads(stdout.split('\n', 1)[0])
264 def _qemu_qga_exec(self, cmd):
265 """Execute QGA command.
267 QGA provide access to a system-level agent via standard QMP commands.
269 :param cmd: QGA command to execute.
272 qga_cmd = '(echo "{ \\"execute\\": \\"' + \
274 '\\" }"; sleep 1) | sudo -S socat - UNIX-CONNECT:' + \
276 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
277 if int(ret_code) != 0:
278 logger.debug('QGA execute failed {0}'.format(stderr))
279 raise RuntimeError('QGA execute "{0}"'
280 ' failed on {1}'.format(cmd, self._node['host']))
284 return json.loads(stdout.split('\n', 1)[0])
286 def _wait_until_vm_boot(self, timeout=60):
287 """Wait until QEMU VM is booted.
289 Ping QEMU guest agent each 5s until VM booted or timeout.
291 :param timeout: Waiting timeout in seconds (optional, default 60s).
296 if time() - start > timeout:
297 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
298 self._qemu_opt['disk_image'], self._node['host']))
300 self._qemu_qga_flush()
301 out = self._qemu_qga_exec('guest-ping')
303 logger.trace('QGA guest-ping unexpected output {}'.format(out))
304 # Empty output - VM not booted yet
307 # Non-error return - VM booted
308 elif out.get('return') is not None:
310 # Skip error and wait
311 elif out.get('error') is not None:
314 # If there is an unexpected output from QGA guest-info, try
315 # again until timeout.
316 logger.trace('QGA guest-ping unexpected output {}'.format(out))
318 logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
321 def _update_vm_interfaces(self):
322 """Update interface names in VM node dict."""
323 # Send guest-network-get-interfaces command via QGA, output example:
324 # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
325 # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
326 out = self._qemu_qga_exec('guest-network-get-interfaces')
327 interfaces = out.get('return')
330 raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
331 self._qemu_opt['disk_image'], self._node['host']))
332 # Create MAC-name dict
333 for interface in interfaces:
334 if 'hardware-address' not in interface:
336 mac_name[interface['hardware-address']] = interface['name']
337 # Match interface by MAC and save interface name
338 for interface in self._vm_info['interfaces'].values():
339 mac = interface.get('mac_address')
340 if_name = mac_name.get(mac)
342 logger.trace('Interface name for MAC {} not found'.format(mac))
344 interface['name'] = if_name
346 def _huge_page_check(self, allocate=False):
347 """Huge page check."""
348 huge_mnt = self._qemu_opt.get('huge_mnt')
349 mem_size = self._qemu_opt.get('mem_size')
351 # Get huge pages information
352 huge_size = self._get_huge_page_size()
353 huge_free = self._get_huge_page_free(huge_size)
354 huge_total = self._get_huge_page_total(huge_size)
356 # Check if memory reqested by qemu is available on host
357 if (mem_size * 1024) > (huge_free * huge_size):
358 # If we want to allocate hugepage dynamically
360 mem_needed = abs((huge_free * huge_size) - (mem_size * 1024))
361 huge_to_allocate = ((mem_needed / huge_size) * 2) + huge_total
362 max_map_count = huge_to_allocate*4
363 # Increase maximum number of memory map areas a process may have
364 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/max_map_count'.format(
366 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
367 # Increase hugepage count
368 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/nr_hugepages'.format(
370 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
371 if int(ret_code) != 0:
372 logger.debug('Mount huge pages failed {0}'.format(stderr))
373 raise RuntimeError('Mount huge pages failed on {0}'.format(
375 # If we do not want to allocate dynamicaly end with error
378 'Not enough free huge pages: {0}, '
379 '{1} MB'.format(huge_free, huge_free * huge_size)
381 # Check if huge pages mount point exist
383 (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
384 for line in output.splitlines():
385 # Try to find something like:
386 # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
388 if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
391 # If huge page mount point not exist create one
393 cmd = 'mkdir -p {0}'.format(huge_mnt)
394 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
395 if int(ret_code) != 0:
396 logger.debug('Create mount dir failed: {0}'.format(stderr))
397 raise RuntimeError('Create mount dir failed on {0}'.format(
399 cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
401 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
402 if int(ret_code) != 0:
403 logger.debug('Mount huge pages failed {0}'.format(stderr))
404 raise RuntimeError('Mount huge pages failed on {0}'.format(
407 def _get_huge_page_size(self):
408 """Get default size of huge pages in system.
410 :returns: Default size of free huge pages in system.
412 :raises: RuntimeError if reading failed for three times.
414 # TODO: remove to dedicated library
415 cmd_huge_size = "grep Hugepagesize /proc/meminfo | awk '{ print $2 }'"
417 (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_size)
422 logger.trace('Reading huge page size information failed')
426 raise RuntimeError('Getting huge page size information failed.')
429 def _get_huge_page_free(self, huge_size):
430 """Get total number of huge pages in system.
432 :param huge_size: Size of hugepages.
434 :returns: Number of free huge pages in system.
436 :raises: RuntimeError if reading failed for three times.
438 # TODO: add numa aware option
439 # TODO: remove to dedicated library
440 cmd_huge_free = 'cat /sys/kernel/mm/hugepages/hugepages-{0}kB/'\
441 'free_hugepages'.format(huge_size)
443 (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_free)
448 logger.trace('Reading free huge pages information failed')
452 raise RuntimeError('Getting free huge pages information failed.')
455 def _get_huge_page_total(self, huge_size):
456 """Get total number of huge pages in system.
458 :param huge_size: Size of hugepages.
460 :returns: Total number of huge pages in system.
462 :raises: RuntimeError if reading failed for three times.
464 # TODO: add numa aware option
465 # TODO: remove to dedicated library
466 cmd_huge_total = 'cat /sys/kernel/mm/hugepages/hugepages-{0}kB/'\
467 'nr_hugepages'.format(huge_size)
469 (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_total)
472 huge_total = int(out)
474 logger.trace('Reading total huge pages information failed')
478 raise RuntimeError('Getting total huge pages information failed.')
481 def qemu_start(self):
482 """Start QEMU and wait until VM boot.
484 :return: VM node info.
486 .. note:: First set at least node to run QEMU on.
487 .. warning:: Starts only one VM on the node.
490 ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
491 self._qemu_opt.get('ssh_fwd_port'))
492 # Memory and huge pages
493 mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
494 'share=on -m {0} -numa node,memdev=mem'.format(
495 self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
497 # By default check only if hugepages are availbale.
498 # If 'huge_allocate' is set to true try to allocate as well.
499 self._huge_page_check(allocate=self._qemu_opt.get('huge_allocate'))
502 drive = '-drive file={},format=raw,cache=none,if=virtio'.format(
503 self._qemu_opt.get('disk_image'))
504 # Setup QMP via unix socket
505 qmp = '-qmp unix:{0},server,nowait'.format(self.__QMP_SOCK)
506 # Setup serial console
507 serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \
508 'nowait -device isa-serial,chardev=gnc0'.format(
509 self._qemu_opt.get('serial_port'))
510 # Setup QGA via chardev (unix socket) and isa-serial channel
511 qga = '-chardev socket,path=/tmp/qga.sock,server,nowait,id=qga0 ' \
512 '-device isa-serial,chardev=qga0'
514 graphic = '-monitor none -display none -vga none'
517 cmd = '{0} {1} {2} {3} {4} {5} {6} {7} {8} {9}'.format(
518 self.__QEMU_BIN, self._qemu_opt.get('smp'), mem, ssh_fwd,
519 self._qemu_opt.get('options'),
520 drive, qmp, serial, qga, graphic)
521 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300)
522 if int(ret_code) != 0:
523 logger.debug('QEMU start failed {0}'.format(stderr))
524 raise RuntimeError('QEMU start failed on {0}'.format(
526 logger.trace('QEMU running')
529 self._wait_until_vm_boot()
530 except (RuntimeError, SSHTimeout):
532 self.qemu_clear_socks()
534 # Update interface names in VM node dict
535 self._update_vm_interfaces()
536 # Return VM node dict
540 """Quit the QEMU emulator."""
541 out = self._qemu_qmp_exec('quit')
542 err = out.get('error')
544 raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
545 self._node['host'], json.dumps(err)))
547 def qemu_system_powerdown(self):
548 """Power down the system (if supported)."""
549 out = self._qemu_qmp_exec('system_powerdown')
550 err = out.get('error')
553 'QEMU system powerdown failed on {0}, '
554 'error: {1}'.format(self._node['host'], json.dumps(err))
557 def qemu_system_reset(self):
558 """Reset the system."""
559 out = self._qemu_qmp_exec('system_reset')
560 err = out.get('error')
563 'QEMU system reset failed on {0}, '
564 'error: {1}'.format(self._node['host'], json.dumps(err)))
567 """Kill qemu process."""
568 # TODO: add PID storage so that we can kill specific PID
569 # Note: in QEMU start phase there are 3 QEMU processes because we
571 self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
573 def qemu_clear_socks(self):
574 """Remove all sockets created by QEMU."""
575 # If serial console port still open kill process
576 cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port'))
577 self._ssh.exec_command_sudo(cmd)
578 # Delete all created sockets
579 for sock in self._socks:
580 cmd = 'rm -f {}'.format(sock)
581 self._ssh.exec_command_sudo(cmd)
583 def qemu_system_status(self):
584 """Return current VM status.
586 VM should be in following status:
588 - debug: QEMU running on a debugger
589 - finish-migrate: paused to finish the migration process
590 - inmigrate: waiting for an incoming migration
591 - internal-error: internal error has occurred
592 - io-error: the last IOP has failed
594 - postmigrate: paused following a successful migrate
595 - prelaunch: QEMU was started with -S and guest has not started
596 - restore-vm: paused to restore VM state
597 - running: actively running
598 - save-vm: paused to save the VM state
599 - shutdown: shut down (and -no-shutdown is in use)
600 - suspended: suspended (ACPI S3)
601 - watchdog: watchdog action has been triggered
602 - guest-panicked: panicked as a result of guest OS panic
607 out = self._qemu_qmp_exec('query-status')
608 ret = out.get('return')
610 return ret.get('status')
612 err = out.get('error')
614 'QEMU query-status failed on {0}, '
615 'error: {1}'.format(self._node['host'], json.dumps(err)))
618 def build_qemu(node):
619 """Build QEMU from sources.
621 :param node: Node to build QEMU on.
627 (ret_code, stdout, stderr) = \
628 ssh.exec_command('sudo -Sn bash {0}/{1}/qemu_build.sh'.format(
629 Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH), 1000)
631 if int(ret_code) != 0:
632 logger.debug('QEMU build failed {0}'.format(stderr))
633 raise RuntimeError('QEMU build failed on {0}'.format(node['host']))