1 # Copyright (c) 2019 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 # Disable due to pylint bug
20 # pylint: disable=no-name-in-module,import-error
21 from distutils.version import StrictVersion
23 from robot.api import logger
25 from resources.libraries.python.ssh import SSH, SSHTimeout
26 from resources.libraries.python.constants import Constants
27 from resources.libraries.python.DUTSetup import DUTSetup
28 from resources.libraries.python.topology import NodeType, Topology
31 class QemuUtils(object):
34 def __init__(self, qemu_id=1):
35 self._qemu_id = qemu_id
41 # Path to QEMU binary. Use x86_64 by default
42 self._qemu_opt['qemu_path'] = '/usr/bin/'
43 self._qemu_opt['qemu_bin'] = 'qemu-system-x86_64'
44 # QEMU Machine Protocol socket
45 self._qemu_opt['qmp_sock'] = '/tmp/qmp{0}.sock'.format(self._qemu_id)
46 # QEMU Guest Agent socket
47 self._qemu_opt['qga_sock'] = '/tmp/qga{0}.sock'.format(self._qemu_id)
49 self._qemu_opt['pid_file'] = '/tmp/qemu{0}.pid'.format(self._qemu_id)
51 self._qemu_opt['smp'] = '-smp 1,sockets=1,cores=1,threads=1'
52 # Daemonize the QEMU process after initialization. Default one
53 # management interface.
54 self._qemu_opt['options'] = '-cpu host -daemonize -enable-kvm ' \
55 '-machine pc,accel=kvm,usb=off,mem-merge=off ' \
56 '-net nic,macaddr=52:54:00:00:{0:02x}:ff -balloon none'\
57 .format(self._qemu_id)
58 self._qemu_opt['ssh_fwd_port'] = 10021 + qemu_id
59 # Default serial console port
60 self._qemu_opt['serial_port'] = 4555 + qemu_id
61 # Default 512MB virtual RAM
62 self._qemu_opt['mem_size'] = 512
63 # Default huge page mount point, required for Vhost-user interfaces.
64 self._qemu_opt['huge_mnt'] = '/mnt/huge'
65 # Default do not allocate huge pages.
66 self._qemu_opt['huge_allocate'] = False
67 # Default image for CSIT virl setup
68 self._qemu_opt['disk_image'] = '/var/lib/vm/vhost-nested.img'
70 self._qemu_opt['queue_count'] = 1
72 self._qemu_opt['queue_size'] = None
76 'port': self._qemu_opt['ssh_fwd_port'],
82 self._socks = [self._qemu_opt.get('qmp_sock'),
83 self._qemu_opt.get('qga_sock')]
85 def qemu_set_path(self, path):
86 """Set binary path for QEMU.
88 :param path: Absolute path in filesystem.
91 self._qemu_opt['qemu_path'] = path
93 def qemu_set_queue_count(self, count):
94 """Set number of virtio queues.
96 :param count: Number of virtio queues.
99 self._qemu_opt['queue_count'] = int(count)
101 def qemu_set_queue_size(self, size):
102 """Set RX/TX size of virtio queues.
104 :param size: Size of virtio queues.
107 self._qemu_opt['queue_size'] = int(size)
109 def qemu_set_smp(self, smp, cores, threads, sockets):
110 """Set SMP option for QEMU.
112 :param smp: Number of CPUs.
113 :param cores: Number of CPU cores on one socket.
114 :param threads: Number of threads on one CPU core.
115 :param sockets: Number of discrete sockets in the system.
121 self._qemu_opt['smp'] = \
122 ('-smp {smp},cores={cores},threads={threads},sockets={sockets}'.
123 format(smp=smp, cores=cores, threads=threads, sockets=sockets))
125 def qemu_set_ssh_fwd_port(self, fwd_port):
126 """Set host port for guest SSH forwarding.
128 :param fwd_port: Port number on host for guest SSH forwarding.
131 self._qemu_opt['ssh_fwd_port'] = fwd_port
132 self._vm_info['port'] = fwd_port
134 def qemu_set_serial_port(self, port):
135 """Set serial console port.
137 :param port: Serial console port.
140 self._qemu_opt['serial_port'] = port
142 def qemu_set_mem_size(self, mem_size):
143 """Set virtual RAM size.
145 :param mem_size: RAM size in Mega Bytes.
148 self._qemu_opt['mem_size'] = int(mem_size)
150 def qemu_set_huge_mnt(self, huge_mnt):
151 """Set hugefile mount point.
153 :param huge_mnt: System hugefile mount point.
156 self._qemu_opt['huge_mnt'] = huge_mnt
158 def qemu_set_huge_allocate(self):
159 """Set flag to allocate more huge pages if needed."""
160 self._qemu_opt['huge_allocate'] = True
162 def qemu_set_disk_image(self, disk_image):
165 :param disk_image: Path of the disk image.
166 :type disk_image: str
168 self._qemu_opt['disk_image'] = disk_image
170 def qemu_set_affinity(self, *host_cpus):
171 """Set qemu affinity by getting thread PIDs via QMP and taskset to list
174 :param host_cpus: List of CPU cores.
175 :type host_cpus: list
177 qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
179 if len(qemu_cpus) != len(host_cpus):
180 raise ValueError('Host CPU count must match Qemu Thread count')
182 for qemu_cpu, host_cpu in zip(qemu_cpus, host_cpus):
183 ret_code, _, _ = self._ssh.exec_command_sudo(
184 'taskset -pc {host_cpu} {thread_id}'.
185 format(host_cpu=host_cpu, thread_id=qemu_cpu['thread_id']))
186 if int(ret_code) != 0:
187 raise RuntimeError('Set affinity failed on {host}'.
188 format(host=self._node['host']))
190 def qemu_set_scheduler_policy(self):
191 """Set scheduler policy to SCHED_RR with priority 1 for all Qemu CPU
194 :raises RuntimeError: Set scheduler policy failed.
196 qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
198 for qemu_cpu in qemu_cpus:
199 ret_code, _, _ = self._ssh.exec_command_sudo(
200 'chrt -r -p 1 {thread_id}'.
201 format(thread_id=qemu_cpu['thread_id']))
202 if int(ret_code) != 0:
203 raise RuntimeError('Set SCHED_RR failed on {host}'.
204 format(host=self._node['host']))
206 def qemu_set_node(self, node):
207 """Set node to run QEMU on.
209 :param node: Node to run QEMU on.
214 self._ssh.connect(node)
215 self._vm_info['host'] = node['host']
216 if node['port'] != 22:
217 self._vm_info['host_port'] = node['port']
218 self._vm_info['host_username'] = node['username']
219 self._vm_info['host_password'] = node['password']
220 arch = Topology.get_node_arch(node)
221 self._qemu_opt['qemu_bin'] = 'qemu-system-{arch}'.format(arch=arch)
223 def qemu_add_vhost_user_if(self, socket, server=True, mac=None,
225 """Add Vhost-user interface.
227 :param socket: Path of the unix socket.
228 :param server: If True the socket shall be a listening socket.
229 :param mac: Vhost-user interface MAC address (optional, otherwise is
230 used auto-generated MAC 52:54:00:00:xx:yy).
231 :param jumbo_frames: Set True if jumbo frames are used in the test.
235 :type jumbo_frames: bool
238 # Create unix socket character device.
239 chardev = (' -chardev socket,id=char{vhost_id},path={socket}{server}'.
240 format(vhost_id=self._vhost_id,
242 server=',server' if server is True else ''))
243 self._qemu_opt['options'] += chardev
244 # Create Vhost-user network backend.
245 netdev = (' -netdev vhost-user,id=vhost{vhost_id},'
246 'chardev=char{vhost_id},queues={queue_count}'.
247 format(vhost_id=self._vhost_id,
248 queue_count=self._qemu_opt.get('queue_count')))
249 self._qemu_opt['options'] += netdev
250 # If MAC is not specified use auto-generated MAC address based on
251 # template 52:54:00:00:<qemu_id>:<vhost_id>, e.g. vhost1 MAC of QEMU
252 # with ID 1 is 52:54:00:00:01:01
253 mac = ('52:54:00:00:{qemu_id:02x}:{vhost_id:02x}'.
254 format(qemu_id=self._qemu_id, vhost_id=self._vhost_id))\
255 if mac is None else mac
257 queue_size = ('rx_queue_size={queue_size},tx_queue_size={queue_size}'.
258 format(queue_size=self._qemu_opt.get('queue_size')))\
259 if self._qemu_opt.get('queue_size') else ''
260 vector_size = ('vectors={vectors}'.
261 format(vectors=2*self._qemu_opt.get('queue_count')+2))\
262 if self._qemu_opt.get('queue_count') else ''
264 # Create Virtio network device.
265 device = (' -device virtio-net-pci,netdev=vhost{vhost_id},mac={mac},'
266 'mq=on,{vector_size},csum=off,gso=off,guest_tso4=off,'
267 'guest_tso6=off,guest_ecn=off,mrg_rxbuf={mbuf},{queue_size}'.
268 format(vhost_id=self._vhost_id, mac=mac,
269 mbuf='on,host_mtu=9200' if jumbo_frames else 'off',
270 queue_size=queue_size, vector_size=vector_size))
271 self._qemu_opt['options'] += device
272 # Add interface MAC and socket to the node dict
273 if_data = {'mac_address': mac, 'socket': socket}
274 if_name = 'vhost{vhost_id}'.format(vhost_id=self._vhost_id)
275 self._vm_info['interfaces'][if_name] = if_data
276 # Add socket to the socket list
277 self._socks.append(socket)
279 def _qemu_qmp_exec(self, cmd):
280 """Execute QMP command.
282 QMP is JSON based protocol which allows to control QEMU instance.
284 :param cmd: QMP command to execute.
286 :returns: Command output in python representation of JSON format. The
287 { "return": {} } response is QMP's success response. An error
288 response will contain the "error" keyword instead of "return".
290 # To enter command mode, the qmp_capabilities command must be issued.
291 ret_code, stdout, _ = self._ssh.exec_command(
292 'echo "{{ \\"execute\\": \\"qmp_capabilities\\" }}'
293 '{{ \\"execute\\": \\"{cmd}\\" }}" | '
294 'sudo -S socat - UNIX-CONNECT:{qmp_sock}'.
295 format(cmd=cmd, qmp_sock=self._qemu_opt.get('qmp_sock')))
296 if int(ret_code) != 0:
297 raise RuntimeError('QMP execute "{cmd}" failed on {host}'.
298 format(cmd=cmd, host=self._node['host']))
299 # Skip capabilities negotiation messages.
300 out_list = stdout.splitlines()
301 if len(out_list) < 3:
302 raise RuntimeError('Invalid QMP output on {host}'.
303 format(host=self._node['host']))
304 return json.loads(out_list[2])
306 def _qemu_qga_flush(self):
307 """Flush the QGA parser state."""
308 ret_code, stdout, _ = self._ssh.exec_command(
309 '(printf "\xFF"; sleep 1) | '
310 'sudo -S socat - UNIX-CONNECT:{qga_sock}'.
311 format(qga_sock=self._qemu_opt.get('qga_sock')))
312 if int(ret_code) != 0:
313 raise RuntimeError('QGA flush failed on {host}'.
314 format(host=self._node['host']))
317 return json.loads(stdout.split('\n', 1)[0])
319 def _qemu_qga_exec(self, cmd):
320 """Execute QGA command.
322 QGA provide access to a system-level agent via standard QMP commands.
324 :param cmd: QGA command to execute.
327 ret_code, stdout, _ = self._ssh.exec_command(
328 '(echo "{{ \\"execute\\": \\"{cmd}\\" }}"; sleep 1) | '
329 'sudo -S socat - UNIX-CONNECT:{qga_sock}'.
330 format(cmd=cmd, qga_sock=self._qemu_opt.get('qga_sock')))
331 if int(ret_code) != 0:
332 raise RuntimeError('QGA execute "{cmd}" failed on {host}'.
333 format(cmd=cmd, host=self._node['host']))
336 return json.loads(stdout.split('\n', 1)[0])
338 def _wait_until_vm_boot(self, timeout=60):
339 """Wait until QEMU VM is booted.
341 First try to flush qga until there is output.
342 Then ping QEMU guest agent each 5s until VM booted or timeout.
344 :param timeout: Waiting timeout in seconds (optional, default 60s).
349 if time() - start > timeout:
350 raise RuntimeError('timeout, VM {disk} not booted on {host}'.
351 format(disk=self._qemu_opt['disk_image'],
352 host=self._node['host']))
355 out = self._qemu_qga_flush()
357 logger.trace('QGA qga flush unexpected output {out}'.
359 # Empty output - VM not booted yet
365 if time() - start > timeout:
366 raise RuntimeError('timeout, VM with {disk} not booted '
368 format(disk=self._qemu_opt['disk_image'],
369 host=self._node['host']))
372 out = self._qemu_qga_exec('guest-ping')
374 logger.trace('QGA guest-ping unexpected output {out}'.
376 # Empty output - VM not booted yet
379 # Non-error return - VM booted
380 elif out.get('return') is not None:
382 # Skip error and wait
383 elif out.get('error') is not None:
386 # If there is an unexpected output from QGA guest-info, try
387 # again until timeout.
388 logger.trace('QGA guest-ping unexpected output {out}'.
391 logger.trace('VM with {disk_image} booted on {host}'.
392 format(disk_image=self._qemu_opt['disk_image'],
393 host=self._node['host']))
395 def _update_vm_interfaces(self):
396 """Update interface names in VM node dict."""
397 # Send guest-network-get-interfaces command via QGA, output example:
398 # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
399 # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
400 out = self._qemu_qga_exec('guest-network-get-interfaces')
401 interfaces = out.get('return')
404 raise RuntimeError('Get VM {disk_image} interface list failed '
406 format(disk_image=self._qemu_opt['disk_image'],
407 host=self._node['host']))
408 # Create MAC-name dict
409 for interface in interfaces:
410 if 'hardware-address' not in interface:
412 mac_name[interface['hardware-address']] = interface['name']
413 # Match interface by MAC and save interface name
414 for interface in self._vm_info['interfaces'].values():
415 mac = interface.get('mac_address')
416 if_name = mac_name.get(mac)
418 logger.trace('Interface name for MAC {mac} not found'.
421 interface['name'] = if_name
423 def qemu_start(self):
424 """Start QEMU and wait until VM boot.
426 .. note:: First set at least node to run QEMU on.
428 :returns: VM node info.
432 bin_path = ('{qemu_path}{qemu_bin}'.
433 format(qemu_path=self._qemu_opt.get('qemu_path'),
434 qemu_bin=self._qemu_opt.get('qemu_bin')))
436 # Memory and huge pages
437 mem = ('-object memory-backend-file,id=mem,size={mem_size}M,'
438 'mem-path={path},share=on -m {mem_size} -numa node,memdev=mem'.
439 format(mem_size=self._qemu_opt.get('mem_size'),
440 path=self._qemu_opt.get('huge_mnt')))
443 drive = ('-drive file={disk_image},format=raw,cache=none,if=virtio'
445 format(disk_image=self._qemu_opt.get('disk_image'),
446 locking=',file.locking=off'\
447 if self._qemu_version_is_greater('2.10') else ''))
450 ssh = ('-net user,hostfwd=tcp::{ssh_fwd_port}-:22'.
451 format(ssh_fwd_port=self._qemu_opt.get('ssh_fwd_port')))
452 # Setup QMP via unix socket
453 qmp = ('-qmp unix:{qmp_sock},server,nowait'.
454 format(qmp_sock=self._qemu_opt.get('qmp_sock')))
455 # Setup QGA via chardev (unix socket) and isa-serial channel
456 qga = ('-chardev socket,path={qga_sock},server,nowait,id=qga0 '
457 '-device isa-serial,chardev=qga0'.
458 format(qga_sock=self._qemu_opt.get('qga_sock')))
459 # Setup serial console
460 serial = ('-chardev socket,host=127.0.0.1,port={serial_port},id=gnc0,'
461 'server,nowait -device isa-serial,chardev=gnc0'.
462 format(serial_port=self._qemu_opt.get('serial_port')))
465 graphic = '-monitor none -display none -vga none'
468 pid = ('-pidfile {pid_file}'.
469 format(pid_file=self._qemu_opt.get('pid_file')))
471 # By default check only if hugepages are available.
472 # If 'huge_allocate' is set to true try to allocate as well.
473 DUTSetup.check_huge_page(self._node, self._qemu_opt.get('huge_mnt'),
474 self._qemu_opt.get('mem_size'),
475 allocate=self._qemu_opt.get('huge_allocate'))
478 cmd = ('{bin_path} {smp} {mem} {ssh} {options} {drive} {qmp} {serial} '
479 '{qga} {graphic} {pid}'.
480 format(bin_path=bin_path, smp=self._qemu_opt.get('smp'),
481 mem=mem, ssh=ssh, options=self._qemu_opt.get('options'),
482 drive=drive, qmp=qmp, serial=serial, qga=qga,
483 graphic=graphic, pid=pid))
485 ret_code, _, _ = self._ssh.exec_command_sudo(cmd, timeout=300)
486 if int(ret_code) != 0:
487 raise RuntimeError('QEMU start failed on {host}'.
488 format(host=self._node['host']))
490 self._wait_until_vm_boot()
491 except (RuntimeError, SSHTimeout):
493 self.qemu_clear_socks()
495 logger.trace('QEMU started successfully.')
496 # Update interface names in VM node dict
497 self._update_vm_interfaces()
498 # Return VM node dict
502 """Quit the QEMU emulator."""
503 out = self._qemu_qmp_exec('quit')
504 err = out.get('error')
506 raise RuntimeError('QEMU quit failed on {host}: {error}'.
507 format(host=self._node['host'],
508 error=json.dumps(err)))
510 def qemu_system_powerdown(self):
511 """Power down the system (if supported)."""
512 out = self._qemu_qmp_exec('system_powerdown')
513 err = out.get('error')
516 'QEMU system powerdown failed on {host}: {error}'.
517 format(host=self._node['host'], error=json.dumps(err)))
519 def qemu_system_reset(self):
520 """Reset the system."""
521 out = self._qemu_qmp_exec('system_reset')
522 err = out.get('error')
525 'QEMU system reset failed on {host}: {error}'.
526 format(host=self._node['host'], error=json.dumps(err)))
529 """Kill qemu process."""
530 # Note: in QEMU start phase there are 3 QEMU processes because we
532 self._ssh.exec_command_sudo('chmod +r {pid}'.
533 format(pid=self._qemu_opt.get('pid_file')))
534 self._ssh.exec_command_sudo('kill -SIGKILL $(cat {pid})'.
535 format(pid=self._qemu_opt.get('pid_file')))
537 self._ssh.exec_command_sudo('rm -f {pid}'.
538 format(pid=self._qemu_opt.get('pid_file')))
540 def qemu_kill_all(self, node=None):
541 """Kill all qemu processes on DUT node if specified.
543 :param node: Node to kill all QEMU processes on.
547 self.qemu_set_node(node)
548 self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
550 def qemu_clear_socks(self):
551 """Remove all sockets created by QEMU."""
552 # If serial console port still open kill process
553 self._ssh.exec_command_sudo('fuser -k {serial_port}/tcp'.
555 self._qemu_opt.get('serial_port')))
556 # Delete all created sockets
557 for socket in self._socks:
558 self._ssh.exec_command_sudo('rm -f {socket}'.
559 format(socket=socket))
561 def qemu_system_status(self):
562 """Return current VM status.
564 VM should be in following status:
566 - debug: QEMU running on a debugger
567 - finish-migrate: paused to finish the migration process
568 - inmigrate: waiting for an incoming migration
569 - internal-error: internal error has occurred
570 - io-error: the last IOP has failed
572 - postmigrate: paused following a successful migrate
573 - prelaunch: QEMU was started with -S and guest has not started
574 - restore-vm: paused to restore VM state
575 - running: actively running
576 - save-vm: paused to save the VM state
577 - shutdown: shut down (and -no-shutdown is in use)
578 - suspended: suspended (ACPI S3)
579 - watchdog: watchdog action has been triggered
580 - guest-panicked: panicked as a result of guest OS panic
585 out = self._qemu_qmp_exec('query-status')
586 ret = out.get('return')
588 return ret.get('status')
590 err = out.get('error')
591 raise RuntimeError('QEMU query-status failed on {host}: {error}'.
592 format(host=self._node['host'],
593 error=json.dumps(err)))
595 def qemu_version(self):
596 """Return Qemu version.
598 :returns: Qemu version.
602 bin_path = ('{qemu_path}{qemu_bin}'.
603 format(qemu_path=self._qemu_opt.get('qemu_path'),
604 qemu_bin=self._qemu_opt.get('qemu_bin')))
607 ret_code, stdout, _ = self._ssh.exec_command_sudo(
608 '{bin_path} --version'.
609 format(bin_path=bin_path))
610 if int(ret_code) != 0:
611 raise RuntimeError('Failed to get QEMU version on {host}'.
612 format(host=self._node['host']))
614 return re.match(r'QEMU emulator version ([\d.]*)', stdout).group(1)
615 except (RuntimeError, SSHTimeout):
617 self.qemu_clear_socks()
620 def _qemu_version_is_greater(self, version):
621 """Compare Qemu versions.
623 :returns: True if installed Qemu version is greater.
626 return StrictVersion(self.qemu_version()) > StrictVersion(version)
629 def build_qemu(node, force_install=False, apply_patch=False):
630 """Build QEMU from sources.
632 :param node: Node to build QEMU on.
633 :param force_install: If True, then remove previous build.
634 :param apply_patch: If True, then apply patches from qemu_patches dir.
636 :type force_install: bool
637 :type apply_patch: bool
638 :raises RuntimeError: If building QEMU failed.
643 directory = (' --directory={install_dir}{patch}'.
644 format(install_dir=Constants.QEMU_INSTALL_DIR,
645 patch='-patch' if apply_patch else '-base'))
646 version = (' --version={install_version}'.
647 format(install_version=Constants.QEMU_INSTALL_VERSION))
648 force = ' --force' if force_install else ''
649 patch = ' --patch' if apply_patch else ''
650 arch = Topology.get_node_arch(node)
651 target_list = (' --target-list={arch}-softmmu'.
654 ret_code, _, _ = ssh.exec_command(
655 "sudo -E sh -c '{fw_dir}/{lib_sh}/qemu_build.sh{version}{directory}"
656 "{force}{patch}{target_list}'".
657 format(fw_dir=Constants.REMOTE_FW_DIR,
658 lib_sh=Constants.RESOURCES_LIB_SH,
659 version=version, directory=directory, force=force,
660 patch=patch, target_list=target_list), 1000)
662 if int(ret_code) != 0:
663 raise RuntimeError('QEMU build failed on {host}'.
664 format(host=node['host']))