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'
67 self._socks = [self.__QMP_SOCK, self.__QGA_SOCK]
69 def qemu_set_smp(self, cpus, cores, threads, sockets):
70 """Set SMP option for QEMU
72 :param cpus: Number of CPUs.
73 :param cores: Number of CPU cores on one socket.
74 :param threads: Number of threads on one CPU core.
75 :param sockets: Number of discrete sockets in the system.
81 self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
82 cpus, cores, threads, sockets)
84 def qemu_set_ssh_fwd_port(self, fwd_port):
85 """Set host port for guest SSH forwarding.
87 :param fwd_port: Port number on host for guest SSH forwarding.
90 self._qemu_opt['ssh_fwd_port'] = fwd_port
91 self._vm_info['port'] = fwd_port
93 def qemu_set_serial_port(self, port):
94 """Set serial console port.
96 :param port: Serial console port.
99 self._qemu_opt['serial_port'] = port
101 def qemu_set_mem_size(self, mem_size):
102 """Set virtual RAM size.
104 :param mem_size: RAM size in Mega Bytes.
107 self._qemu_opt['mem_size'] = int(mem_size)
109 def qemu_set_huge_mnt(self, huge_mnt):
110 """Set hugefile mount point.
112 :param huge_mnt: System hugefile mount point.
115 self._qemu_opt['huge_mnt'] = huge_mnt
117 def qemu_set_huge_allocate(self):
118 """Set flag to allocate more huge pages if needed."""
119 self._qemu_opt['huge_allocate'] = True
121 def qemu_set_disk_image(self, disk_image):
124 :param disk_image: Path of the disk image.
125 :type disk_image: str
127 self._qemu_opt['disk_image'] = disk_image
129 def qemu_set_affinity(self, *host_cpus):
130 """Set qemu affinity by getting thread PIDs via QMP and taskset to list
133 :param host_cpus: List of CPU cores.
134 :type host_cpus: list
136 qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
138 if len(qemu_cpus) != len(host_cpus):
139 logger.debug('Host CPU count {0}, Qemu Thread count {1}'.format(
140 len(host_cpus), len(qemu_cpus)))
141 raise ValueError('Host CPU count must match Qemu Thread count')
143 for qemu_cpu, host_cpu in zip(qemu_cpus, host_cpus):
144 cmd = 'taskset -p {0} {1}'.format(hex(1 << int(host_cpu)),
145 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_node(self, node):
153 """Set node to run QEMU on.
155 :param node: Node to run QEMU on.
160 self._ssh.connect(node)
161 self._vm_info['host'] = node['host']
163 def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
164 """Add Vhost-user interface.
166 :param socket: Path of the unix socket.
167 :param server: If True the socket shall be a listening socket.
168 :param mac: Vhost-user interface MAC address (optional, otherwise is
169 used autogenerated MAC 52:54:00:00:04:xx).
175 # Create unix socket character device.
176 chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
180 self._qemu_opt['options'] += chardev
181 # Create Vhost-user network backend.
182 netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0}'.format(
184 self._qemu_opt['options'] += netdev
185 # If MAC is not specified use autogenerated 52:54:00:00:04:<vhost_id>
186 # e.g. vhost1 MAC is 52:54:00:00:04:01
188 mac = '52:54:00:00:04:{0:02x}'.format(self._vhost_id)
189 extend_options = 'csum=off,gso=off,guest_tso4=off,guest_tso6=off,'\
190 'guest_ecn=off,mrg_rxbuf=off'
191 # Create Virtio network device.
192 device = ' -device virtio-net-pci,netdev=vhost{0},mac={1},{2}'.format(
193 self._vhost_id, mac, extend_options)
194 self._qemu_opt['options'] += device
195 # Add interface MAC and socket to the node dict
196 if_data = {'mac_address': mac, 'socket': socket}
197 if_name = 'vhost{}'.format(self._vhost_id)
198 self._vm_info['interfaces'][if_name] = if_data
199 # Add socket to the socket list
200 self._socks.append(socket)
202 def _qemu_qmp_exec(self, cmd):
203 """Execute QMP command.
205 QMP is JSON based protocol which allows to control QEMU instance.
207 :param cmd: QMP command to execute.
209 :return: Command output in python representation of JSON format. The
210 { "return": {} } response is QMP's success response. An error
211 response will contain the "error" keyword instead of "return".
213 # To enter command mode, the qmp_capabilities command must be issued.
214 qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' + \
215 '{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc -U ' + \
217 (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
218 if 0 != int(ret_code):
219 logger.debug('QMP execute failed {0}'.format(stderr))
220 raise RuntimeError('QMP execute "{0}" failed on {1}'.format(cmd,
223 # Skip capabilities negotiation messages.
224 out_list = stdout.splitlines()
225 if len(out_list) < 3:
226 raise RuntimeError('Invalid QMP output on {0}'.format(
228 return json.loads(out_list[2])
230 def _qemu_qga_flush(self):
231 """Flush the QGA parser state
233 qga_cmd = 'printf "\xFF" | sudo -S nc ' \
234 '-q 1 -U ' + self.__QGA_SOCK
235 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
236 if 0 != int(ret_code):
237 logger.debug('QGA execute failed {0}'.format(stderr))
238 raise RuntimeError('QGA execute "{0}" failed on {1}'.format(cmd,
243 return json.loads(stdout.split('\n', 1)[0])
245 def _qemu_qga_exec(self, cmd):
246 """Execute QGA command.
248 QGA provide access to a system-level agent via standard QMP commands.
250 :param cmd: QGA command to execute.
253 qga_cmd = 'echo "{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc ' \
254 '-q 1 -U ' + self.__QGA_SOCK
255 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
256 if 0 != int(ret_code):
257 logger.debug('QGA execute failed {0}'.format(stderr))
258 raise RuntimeError('QGA execute "{0}" failed on {1}'.format(cmd,
263 return json.loads(stdout.split('\n', 1)[0])
265 def _wait_until_vm_boot(self, timeout=300):
266 """Wait until QEMU VM is booted.
268 Ping QEMU guest agent each 5s until VM booted or timeout.
270 :param timeout: Waiting timeout in seconds (optional, default 300s).
275 if time() - start > timeout:
276 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
277 self._qemu_opt['disk_image'], self._node['host']))
278 self._qemu_qga_flush()
279 out = self._qemu_qga_exec('guest-ping')
280 # Empty output - VM not booted yet
283 # Non-error return - VM booted
284 elif out.get('return') is not None:
286 # Skip error and wait
287 elif out.get('error') is not None:
290 raise RuntimeError('QGA guest-ping unexpected output {}'.format(
292 logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
295 def _update_vm_interfaces(self):
296 """Update interface names in VM node dict."""
297 # Send guest-network-get-interfaces command via QGA, output example:
298 # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
299 # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
300 out = self._qemu_qga_exec('guest-network-get-interfaces')
301 interfaces = out.get('return')
304 raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
305 self._qemu_opt['disk_image'], self._node['host']))
306 # Create MAC-name dict
307 for interface in interfaces:
308 if 'hardware-address' not in interface:
310 mac_name[interface['hardware-address']] = interface['name']
311 # Match interface by MAC and save interface name
312 for interface in self._vm_info['interfaces'].values():
313 mac = interface.get('mac_address')
314 if_name = mac_name.get(mac)
316 logger.trace('Interface name for MAC {} not found'.format(mac))
318 interface['name'] = if_name
320 def _huge_page_check(self):
321 """Huge page check."""
322 huge_mnt = self._qemu_opt.get('huge_mnt')
323 mem_size = self._qemu_opt.get('mem_size')
324 # Check size of free huge pages
325 (_, output, _) = self._ssh.exec_command('grep Huge /proc/meminfo')
326 regex = re.compile(r'HugePages_Free:\s+(\d+)')
327 match = regex.search(output)
328 huge_free = int(match.group(1))
329 regex = re.compile(r'Hugepagesize:\s+(\d+)')
330 match = regex.search(output)
331 huge_size = int(match.group(1))
332 if (mem_size * 1024) > (huge_free * huge_size):
333 raise RuntimeError('Not enough free huge pages {0} kB, required '
334 '{1} MB'.format(huge_free * huge_size, mem_size))
335 # Check if huge pages mount point exist
337 (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
338 for line in output.splitlines():
339 # Try to find something like:
340 # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
342 if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
345 # If huge page mount point not exist create one
347 cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
349 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
350 if int(ret_code) != 0:
351 logger.debug('Mount huge pages failed {0}'.format(stderr))
352 raise RuntimeError('Mount huge pages failed on {0}'.format(
355 def _huge_page_allocate(self):
356 """Huge page allocate."""
357 huge_mnt = self._qemu_opt.get('huge_mnt')
358 mem_size = self._qemu_opt.get('mem_size')
359 # Check size of free huge pages
360 (_, output, _) = self._ssh.exec_command('grep Huge /proc/meminfo')
361 regex = re.compile(r'HugePages_Free:\s+(\d+)')
362 match = regex.search(output)
363 huge_free = int(match.group(1))
364 regex = re.compile(r'HugePages_Total:\s+(\d+)')
365 match = regex.search(output)
366 huge_total = int(match.group(1))
367 regex = re.compile(r'Hugepagesize:\s+(\d+)')
368 match = regex.search(output)
369 huge_size = int(match.group(1))
371 mem_needed = abs((huge_free * huge_size) - (mem_size * 1024))
374 huge_to_allocate = (mem_needed / huge_size) + huge_total
375 # Increase limit of allowed max hugepage count
376 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/max_map_count'.format(
378 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
379 # Increase hugepage count
380 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/nr_hugepages'.format(
382 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
383 if int(ret_code) != 0:
384 logger.debug('Mount huge pages failed {0}'.format(stderr))
385 raise RuntimeError('Mount huge pages failed on {0}'.format(
388 def qemu_start(self):
389 """Start QEMU and wait until VM boot.
391 :return: VM node info.
393 .. note:: First set at least node to run QEMU on.
394 .. warning:: Starts only one VM on the node.
397 ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
398 self._qemu_opt.get('ssh_fwd_port'))
399 # Memory and huge pages
400 mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
401 'share=on -m {0} -numa node,memdev=mem'.format(
402 self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
404 # By default check only if hugepages are availbale.
405 # If 'huge_allocate' is set to true try to allocate as well.
407 self._huge_page_check()
408 except RuntimeError as runtime_error:
409 if self._qemu_opt.get('huge_allocate'):
410 self._huge_page_allocate()
414 # Setup QMP via unix socket
415 qmp = '-qmp unix:{0},server,nowait'.format(self.__QMP_SOCK)
416 # Setup serial console
417 serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \
418 'nowait -device isa-serial,chardev=gnc0'.format(
419 self._qemu_opt.get('serial_port'))
420 # Setup QGA via chardev (unix socket) and isa-serial channel
421 qga = '-chardev socket,path=/tmp/qga.sock,server,nowait,id=qga0 ' \
422 '-device isa-serial,chardev=qga0'
424 graphic = '-monitor none -display none -vga none'
426 cmd = '{0} {1} {2} {3} {4} -hda {5} {6} {7} {8} {9}'.format(
427 self.__QEMU_BIN, self._qemu_opt.get('smp'), mem, ssh_fwd,
428 self._qemu_opt.get('options'),
429 self._qemu_opt.get('disk_image'), qmp, serial, qga, graphic)
430 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300)
431 if int(ret_code) != 0:
432 logger.debug('QEMU start failed {0}'.format(stderr))
433 raise RuntimeError('QEMU start failed on {0}'.format(
435 logger.trace('QEMU running')
437 self._wait_until_vm_boot()
438 # Update interface names in VM node dict
439 self._update_vm_interfaces()
440 # Return VM node dict
444 """Quit the QEMU emulator."""
445 out = self._qemu_qmp_exec('quit')
446 err = out.get('error')
448 raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
449 self._node['host'], json.dumps(err)))
451 def qemu_system_powerdown(self):
452 """Power down the system (if supported)."""
453 out = self._qemu_qmp_exec('system_powerdown')
454 err = out.get('error')
456 raise RuntimeError('QEMU system powerdown failed on {0}, '
457 'error: {1}'.format(self._node['host'], json.dumps(err)))
459 def qemu_system_reset(self):
460 """Reset the system."""
461 out = self._qemu_qmp_exec('system_reset')
462 err = out.get('error')
464 raise RuntimeError('QEMU system reset failed on {0}, '
465 'error: {1}'.format(self._node['host'], json.dumps(err)))
468 """Kill qemu process."""
469 # TODO: add PID storage so that we can kill specific PID
470 # Note: in QEMU start phase there are 3 QEMU processes because we
472 self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
474 def qemu_clear_socks(self):
475 """Remove all sockets created by QEMU."""
476 # If serial console port still open kill process
477 cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port'))
478 self._ssh.exec_command_sudo(cmd)
479 # Delete all created sockets
480 for sock in self._socks:
481 cmd = 'rm -f {}'.format(sock)
482 self._ssh.exec_command_sudo(cmd)
484 def qemu_system_status(self):
485 """Return current VM status.
487 VM should be in following status:
489 - debug: QEMU running on a debugger
490 - finish-migrate: paused to finish the migration process
491 - inmigrate: waiting for an incoming migration
492 - internal-error: internal error has occurred
493 - io-error: the last IOP has failed
495 - postmigrate: paused following a successful migrate
496 - prelaunch: QEMU was started with -S and guest has not started
497 - restore-vm: paused to restore VM state
498 - running: actively running
499 - save-vm: paused to save the VM state
500 - shutdown: shut down (and -no-shutdown is in use)
501 - suspended: suspended (ACPI S3)
502 - watchdog: watchdog action has been triggered
503 - guest-panicked: panicked as a result of guest OS panic
508 out = self._qemu_qmp_exec('query-status')
509 ret = out.get('return')
511 return ret.get('status')
513 err = out.get('error')
514 raise RuntimeError('QEMU query-status failed on {0}, '
515 'error: {1}'.format(self._node['host'], json.dumps(err)))
518 def build_qemu(node):
519 """Build QEMU from sources.
521 :param node: Node to build QEMU on.
527 (ret_code, stdout, stderr) = \
528 ssh.exec_command('sudo -Sn bash {0}/{1}/qemu_build.sh'.format(
529 Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH), 1000)
531 if 0 != int(ret_code):
532 logger.debug('QEMU build failed {0}'.format(stderr))
533 raise RuntimeError('QEMU build failed on {0}'.format(node['host']))