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
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-1.0,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'
66 self._socks = [self.__QMP_SOCK, self.__QGA_SOCK]
68 def qemu_set_smp(self, cpus, cores, threads, sockets):
69 """Set SMP option for QEMU
71 :param cpus: Number of CPUs.
72 :param cores: Number of CPU cores on one socket.
73 :param threads: Number of threads on one CPU core.
74 :param sockets: Number of discrete sockets in the system.
80 self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
81 cpus, cores, threads, sockets)
83 def qemu_set_ssh_fwd_port(self, fwd_port):
84 """Set host port for guest SSH forwarding.
86 :param fwd_port: Port number on host for guest SSH forwarding.
89 self._qemu_opt['ssh_fwd_port'] = fwd_port
90 self._vm_info['port'] = fwd_port
92 def qemu_set_serial_port(self, port):
93 """Set serial console port.
95 :param port: Serial console port.
98 self._qemu_opt['serial_port'] = port
100 def qemu_set_mem_size(self, mem_size):
101 """Set virtual RAM size.
103 :param mem_size: RAM size in Mega Bytes.
106 self._qemu_opt['mem_size'] = int(mem_size)
108 def qemu_set_huge_mnt(self, huge_mnt):
109 """Set hugefile mount point.
111 :param huge_mnt: System hugefile mount point.
114 self._qemu_opt['huge_mnt'] = huge_mnt
116 def qemu_set_huge_allocate(self):
117 """Set flag to allocate more huge pages if needed."""
118 self._qemu_opt['huge_allocate'] = True
120 def qemu_set_disk_image(self, disk_image):
123 :param disk_image: Path of the disk image.
124 :type disk_image: str
126 self._qemu_opt['disk_image'] = disk_image
128 def qemu_set_affinity(self, *host_cpus):
129 """Set qemu affinity by getting thread PIDs via QMP and taskset to list
132 :param host_cpus: List of CPU cores.
133 :type host_cpus: list
135 qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
137 if len(qemu_cpus) != len(host_cpus):
138 logger.debug('Host CPU count {0}, Qemu Thread count {1}'.format(
139 len(host_cpus), len(qemu_cpus)))
140 raise ValueError('Host CPU count must match Qemu Thread count')
142 for qemu_cpu, host_cpu in zip(qemu_cpus, host_cpus):
143 cmd = 'taskset -p {0} {1}'.format(hex(1 << int(host_cpu)),
144 qemu_cpu['thread_id'])
145 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
146 if int(ret_code) != 0:
147 logger.debug('Set affinity failed {0}'.format(stderr))
148 raise RuntimeError('Set affinity failed on {0}'.format(
151 def qemu_set_scheduler_policy(self):
152 """Set scheduler policy to SCHED_RR with priority 1 for all Qemu CPU
155 :raises RuntimeError: Set scheduler policy failed.
157 qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
159 for qemu_cpu in qemu_cpus:
160 cmd = 'chrt -r -p 1 {0}'.format(qemu_cpu['thread_id'])
161 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
162 if int(ret_code) != 0:
163 logger.debug('Set SCHED_RR failed {0}'.format(stderr))
164 raise RuntimeError('Set SCHED_RR failed on {0}'.format(
167 def qemu_set_node(self, node):
168 """Set node to run QEMU on.
170 :param node: Node to run QEMU on.
175 self._ssh.connect(node)
176 self._vm_info['host'] = node['host']
178 def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
179 """Add Vhost-user interface.
181 :param socket: Path of the unix socket.
182 :param server: If True the socket shall be a listening socket.
183 :param mac: Vhost-user interface MAC address (optional, otherwise is
184 used autogenerated MAC 52:54:00:00:04:xx).
190 # Create unix socket character device.
191 chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
195 self._qemu_opt['options'] += chardev
196 # Create Vhost-user network backend.
197 netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0}'.format(
199 self._qemu_opt['options'] += netdev
200 # If MAC is not specified use autogenerated 52:54:00:00:04:<vhost_id>
201 # e.g. vhost1 MAC is 52:54:00:00:04:01
203 mac = '52:54:00:00:04:{0:02x}'.format(self._vhost_id)
204 extend_options = 'csum=off,gso=off,guest_tso4=off,guest_tso6=off,'\
205 'guest_ecn=off,mrg_rxbuf=off'
206 # Create Virtio network device.
207 device = ' -device virtio-net-pci,netdev=vhost{0},mac={1},{2}'.format(
208 self._vhost_id, mac, extend_options)
209 self._qemu_opt['options'] += device
210 # Add interface MAC and socket to the node dict
211 if_data = {'mac_address': mac, 'socket': socket}
212 if_name = 'vhost{}'.format(self._vhost_id)
213 self._vm_info['interfaces'][if_name] = if_data
214 # Add socket to the socket list
215 self._socks.append(socket)
217 def _qemu_qmp_exec(self, cmd):
218 """Execute QMP command.
220 QMP is JSON based protocol which allows to control QEMU instance.
222 :param cmd: QMP command to execute.
224 :return: Command output in python representation of JSON format. The
225 { "return": {} } response is QMP's success response. An error
226 response will contain the "error" keyword instead of "return".
228 # To enter command mode, the qmp_capabilities command must be issued.
229 qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' + \
230 '{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc -U ' + \
232 (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
233 if int(ret_code) != 0:
234 logger.debug('QMP execute failed {0}'.format(stderr))
235 raise RuntimeError('QMP execute "{0}"'
236 ' failed on {1}'.format(cmd, self._node['host']))
238 # Skip capabilities negotiation messages.
239 out_list = stdout.splitlines()
240 if len(out_list) < 3:
241 raise RuntimeError('Invalid QMP output on {0}'.format(
243 return json.loads(out_list[2])
245 def _qemu_qga_flush(self):
246 """Flush the QGA parser state
248 qga_cmd = 'printf "\xFF" | sudo -S nc ' \
249 '-q 1 -U ' + self.__QGA_SOCK
250 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
251 if int(ret_code) != 0:
252 logger.debug('QGA execute failed {0}'.format(stderr))
253 raise RuntimeError('QGA execute "{0}" '
254 'failed on {1}'.format(qga_cmd,
259 return json.loads(stdout.split('\n', 1)[0])
261 def _qemu_qga_exec(self, cmd):
262 """Execute QGA command.
264 QGA provide access to a system-level agent via standard QMP commands.
266 :param cmd: QGA command to execute.
269 qga_cmd = 'echo "{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc ' \
270 '-q 1 -U ' + self.__QGA_SOCK
271 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
272 if int(ret_code) != 0:
273 logger.debug('QGA execute failed {0}'.format(stderr))
274 raise RuntimeError('QGA execute "{0}"'
275 ' failed on {1}'.format(cmd, self._node['host']))
279 return json.loads(stdout.split('\n', 1)[0])
281 def _wait_until_vm_boot(self, timeout=300):
282 """Wait until QEMU VM is booted.
284 Ping QEMU guest agent each 5s until VM booted or timeout.
286 :param timeout: Waiting timeout in seconds (optional, default 300s).
291 if time() - start > timeout:
292 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
293 self._qemu_opt['disk_image'], self._node['host']))
294 self._qemu_qga_flush()
295 out = self._qemu_qga_exec('guest-ping')
296 # Empty output - VM not booted yet
299 # Non-error return - VM booted
300 elif out.get('return') is not None:
302 # Skip error and wait
303 elif out.get('error') is not None:
306 raise RuntimeError('QGA guest-ping unexpected output {}'.format(
308 logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
311 def _update_vm_interfaces(self):
312 """Update interface names in VM node dict."""
313 # Send guest-network-get-interfaces command via QGA, output example:
314 # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
315 # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
316 out = self._qemu_qga_exec('guest-network-get-interfaces')
317 interfaces = out.get('return')
320 raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
321 self._qemu_opt['disk_image'], self._node['host']))
322 # Create MAC-name dict
323 for interface in interfaces:
324 if 'hardware-address' not in interface:
326 mac_name[interface['hardware-address']] = interface['name']
327 # Match interface by MAC and save interface name
328 for interface in self._vm_info['interfaces'].values():
329 mac = interface.get('mac_address')
330 if_name = mac_name.get(mac)
332 logger.trace('Interface name for MAC {} not found'.format(mac))
334 interface['name'] = if_name
336 def _huge_page_check(self, allocate=False):
337 """Huge page check."""
338 huge_mnt = self._qemu_opt.get('huge_mnt')
339 mem_size = self._qemu_opt.get('mem_size')
341 # Get huge pages information
342 huge_size = self._get_huge_page_size()
343 huge_free = self._get_huge_page_free(huge_size)
344 huge_total = self._get_huge_page_total(huge_size)
346 # Check if memory reqested by qemu is available on host
347 if (mem_size * 1024) > (huge_free * huge_size):
348 # If we want to allocate hugepage dynamically
350 mem_needed = abs((huge_free * huge_size) - (mem_size * 1024))
351 huge_to_allocate = ((mem_needed / huge_size) * 2) + huge_total
352 max_map_count = huge_to_allocate*4
353 # Increase maximum number of memory map areas a process may have
354 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/max_map_count'.format(
356 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
357 # Increase hugepage count
358 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/nr_hugepages'.format(
360 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
361 if int(ret_code) != 0:
362 logger.debug('Mount huge pages failed {0}'.format(stderr))
363 raise RuntimeError('Mount huge pages failed on {0}'.format(
365 # If we do not want to allocate dynamicaly end with error
368 'Not enough free huge pages: {0}, '
369 '{1} MB'.format(huge_free, huge_free * huge_size)
371 # Check if huge pages mount point exist
373 (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
374 for line in output.splitlines():
375 # Try to find something like:
376 # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
378 if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
381 # If huge page mount point not exist create one
383 cmd = 'mkdir -p {0}'.format(huge_mnt)
384 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
385 if int(ret_code) != 0:
386 logger.debug('Create mount dir failed: {0}'.format(stderr))
387 raise RuntimeError('Create mount dir failed on {0}'.format(
389 cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
391 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
392 if int(ret_code) != 0:
393 logger.debug('Mount huge pages failed {0}'.format(stderr))
394 raise RuntimeError('Mount huge pages failed on {0}'.format(
397 def _get_huge_page_size(self):
398 """Get default size of huge pages in system.
400 :returns: Default size of free huge pages in system.
402 :raises: RuntimeError if reading failed for three times.
404 # TODO: remove to dedicated library
405 cmd_huge_size = "grep Hugepagesize /proc/meminfo | awk '{ print $2 }'"
407 (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_size)
412 logger.trace('Reading huge page size information failed')
416 raise RuntimeError('Getting huge page size information failed.')
419 def _get_huge_page_free(self, huge_size):
420 """Get total number of huge pages in system.
422 :param huge_size: Size of hugepages.
424 :returns: Number of free huge pages in system.
426 :raises: RuntimeError if reading failed for three times.
428 # TODO: add numa aware option
429 # TODO: remove to dedicated library
430 cmd_huge_free = 'cat /sys/kernel/mm/hugepages/hugepages-{0}kB/'\
431 'free_hugepages'.format(huge_size)
433 (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_free)
438 logger.trace('Reading free huge pages information failed')
442 raise RuntimeError('Getting free huge pages information failed.')
445 def _get_huge_page_total(self, huge_size):
446 """Get total number of huge pages in system.
448 :param huge_size: Size of hugepages.
450 :returns: Total number of huge pages in system.
452 :raises: RuntimeError if reading failed for three times.
454 # TODO: add numa aware option
455 # TODO: remove to dedicated library
456 cmd_huge_total = 'cat /sys/kernel/mm/hugepages/hugepages-{0}kB/'\
457 'nr_hugepages'.format(huge_size)
459 (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_total)
462 huge_total = int(out)
464 logger.trace('Reading total huge pages information failed')
468 raise RuntimeError('Getting total huge pages information failed.')
471 def qemu_start(self):
472 """Start QEMU and wait until VM boot.
474 :return: VM node info.
476 .. note:: First set at least node to run QEMU on.
477 .. warning:: Starts only one VM on the node.
480 ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
481 self._qemu_opt.get('ssh_fwd_port'))
482 # Memory and huge pages
483 mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
484 'share=on -m {0} -numa node,memdev=mem'.format(
485 self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
487 # By default check only if hugepages are availbale.
488 # If 'huge_allocate' is set to true try to allocate as well.
489 self._huge_page_check(allocate=self._qemu_opt.get('huge_allocate'))
491 # Setup QMP via unix socket
492 qmp = '-qmp unix:{0},server,nowait'.format(self.__QMP_SOCK)
493 # Setup serial console
494 serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \
495 'nowait -device isa-serial,chardev=gnc0'.format(
496 self._qemu_opt.get('serial_port'))
497 # Setup QGA via chardev (unix socket) and isa-serial channel
498 qga = '-chardev socket,path=/tmp/qga.sock,server,nowait,id=qga0 ' \
499 '-device isa-serial,chardev=qga0'
501 graphic = '-monitor none -display none -vga none'
503 cmd = '{0} {1} {2} {3} {4} -hda {5} {6} {7} {8} {9}'.format(
504 self.__QEMU_BIN, self._qemu_opt.get('smp'), mem, ssh_fwd,
505 self._qemu_opt.get('options'),
506 self._qemu_opt.get('disk_image'), qmp, serial, qga, graphic)
507 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300)
508 if int(ret_code) != 0:
509 logger.debug('QEMU start failed {0}'.format(stderr))
510 raise RuntimeError('QEMU start failed on {0}'.format(
512 logger.trace('QEMU running')
514 self._wait_until_vm_boot()
515 # Update interface names in VM node dict
516 self._update_vm_interfaces()
517 # Return VM node dict
521 """Quit the QEMU emulator."""
522 out = self._qemu_qmp_exec('quit')
523 err = out.get('error')
525 raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
526 self._node['host'], json.dumps(err)))
528 def qemu_system_powerdown(self):
529 """Power down the system (if supported)."""
530 out = self._qemu_qmp_exec('system_powerdown')
531 err = out.get('error')
534 'QEMU system powerdown failed on {0}, '
535 'error: {1}'.format(self._node['host'], json.dumps(err))
538 def qemu_system_reset(self):
539 """Reset the system."""
540 out = self._qemu_qmp_exec('system_reset')
541 err = out.get('error')
544 'QEMU system reset failed on {0}, '
545 'error: {1}'.format(self._node['host'], json.dumps(err)))
548 """Kill qemu process."""
549 # TODO: add PID storage so that we can kill specific PID
550 # Note: in QEMU start phase there are 3 QEMU processes because we
552 self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
554 def qemu_clear_socks(self):
555 """Remove all sockets created by QEMU."""
556 # If serial console port still open kill process
557 cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port'))
558 self._ssh.exec_command_sudo(cmd)
559 # Delete all created sockets
560 for sock in self._socks:
561 cmd = 'rm -f {}'.format(sock)
562 self._ssh.exec_command_sudo(cmd)
564 def qemu_system_status(self):
565 """Return current VM status.
567 VM should be in following status:
569 - debug: QEMU running on a debugger
570 - finish-migrate: paused to finish the migration process
571 - inmigrate: waiting for an incoming migration
572 - internal-error: internal error has occurred
573 - io-error: the last IOP has failed
575 - postmigrate: paused following a successful migrate
576 - prelaunch: QEMU was started with -S and guest has not started
577 - restore-vm: paused to restore VM state
578 - running: actively running
579 - save-vm: paused to save the VM state
580 - shutdown: shut down (and -no-shutdown is in use)
581 - suspended: suspended (ACPI S3)
582 - watchdog: watchdog action has been triggered
583 - guest-panicked: panicked as a result of guest OS panic
588 out = self._qemu_qmp_exec('query-status')
589 ret = out.get('return')
591 return ret.get('status')
593 err = out.get('error')
595 'QEMU query-status failed on {0}, '
596 'error: {1}'.format(self._node['host'], json.dumps(err)))
599 def build_qemu(node):
600 """Build QEMU from sources.
602 :param node: Node to build QEMU on.
608 (ret_code, stdout, stderr) = \
609 ssh.exec_command('sudo -Sn bash {0}/{1}/qemu_build.sh'.format(
610 Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH), 1000)
612 if int(ret_code) != 0:
613 logger.debug('QEMU build failed {0}'.format(stderr))
614 raise RuntimeError('QEMU build failed on {0}'.format(node['host']))