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."""
18 from time import time, sleep
19 from robot.api import logger
20 from resources.libraries.python.ssh import SSH
21 from resources.libraries.python.constants import Constants
22 from resources.libraries.python.topology import NodeType
25 class QemuUtils(object):
28 __QEMU_BIN = '/opt/qemu/bin/qemu-system-x86_64'
29 # QEMU Machine Protocol socket
30 __QMP_SOCK = '/tmp/qmp.sock'
31 # QEMU Guest Agent socket
32 __QGA_SOCK = '/tmp/qga.sock'
37 self._qemu_opt['smp'] = '-smp 1,sockets=1,cores=1,threads=1'
38 # Daemonize the QEMU process after initialization. Default one
39 # management interface.
40 self._qemu_opt['options'] = '-daemonize -enable-kvm ' \
41 '-machine pc-1.0,accel=kvm,usb=off,mem-merge=off ' \
42 '-net nic,macaddr=52:54:00:00:02:01'
43 self._qemu_opt['ssh_fwd_port'] = 10022
44 # Default serial console port
45 self._qemu_opt['serial_port'] = 4556
46 # Default 512MB virtual RAM
47 self._qemu_opt['mem_size'] = 512
48 # Default huge page mount point, required for Vhost-user interfaces.
49 self._qemu_opt['huge_mnt'] = '/mnt/huge'
50 # Default image for CSIT virl setup
51 self._qemu_opt['disk_image'] = '/var/lib/vm/vhost-nested.img'
63 self._socks = [self.__QMP_SOCK, self.__QGA_SOCK]
65 def qemu_set_smp(self, cpus, cores, threads, sockets):
66 """Set SMP option for QEMU
68 :param cpus: Number of CPUs.
69 :param cores: Number of CPU cores on one socket.
70 :param threads: Number of threads on one CPU core.
71 :param sockets: Number of discrete sockets in the system.
77 self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
78 cpus, cores, threads, sockets)
80 def qemu_set_ssh_fwd_port(self, fwd_port):
81 """Set host port for guest SSH forwarding.
83 :param fwd_port: Port number on host for guest SSH forwarding.
86 self._qemu_opt['ssh_fwd_port'] = fwd_port
87 self._vm_info['port'] = fwd_port
89 def qemu_set_serial_port(self, port):
90 """Set serial console port.
92 :param port: Serial console port.
95 self._qemu_opt['serial_port'] = port
97 def qemu_set_mem_size(self, mem_size):
98 """Set virtual RAM size.
100 :param mem_size: RAM size in Mega Bytes.
103 self._qemu_opt['mem_size'] = mem_size
105 def qemu_set_huge_mnt(self, huge_mnt):
106 """Set hugefile mount point.
108 :param huge_mnt: System hugefile mount point.
111 self._qemu_opt['huge_mnt'] = huge_mnt
113 def qemu_set_disk_image(self, disk_image):
116 :param disk_image: Path of the disk image.
117 :type disk_image: str
119 self._qemu_opt['disk_image'] = disk_image
121 def qemu_set_node(self, node):
122 """Set node to run QEMU on.
124 :param node: Node to run QEMU on.
129 self._ssh.connect(node)
130 self._vm_info['host'] = node['host']
132 def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
133 """Add Vhost-user interface.
135 :param socket: Path of the unix socket.
136 :param server: If True the socket shall be a listening socket.
137 :param mac: Vhost-user interface MAC address (optional, otherwise is
138 used autogenerated MAC 52:54:00:00:04:xx).
144 # Create unix socket character device.
145 chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
149 self._qemu_opt['options'] += chardev
150 # Create Vhost-user network backend.
151 netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0}'.format(
153 self._qemu_opt['options'] += netdev
154 # If MAC is not specified use autogenerated 52:54:00:00:04:<vhost_id>
155 # e.g. vhost1 MAC is 52:54:00:00:04:01
157 mac = '52:54:00:00:04:{0:02x}'.format(self._vhost_id)
158 # Create Virtio network device.
159 device = ' -device virtio-net-pci,netdev=vhost{0},mac={1}'.format(
161 self._qemu_opt['options'] += device
162 # Add interface MAC and socket to the node dict
163 if_data = {'mac_address': mac, 'socket': socket}
164 if_name = 'vhost{}'.format(self._vhost_id)
165 self._vm_info['interfaces'][if_name] = if_data
166 # Add socket to the socket list
167 self._socks.append(socket)
169 def _qemu_qmp_exec(self, cmd):
170 """Execute QMP command.
172 QMP is JSON based protocol which allows to control QEMU instance.
174 :param cmd: QMP command to execute.
176 :return: Command output in python representation of JSON format. The
177 { "return": {} } response is QMP's success response. An error
178 response will contain the "error" keyword instead of "return".
180 # To enter command mode, the qmp_capabilities command must be issued.
181 qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' + \
182 '{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc -U ' + \
184 (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
185 if 0 != int(ret_code):
186 logger.debug('QMP execute failed {0}'.format(stderr))
187 raise RuntimeError('QMP execute "{0}" failed on {1}'.format(cmd,
190 # Skip capabilities negotiation messages.
191 out_list = stdout.splitlines()
192 if len(out_list) < 3:
193 raise RuntimeError('Invalid QMP output on {0}'.format(
195 return json.loads(out_list[2])
197 def _qemu_qga_exec(self, cmd):
198 """Execute QGA command.
200 QGA provide access to a system-level agent via standard QMP commands.
202 :param cmd: QGA command to execute.
205 qga_cmd = 'echo "{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc ' \
206 '-q 1 -U ' + self.__QGA_SOCK
207 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
208 if 0 != int(ret_code):
209 logger.debug('QGA execute failed {0}'.format(stderr))
210 raise RuntimeError('QGA execute "{0}" failed on {1}'.format(cmd,
215 return json.loads(stdout)
217 def _wait_until_vm_boot(self, timeout=300):
218 """Wait until QEMU VM is booted.
220 Ping QEMU guest agent each 5s until VM booted or timeout.
222 :param timeout: Waiting timeout in seconds (optional, default 300s).
227 if time() - start > timeout:
228 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
229 self._qemu_opt['disk_image'], self._node['host']))
230 out = self._qemu_qga_exec('guest-ping')
231 # Empty output - VM not booted yet
234 # Non-error return - VM booted
235 elif out.get('return') is not None:
238 raise RuntimeError('QGA guest-ping unexpected output {}'.format(
240 logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
243 def _update_vm_interfaces(self):
244 """Update interface names in VM node dict."""
245 # Send guest-network-get-interfaces command via QGA, output example:
246 # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
247 # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
248 out = self._qemu_qga_exec('guest-network-get-interfaces')
249 interfaces = out.get('return')
252 raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
253 self._qemu_opt['disk_image'], self._node['host']))
254 # Create MAC-name dict
255 for interface in interfaces:
256 if 'hardware-address' not in interface:
258 mac_name[interface['hardware-address']] = interface['name']
259 # Match interface by MAC and save interface name
260 for interface in self._vm_info['interfaces'].values():
261 mac = interface.get('mac_address')
262 if_name = mac_name.get(mac)
264 logger.trace('Interface name for MAC {} not found'.format(mac))
266 interface['name'] = if_name
268 def _huge_page_check(self):
269 """Huge page check."""
270 huge_mnt = self._qemu_opt.get('huge_mnt')
271 mem_size = self._qemu_opt.get('mem_size')
272 # Check size of free huge pages
273 (_, output, _) = self._ssh.exec_command('grep Huge /proc/meminfo')
274 regex = re.compile(r'HugePages_Free:\s+(\d+)')
275 match = regex.search(output)
276 huge_free = int(match.group(1))
277 regex = re.compile(r'Hugepagesize:\s+(\d+)')
278 match = regex.search(output)
279 huge_size = int(match.group(1))
280 if (mem_size * 1024) > (huge_free * huge_size):
281 raise RuntimeError('Not enough free huge pages {0} kB, required '
282 '{1} MB'.format(huge_free * huge_size, mem_size))
283 # Check if huge pages mount point exist
285 (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
286 for line in output.splitlines():
287 # Try to find something like:
288 # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
290 if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
293 # If huge page mount point not exist create one
295 cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
297 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
298 if int(ret_code) != 0:
299 logger.debug('Mount huge pages failed {0}'.format(stderr))
300 raise RuntimeError('Mount huge pages failed on {0}'.format(
303 def qemu_start(self):
304 """Start QEMU and wait until VM boot.
306 :return: VM node info
308 .. note:: First set at least node to run QEMU on.
309 .. warning:: Starts only one VM on the node.
312 ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
313 self._qemu_opt.get('ssh_fwd_port'))
314 # Memory and huge pages
315 mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
316 'share=on -m {0} -numa node,memdev=mem'.format(
317 self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
318 self._huge_page_check()
319 # Setup QMP via unix socket
320 qmp = '-qmp unix:{0},server,nowait'.format(self.__QMP_SOCK)
321 # Setup serial console
322 serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \
323 'nowait -device isa-serial,chardev=gnc0'.format(
324 self._qemu_opt.get('serial_port'))
325 # Setup QGA via chardev (unix socket) and isa-serial channel
326 qga = '-chardev socket,path=/tmp/qga.sock,server,nowait,id=qga0 ' \
327 '-device isa-serial,chardev=qga0'
329 graphic = '-monitor none -display none -vga none'
331 cmd = '{0} {1} {2} {3} {4} -hda {5} {6} {7} {8} {9}'.format(
332 self.__QEMU_BIN, self._qemu_opt.get('smp'), mem, ssh_fwd,
333 self._qemu_opt.get('options'),
334 self._qemu_opt.get('disk_image'), qmp, serial, qga, graphic)
335 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300)
336 if int(ret_code) != 0:
337 logger.debug('QEMU start failed {0}'.format(stderr))
338 raise RuntimeError('QEMU start failed on {0}'.format(
340 logger.trace('QEMU running')
342 self._wait_until_vm_boot()
343 # Update interface names in VM node dict
344 self._update_vm_interfaces()
345 # Return VM node dict
349 """Quit the QEMU emulator."""
350 out = self._qemu_qmp_exec('quit')
351 err = out.get('error')
353 raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
354 self._node['host'], json.dumps(err)))
356 def qemu_system_powerdown(self):
357 """Power down the system (if supported)."""
358 out = self._qemu_qmp_exec('system_powerdown')
359 err = out.get('error')
361 raise RuntimeError('QEMU system powerdown failed on {0}, '
362 'error: {1}'.format(self._node['host'], json.dumps(err)))
364 def qemu_system_reset(self):
365 """Reset the system."""
366 out = self._qemu_qmp_exec('system_reset')
367 err = out.get('error')
369 raise RuntimeError('QEMU system reset failed on {0}, '
370 'error: {1}'.format(self._node['host'], json.dumps(err)))
373 """Kill qemu process."""
374 # TODO: add PID storage so that we can kill specific PID
375 # Note: in QEMU start phase there are 3 QEMU processes because we
377 self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
379 def qemu_clear_socks(self):
380 """Remove all sockets created by QEMU."""
381 # If serial console port still open kill process
382 cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port'))
383 self._ssh.exec_command_sudo(cmd)
384 # Delete all created sockets
385 for sock in self._socks:
386 cmd = 'rm -f {}'.format(sock)
387 self._ssh.exec_command_sudo(cmd)
389 def qemu_system_status(self):
390 """Return current VM status.
392 VM should be in following status:
394 - debug: QEMU running on a debugger
395 - finish-migrate: paused to finish the migration process
396 - inmigrate: waiting for an incoming migration
397 - internal-error: internal error has occurred
398 - io-error: the last IOP has failed
400 - postmigrate: paused following a successful migrate
401 - prelaunch: QEMU was started with -S and guest has not started
402 - restore-vm: paused to restore VM state
403 - running: actively running
404 - save-vm: paused to save the VM state
405 - shutdown: shut down (and -no-shutdown is in use)
406 - suspended: suspended (ACPI S3)
407 - watchdog: watchdog action has been triggered
408 - guest-panicked: panicked as a result of guest OS panic
413 out = self._qemu_qmp_exec('query-status')
414 ret = out.get('return')
416 return ret.get('status')
418 err = out.get('error')
419 raise RuntimeError('QEMU query-status failed on {0}, '
420 'error: {1}'.format(self._node['host'], json.dumps(err)))
423 def build_qemu(node):
424 """Build QEMU from sources.
426 :param node: Node to build QEMU on.
432 (ret_code, stdout, stderr) = \
433 ssh.exec_command('sudo -Sn bash {0}/{1}/qemu_build.sh'.format(
434 Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH), 1000)
436 if 0 != int(ret_code):
437 logger.debug('QEMU build failed {0}'.format(stderr))
438 raise RuntimeError('QEMU build failed on {0}'.format(node['host']))