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 do not allocate huge pages.
53 self._qemu_opt['huge_allocate'] = False
54 # Default image for CSIT virl setup
55 self._qemu_opt['disk_image'] = '/var/lib/vm/vhost-nested.img'
56 # Affinity of qemu processes
57 self._qemu_opt['affinity'] = False
69 self._socks = [self.__QMP_SOCK, self.__QGA_SOCK]
71 def qemu_set_smp(self, cpus, cores, threads, sockets):
72 """Set SMP option for QEMU
74 :param cpus: Number of CPUs.
75 :param cores: Number of CPU cores on one socket.
76 :param threads: Number of threads on one CPU core.
77 :param sockets: Number of discrete sockets in the system.
83 self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
84 cpus, cores, threads, sockets)
86 def qemu_set_ssh_fwd_port(self, fwd_port):
87 """Set host port for guest SSH forwarding.
89 :param fwd_port: Port number on host for guest SSH forwarding.
92 self._qemu_opt['ssh_fwd_port'] = fwd_port
93 self._vm_info['port'] = fwd_port
95 def qemu_set_serial_port(self, port):
96 """Set serial console port.
98 :param port: Serial console port.
101 self._qemu_opt['serial_port'] = port
103 def qemu_set_mem_size(self, mem_size):
104 """Set virtual RAM size.
106 :param mem_size: RAM size in Mega Bytes.
109 self._qemu_opt['mem_size'] = int(mem_size)
111 def qemu_set_huge_mnt(self, huge_mnt):
112 """Set hugefile mount point.
114 :param huge_mnt: System hugefile mount point.
117 self._qemu_opt['huge_mnt'] = huge_mnt
119 def qemu_set_huge_allocate(self):
120 """Set flag to allocate more huge pages if needed."""
121 self._qemu_opt['huge_allocate'] = True
123 def qemu_set_disk_image(self, disk_image):
126 :param disk_image: Path of the disk image.
127 :type disk_image: str
129 self._qemu_opt['disk_image'] = disk_image
131 def qemu_set_affinity(self, mask):
132 """Set qemu affinity by taskset with cpu mask.
134 :param mask: Hex CPU mask.
137 self._qemu_opt['affinity'] = mask
139 def qemu_set_node(self, node):
140 """Set node to run QEMU on.
142 :param node: Node to run QEMU on.
147 self._ssh.connect(node)
148 self._vm_info['host'] = node['host']
150 def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
151 """Add Vhost-user interface.
153 :param socket: Path of the unix socket.
154 :param server: If True the socket shall be a listening socket.
155 :param mac: Vhost-user interface MAC address (optional, otherwise is
156 used autogenerated MAC 52:54:00:00:04:xx).
162 # Create unix socket character device.
163 chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
167 self._qemu_opt['options'] += chardev
168 # Create Vhost-user network backend.
169 netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0}'.format(
171 self._qemu_opt['options'] += netdev
172 # If MAC is not specified use autogenerated 52:54:00:00:04:<vhost_id>
173 # e.g. vhost1 MAC is 52:54:00:00:04:01
175 mac = '52:54:00:00:04:{0:02x}'.format(self._vhost_id)
176 extend_options = 'csum=off,gso=off,guest_tso4=off,guest_tso6=off,'\
177 'guest_ecn=off,mrg_rxbuf=off'
178 # Create Virtio network device.
179 device = ' -device virtio-net-pci,netdev=vhost{0},mac={1},{2}'.format(
180 self._vhost_id, mac, extend_options)
181 self._qemu_opt['options'] += device
182 # Add interface MAC and socket to the node dict
183 if_data = {'mac_address': mac, 'socket': socket}
184 if_name = 'vhost{}'.format(self._vhost_id)
185 self._vm_info['interfaces'][if_name] = if_data
186 # Add socket to the socket list
187 self._socks.append(socket)
189 def _qemu_qmp_exec(self, cmd):
190 """Execute QMP command.
192 QMP is JSON based protocol which allows to control QEMU instance.
194 :param cmd: QMP command to execute.
196 :return: Command output in python representation of JSON format. The
197 { "return": {} } response is QMP's success response. An error
198 response will contain the "error" keyword instead of "return".
200 # To enter command mode, the qmp_capabilities command must be issued.
201 qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' + \
202 '{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc -U ' + \
204 (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
205 if 0 != int(ret_code):
206 logger.debug('QMP execute failed {0}'.format(stderr))
207 raise RuntimeError('QMP execute "{0}" failed on {1}'.format(cmd,
210 # Skip capabilities negotiation messages.
211 out_list = stdout.splitlines()
212 if len(out_list) < 3:
213 raise RuntimeError('Invalid QMP output on {0}'.format(
215 return json.loads(out_list[2])
217 def _qemu_qga_flush(self):
218 """Flush the QGA parser state
220 qga_cmd = 'printf "\xFF" | sudo -S nc ' \
221 '-q 1 -U ' + self.__QGA_SOCK
222 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
223 if 0 != int(ret_code):
224 logger.debug('QGA execute failed {0}'.format(stderr))
225 raise RuntimeError('QGA execute "{0}" failed on {1}'.format(cmd,
230 return json.loads(stdout.split('\n', 1)[0])
232 def _qemu_qga_exec(self, cmd):
233 """Execute QGA command.
235 QGA provide access to a system-level agent via standard QMP commands.
237 :param cmd: QGA command to execute.
240 qga_cmd = 'echo "{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc ' \
241 '-q 1 -U ' + self.__QGA_SOCK
242 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
243 if 0 != int(ret_code):
244 logger.debug('QGA execute failed {0}'.format(stderr))
245 raise RuntimeError('QGA execute "{0}" failed on {1}'.format(cmd,
250 return json.loads(stdout.split('\n', 1)[0])
252 def _wait_until_vm_boot(self, timeout=300):
253 """Wait until QEMU VM is booted.
255 Ping QEMU guest agent each 5s until VM booted or timeout.
257 :param timeout: Waiting timeout in seconds (optional, default 300s).
262 if time() - start > timeout:
263 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
264 self._qemu_opt['disk_image'], self._node['host']))
265 self._qemu_qga_flush()
266 out = self._qemu_qga_exec('guest-ping')
267 # Empty output - VM not booted yet
270 # Non-error return - VM booted
271 elif out.get('return') is not None:
273 # Skip error and wait
274 elif out.get('error') is not None:
277 raise RuntimeError('QGA guest-ping unexpected output {}'.format(
279 logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
282 def _update_vm_interfaces(self):
283 """Update interface names in VM node dict."""
284 # Send guest-network-get-interfaces command via QGA, output example:
285 # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
286 # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
287 out = self._qemu_qga_exec('guest-network-get-interfaces')
288 interfaces = out.get('return')
291 raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
292 self._qemu_opt['disk_image'], self._node['host']))
293 # Create MAC-name dict
294 for interface in interfaces:
295 if 'hardware-address' not in interface:
297 mac_name[interface['hardware-address']] = interface['name']
298 # Match interface by MAC and save interface name
299 for interface in self._vm_info['interfaces'].values():
300 mac = interface.get('mac_address')
301 if_name = mac_name.get(mac)
303 logger.trace('Interface name for MAC {} not found'.format(mac))
305 interface['name'] = if_name
307 def _huge_page_check(self):
308 """Huge page check."""
309 huge_mnt = self._qemu_opt.get('huge_mnt')
310 mem_size = self._qemu_opt.get('mem_size')
311 # Check size of free huge pages
312 (_, output, _) = self._ssh.exec_command('grep Huge /proc/meminfo')
313 regex = re.compile(r'HugePages_Free:\s+(\d+)')
314 match = regex.search(output)
315 huge_free = int(match.group(1))
316 regex = re.compile(r'Hugepagesize:\s+(\d+)')
317 match = regex.search(output)
318 huge_size = int(match.group(1))
319 if (mem_size * 1024) > (huge_free * huge_size):
320 raise RuntimeError('Not enough free huge pages {0} kB, required '
321 '{1} MB'.format(huge_free * huge_size, mem_size))
322 # Check if huge pages mount point exist
324 (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
325 for line in output.splitlines():
326 # Try to find something like:
327 # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
329 if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
332 # If huge page mount point not exist create one
334 cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
336 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
337 if int(ret_code) != 0:
338 logger.debug('Mount huge pages failed {0}'.format(stderr))
339 raise RuntimeError('Mount huge pages failed on {0}'.format(
342 def _huge_page_allocate(self):
343 """Huge page allocate."""
344 huge_mnt = self._qemu_opt.get('huge_mnt')
345 mem_size = self._qemu_opt.get('mem_size')
346 # Check size of free huge pages
347 (_, output, _) = self._ssh.exec_command('grep Huge /proc/meminfo')
348 regex = re.compile(r'HugePages_Free:\s+(\d+)')
349 match = regex.search(output)
350 huge_free = int(match.group(1))
351 regex = re.compile(r'HugePages_Total:\s+(\d+)')
352 match = regex.search(output)
353 huge_total = int(match.group(1))
354 regex = re.compile(r'Hugepagesize:\s+(\d+)')
355 match = regex.search(output)
356 huge_size = int(match.group(1))
358 mem_needed = abs((huge_free * huge_size) - (mem_size * 1024))
361 huge_to_allocate = (mem_needed / huge_size) + huge_total
362 # Increase limit of allowed max hugepage count
363 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/max_map_count'.format(
365 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
366 # Increase hugepage count
367 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/nr_hugepages'.format(
369 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
370 if int(ret_code) != 0:
371 logger.debug('Mount huge pages failed {0}'.format(stderr))
372 raise RuntimeError('Mount huge pages failed on {0}'.format(
375 def qemu_start(self):
376 """Start QEMU and wait until VM boot.
378 :return: VM node info.
380 .. note:: First set at least node to run QEMU on.
381 .. warning:: Starts only one VM on the node.
384 ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
385 self._qemu_opt.get('ssh_fwd_port'))
386 # Memory and huge pages
387 mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
388 'share=on -m {0} -numa node,memdev=mem'.format(
389 self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
391 # By default check only if hugepages are availbale.
392 # If 'huge_allocate' is set to true try to allocate as well.
394 self._huge_page_check()
395 except RuntimeError as runtime_error:
396 if self._qemu_opt.get('huge_allocate'):
397 self._huge_page_allocate()
401 # Setup QMP via unix socket
402 qmp = '-qmp unix:{0},server,nowait'.format(self.__QMP_SOCK)
403 # Setup serial console
404 serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \
405 'nowait -device isa-serial,chardev=gnc0'.format(
406 self._qemu_opt.get('serial_port'))
407 # Setup QGA via chardev (unix socket) and isa-serial channel
408 qga = '-chardev socket,path=/tmp/qga.sock,server,nowait,id=qga0 ' \
409 '-device isa-serial,chardev=qga0'
411 graphic = '-monitor none -display none -vga none'
412 qbin = 'taskset {0} {1}'.format(self._qemu_opt.get('affinity'),
413 self.__QEMU_BIN) if self._qemu_opt.get(
414 'affinity') else self.__QEMU_BIN
416 cmd = '{0} {1} {2} {3} {4} -hda {5} {6} {7} {8} {9}'.format(
417 qbin, self._qemu_opt.get('smp'), mem, ssh_fwd,
418 self._qemu_opt.get('options'),
419 self._qemu_opt.get('disk_image'), qmp, serial, qga, graphic)
420 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300)
421 if int(ret_code) != 0:
422 logger.debug('QEMU start failed {0}'.format(stderr))
423 raise RuntimeError('QEMU start failed on {0}'.format(
425 logger.trace('QEMU running')
427 self._wait_until_vm_boot()
428 # Update interface names in VM node dict
429 self._update_vm_interfaces()
430 # Return VM node dict
434 """Quit the QEMU emulator."""
435 out = self._qemu_qmp_exec('quit')
436 err = out.get('error')
438 raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
439 self._node['host'], json.dumps(err)))
441 def qemu_system_powerdown(self):
442 """Power down the system (if supported)."""
443 out = self._qemu_qmp_exec('system_powerdown')
444 err = out.get('error')
446 raise RuntimeError('QEMU system powerdown failed on {0}, '
447 'error: {1}'.format(self._node['host'], json.dumps(err)))
449 def qemu_system_reset(self):
450 """Reset the system."""
451 out = self._qemu_qmp_exec('system_reset')
452 err = out.get('error')
454 raise RuntimeError('QEMU system reset failed on {0}, '
455 'error: {1}'.format(self._node['host'], json.dumps(err)))
458 """Kill qemu process."""
459 # TODO: add PID storage so that we can kill specific PID
460 # Note: in QEMU start phase there are 3 QEMU processes because we
462 self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
464 def qemu_clear_socks(self):
465 """Remove all sockets created by QEMU."""
466 # If serial console port still open kill process
467 cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port'))
468 self._ssh.exec_command_sudo(cmd)
469 # Delete all created sockets
470 for sock in self._socks:
471 cmd = 'rm -f {}'.format(sock)
472 self._ssh.exec_command_sudo(cmd)
474 def qemu_system_status(self):
475 """Return current VM status.
477 VM should be in following status:
479 - debug: QEMU running on a debugger
480 - finish-migrate: paused to finish the migration process
481 - inmigrate: waiting for an incoming migration
482 - internal-error: internal error has occurred
483 - io-error: the last IOP has failed
485 - postmigrate: paused following a successful migrate
486 - prelaunch: QEMU was started with -S and guest has not started
487 - restore-vm: paused to restore VM state
488 - running: actively running
489 - save-vm: paused to save the VM state
490 - shutdown: shut down (and -no-shutdown is in use)
491 - suspended: suspended (ACPI S3)
492 - watchdog: watchdog action has been triggered
493 - guest-panicked: panicked as a result of guest OS panic
498 out = self._qemu_qmp_exec('query-status')
499 ret = out.get('return')
501 return ret.get('status')
503 err = out.get('error')
504 raise RuntimeError('QEMU query-status failed on {0}, '
505 'error: {1}'.format(self._node['host'], json.dumps(err)))
508 def build_qemu(node):
509 """Build QEMU from sources.
511 :param node: Node to build QEMU on.
517 (ret_code, stdout, stderr) = \
518 ssh.exec_command('sudo -Sn bash {0}/{1}/qemu_build.sh'.format(
519 Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH), 1000)
521 if 0 != int(ret_code):
522 logger.debug('QEMU build failed {0}'.format(stderr))
523 raise RuntimeError('QEMU build failed on {0}'.format(node['host']))