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'] = '-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'
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 image for CSIT virl setup
53 self._qemu_opt['disk_image'] = '/var/lib/vm/vhost-nested.img'
65 self._socks = [self.__QMP_SOCK, self.__QGA_SOCK]
67 def qemu_set_smp(self, cpus, cores, threads, sockets):
68 """Set SMP option for QEMU
70 :param cpus: Number of CPUs.
71 :param cores: Number of CPU cores on one socket.
72 :param threads: Number of threads on one CPU core.
73 :param sockets: Number of discrete sockets in the system.
79 self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
80 cpus, cores, threads, sockets)
82 def qemu_set_ssh_fwd_port(self, fwd_port):
83 """Set host port for guest SSH forwarding.
85 :param fwd_port: Port number on host for guest SSH forwarding.
88 self._qemu_opt['ssh_fwd_port'] = fwd_port
89 self._vm_info['port'] = fwd_port
91 def qemu_set_serial_port(self, port):
92 """Set serial console port.
94 :param port: Serial console port.
97 self._qemu_opt['serial_port'] = port
99 def qemu_set_mem_size(self, mem_size):
100 """Set virtual RAM size.
102 :param mem_size: RAM size in Mega Bytes.
105 self._qemu_opt['mem_size'] = mem_size
107 def qemu_set_huge_mnt(self, huge_mnt):
108 """Set hugefile mount point.
110 :param huge_mnt: System hugefile mount point.
113 self._qemu_opt['huge_mnt'] = huge_mnt
115 def qemu_set_disk_image(self, disk_image):
118 :param disk_image: Path of the disk image.
119 :type disk_image: str
121 self._qemu_opt['disk_image'] = disk_image
123 def qemu_set_node(self, node):
124 """Set node to run QEMU on.
126 :param node: Node to run QEMU on.
131 self._ssh.connect(node)
132 self._vm_info['host'] = node['host']
134 def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
135 """Add Vhost-user interface.
137 :param socket: Path of the unix socket.
138 :param server: If True the socket shall be a listening socket.
139 :param mac: Vhost-user interface MAC address (optional, otherwise is
140 used autogenerated MAC 52:54:00:00:04:xx).
146 # Create unix socket character device.
147 chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
151 self._qemu_opt['options'] += chardev
152 # Create Vhost-user network backend.
153 netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0}'.format(
155 self._qemu_opt['options'] += netdev
156 # If MAC is not specified use autogenerated 52:54:00:00:04:<vhost_id>
157 # e.g. vhost1 MAC is 52:54:00:00:04:01
159 mac = '52:54:00:00:04:{0:02x}'.format(self._vhost_id)
160 # Create Virtio network device.
161 device = ' -device virtio-net-pci,netdev=vhost{0},mac={1}'.format(
163 self._qemu_opt['options'] += device
164 # Add interface MAC and socket to the node dict
165 if_data = {'mac_address': mac, 'socket': socket}
166 if_name = 'vhost{}'.format(self._vhost_id)
167 self._vm_info['interfaces'][if_name] = if_data
168 # Add socket to the socket list
169 self._socks.append(socket)
171 def _qemu_qmp_exec(self, cmd):
172 """Execute QMP command.
174 QMP is JSON based protocol which allows to control QEMU instance.
176 :param cmd: QMP command to execute.
178 :return: Command output in python representation of JSON format. The
179 { "return": {} } response is QMP's success response. An error
180 response will contain the "error" keyword instead of "return".
182 # To enter command mode, the qmp_capabilities command must be issued.
183 qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' + \
184 '{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc -U ' + \
186 (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
187 if 0 != int(ret_code):
188 logger.debug('QMP execute failed {0}'.format(stderr))
189 raise RuntimeError('QMP execute "{0}" failed on {1}'.format(cmd,
192 # Skip capabilities negotiation messages.
193 out_list = stdout.splitlines()
194 if len(out_list) < 3:
195 raise RuntimeError('Invalid QMP output on {0}'.format(
197 return json.loads(out_list[2])
199 def _qemu_qga_flush(self):
200 """Flush the QGA parser state
202 qga_cmd = 'printf "\xFF" | sudo -S nc ' \
203 '-q 1 -U ' + self.__QGA_SOCK
204 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
205 if 0 != int(ret_code):
206 logger.debug('QGA execute failed {0}'.format(stderr))
207 raise RuntimeError('QGA execute "{0}" failed on {1}'.format(cmd,
212 return json.loads(stdout.split('\n', 1)[0])
214 def _qemu_qga_exec(self, cmd):
215 """Execute QGA command.
217 QGA provide access to a system-level agent via standard QMP commands.
219 :param cmd: QGA command to execute.
222 qga_cmd = 'echo "{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc ' \
223 '-q 1 -U ' + self.__QGA_SOCK
224 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
225 if 0 != int(ret_code):
226 logger.debug('QGA execute failed {0}'.format(stderr))
227 raise RuntimeError('QGA execute "{0}" failed on {1}'.format(cmd,
232 return json.loads(stdout.split('\n', 1)[0])
234 def _wait_until_vm_boot(self, timeout=300):
235 """Wait until QEMU VM is booted.
237 Ping QEMU guest agent each 5s until VM booted or timeout.
239 :param timeout: Waiting timeout in seconds (optional, default 300s).
244 if time() - start > timeout:
245 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
246 self._qemu_opt['disk_image'], self._node['host']))
247 self._qemu_qga_flush()
248 out = self._qemu_qga_exec('guest-ping')
249 # Empty output - VM not booted yet
252 # Non-error return - VM booted
253 elif out.get('return') is not None:
255 # Skip error and wait
256 elif out.get('error') is not None:
259 raise RuntimeError('QGA guest-ping unexpected output {}'.format(
261 logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
264 def _update_vm_interfaces(self):
265 """Update interface names in VM node dict."""
266 # Send guest-network-get-interfaces command via QGA, output example:
267 # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
268 # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
269 out = self._qemu_qga_exec('guest-network-get-interfaces')
270 interfaces = out.get('return')
273 raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
274 self._qemu_opt['disk_image'], self._node['host']))
275 # Create MAC-name dict
276 for interface in interfaces:
277 if 'hardware-address' not in interface:
279 mac_name[interface['hardware-address']] = interface['name']
280 # Match interface by MAC and save interface name
281 for interface in self._vm_info['interfaces'].values():
282 mac = interface.get('mac_address')
283 if_name = mac_name.get(mac)
285 logger.trace('Interface name for MAC {} not found'.format(mac))
287 interface['name'] = if_name
289 def _huge_page_check(self):
290 """Huge page check."""
291 huge_mnt = self._qemu_opt.get('huge_mnt')
292 mem_size = self._qemu_opt.get('mem_size')
293 # Check size of free huge pages
294 (_, output, _) = self._ssh.exec_command('grep Huge /proc/meminfo')
295 regex = re.compile(r'HugePages_Free:\s+(\d+)')
296 match = regex.search(output)
297 huge_free = int(match.group(1))
298 regex = re.compile(r'Hugepagesize:\s+(\d+)')
299 match = regex.search(output)
300 huge_size = int(match.group(1))
301 if (mem_size * 1024) > (huge_free * huge_size):
302 raise RuntimeError('Not enough free huge pages {0} kB, required '
303 '{1} MB'.format(huge_free * huge_size, mem_size))
304 # Check if huge pages mount point exist
306 (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
307 for line in output.splitlines():
308 # Try to find something like:
309 # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
311 if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
314 # If huge page mount point not exist create one
316 cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
318 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
319 if int(ret_code) != 0:
320 logger.debug('Mount huge pages failed {0}'.format(stderr))
321 raise RuntimeError('Mount huge pages failed on {0}'.format(
324 def qemu_start(self):
325 """Start QEMU and wait until VM boot.
327 :return: VM node info.
329 .. note:: First set at least node to run QEMU on.
330 .. warning:: Starts only one VM on the node.
333 ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
334 self._qemu_opt.get('ssh_fwd_port'))
335 # Memory and huge pages
336 mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
337 'share=on -m {0} -numa node,memdev=mem'.format(
338 self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
339 self._huge_page_check()
340 # Setup QMP via unix socket
341 qmp = '-qmp unix:{0},server,nowait'.format(self.__QMP_SOCK)
342 # Setup serial console
343 serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \
344 'nowait -device isa-serial,chardev=gnc0'.format(
345 self._qemu_opt.get('serial_port'))
346 # Setup QGA via chardev (unix socket) and isa-serial channel
347 qga = '-chardev socket,path=/tmp/qga.sock,server,nowait,id=qga0 ' \
348 '-device isa-serial,chardev=qga0'
350 graphic = '-monitor none -display none -vga none'
352 cmd = '{0} {1} {2} {3} {4} -hda {5} {6} {7} {8} {9}'.format(
353 self.__QEMU_BIN, self._qemu_opt.get('smp'), mem, ssh_fwd,
354 self._qemu_opt.get('options'),
355 self._qemu_opt.get('disk_image'), qmp, serial, qga, graphic)
356 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300)
357 if int(ret_code) != 0:
358 logger.debug('QEMU start failed {0}'.format(stderr))
359 raise RuntimeError('QEMU start failed on {0}'.format(
361 logger.trace('QEMU running')
363 self._wait_until_vm_boot()
364 # Update interface names in VM node dict
365 self._update_vm_interfaces()
366 # Return VM node dict
370 """Quit the QEMU emulator."""
371 out = self._qemu_qmp_exec('quit')
372 err = out.get('error')
374 raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
375 self._node['host'], json.dumps(err)))
377 def qemu_system_powerdown(self):
378 """Power down the system (if supported)."""
379 out = self._qemu_qmp_exec('system_powerdown')
380 err = out.get('error')
382 raise RuntimeError('QEMU system powerdown failed on {0}, '
383 'error: {1}'.format(self._node['host'], json.dumps(err)))
385 def qemu_system_reset(self):
386 """Reset the system."""
387 out = self._qemu_qmp_exec('system_reset')
388 err = out.get('error')
390 raise RuntimeError('QEMU system reset failed on {0}, '
391 'error: {1}'.format(self._node['host'], json.dumps(err)))
394 """Kill qemu process."""
395 # TODO: add PID storage so that we can kill specific PID
396 # Note: in QEMU start phase there are 3 QEMU processes because we
398 self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
400 def qemu_clear_socks(self):
401 """Remove all sockets created by QEMU."""
402 # If serial console port still open kill process
403 cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port'))
404 self._ssh.exec_command_sudo(cmd)
405 # Delete all created sockets
406 for sock in self._socks:
407 cmd = 'rm -f {}'.format(sock)
408 self._ssh.exec_command_sudo(cmd)
410 def qemu_system_status(self):
411 """Return current VM status.
413 VM should be in following status:
415 - debug: QEMU running on a debugger
416 - finish-migrate: paused to finish the migration process
417 - inmigrate: waiting for an incoming migration
418 - internal-error: internal error has occurred
419 - io-error: the last IOP has failed
421 - postmigrate: paused following a successful migrate
422 - prelaunch: QEMU was started with -S and guest has not started
423 - restore-vm: paused to restore VM state
424 - running: actively running
425 - save-vm: paused to save the VM state
426 - shutdown: shut down (and -no-shutdown is in use)
427 - suspended: suspended (ACPI S3)
428 - watchdog: watchdog action has been triggered
429 - guest-panicked: panicked as a result of guest OS panic
434 out = self._qemu_qmp_exec('query-status')
435 ret = out.get('return')
437 return ret.get('status')
439 err = out.get('error')
440 raise RuntimeError('QEMU query-status failed on {0}, '
441 'error: {1}'.format(self._node['host'], json.dumps(err)))
444 def build_qemu(node):
445 """Build QEMU from sources.
447 :param node: Node to build QEMU on.
453 (ret_code, stdout, stderr) = \
454 ssh.exec_command('sudo -Sn bash {0}/{1}/qemu_build.sh'.format(
455 Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH), 1000)
457 if 0 != int(ret_code):
458 logger.debug('QEMU build failed {0}'.format(stderr))
459 raise RuntimeError('QEMU build failed on {0}'.format(node['host']))