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 = '/tmp/qemu-2.2.1/build/x86_64-softmmu/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 512MB virtual RAM
45 self._qemu_opt['mem_size'] = 512
46 # Default huge page mount point, required for Vhost-user interfaces.
47 self._qemu_opt['huge_mnt'] = '/mnt/huge'
59 self._socks = [self.__QMP_SOCK, self.__QGA_SOCK]
61 def qemu_set_smp(self, cpus, cores, threads, sockets):
62 """Set SMP option for QEMU
64 :param cpus: Number of CPUs.
65 :param cores: Number of CPU cores on one socket.
66 :param threads: Number of threads on one CPU core.
67 :param sockets: Number of discrete sockets in the system.
73 self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
74 cpus, cores, threads, sockets)
76 def qemu_set_ssh_fwd_port(self, fwd_port):
77 """Set host port for guest SSH forwarding.
79 :param fwd_port: Port number on host for guest SSH forwarding.
82 self._qemu_opt['ssh_fwd_port'] = fwd_port
83 self._vm_info['port'] = fwd_port
85 def qemu_set_mem_size(self, mem_size):
86 """Set virtual RAM size.
88 :param mem_size: RAM size in Mega Bytes.
91 self._qemu_opt['mem_size'] = mem_size
93 def qemu_set_huge_mnt(self, huge_mnt):
94 """Set hugefile mount point.
96 :param huge_mnt: System hugefile mount point.
99 self._qemu_opt['huge_mnt'] = huge_mnt
101 def qemu_set_disk_image(self, disk_image):
104 :param disk_image: Path of the disk image.
105 :type disk_image: str
107 self._qemu_opt['disk_image'] = disk_image
109 def qemu_set_node(self, node):
110 """Set node to run QEMU on.
112 :param node: Node to run QEMU on.
117 self._ssh.connect(node)
118 self._vm_info['host'] = node['host']
120 def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
121 """Add Vhost-user interface.
123 :param socket: Path of the unix socket.
124 :param server: If True the socket shall be a listening socket.
125 :param mac: Vhost-user interface MAC address (optional, otherwise is
126 used autogenerated MAC 52:54:00:00:04:xx).
132 # Create unix socket character device.
133 chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
137 self._qemu_opt['options'] += chardev
138 # Create Vhost-user network backend.
139 netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0}'.format(
141 self._qemu_opt['options'] += netdev
142 # If MAC is not specified use autogenerated 52:54:00:00:04:<vhost_id>
143 # e.g. vhost1 MAC is 52:54:00:00:04:01
145 mac = '52:54:00:00:04:{0:02x}'.format(self._vhost_id)
146 # Create Virtio network device.
147 device = ' -device virtio-net-pci,netdev=vhost{0},mac={1}'.format(
149 self._qemu_opt['options'] += device
150 # Add interface MAC and socket to the node dict
151 if_data = {'mac_address': mac, 'socket': socket}
152 if_name = 'vhost{}'.format(self._vhost_id)
153 self._vm_info['interfaces'][if_name] = if_data
154 # Add socket to the socket list
155 self._socks.append(socket)
157 def _qemu_qmp_exec(self, cmd):
158 """Execute QMP command.
160 QMP is JSON based protocol which allows to control QEMU instance.
162 :param cmd: QMP command to execute.
164 :return: Command output in python representation of JSON format. The
165 { "return": {} } response is QMP's success response. An error
166 response will contain the "error" keyword instead of "return".
168 # To enter command mode, the qmp_capabilities command must be issued.
169 qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' + \
170 '{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc -U ' + \
172 (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
173 if 0 != int(ret_code):
174 logger.debug('QMP execute failed {0}'.format(stderr))
175 raise RuntimeError('QMP execute "{0}" failed on {1}'.format(cmd,
178 # Skip capabilities negotiation messages.
179 out_list = stdout.splitlines()
180 if len(out_list) < 3:
181 raise RuntimeError('Invalid QMP output on {0}'.format(
183 return json.loads(out_list[2])
185 def _qemu_qga_exec(self, cmd):
186 """Execute QGA command.
188 QGA provide access to a system-level agent via standard QMP commands.
190 :param cmd: QGA command to execute.
193 qga_cmd = 'echo "{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc ' \
194 '-q 1 -U ' + self.__QGA_SOCK
195 (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
196 if 0 != int(ret_code):
197 logger.debug('QGA execute failed {0}'.format(stderr))
198 raise RuntimeError('QGA execute "{0}" failed on {1}'.format(cmd,
203 return json.loads(stdout)
205 def _wait_until_vm_boot(self, timeout=300):
206 """Wait until QEMU VM is booted.
208 Ping QEMU guest agent each 5s until VM booted or timeout.
210 :param timeout: Waiting timeout in seconds (optional, default 300s).
215 if time() - start > timeout:
216 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
217 self._qemu_opt['disk_image'], self._node['host']))
218 out = self._qemu_qga_exec('guest-ping')
219 # Empty output - VM not booted yet
222 # Non-error return - VM booted
223 elif out.get('return') is not None:
226 raise RuntimeError('QGA guest-ping unexpected output {}'.format(
228 logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
231 def _update_vm_interfaces(self):
232 """Update interface names in VM node dict."""
233 # Send guest-network-get-interfaces command via QGA, output example:
234 # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
235 # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
236 out = self._qemu_qga_exec('guest-network-get-interfaces')
237 interfaces = out.get('return')
240 raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
241 self._qemu_opt['disk_image'], self._node['host']))
242 # Create MAC-name dict
243 for interface in interfaces:
244 mac_name[interface['hardware-address']] = interface['name']
245 # Match interface by MAC and save interface name
246 for interface in self._vm_info['interfaces'].values():
247 mac = interface.get('mac_address')
248 if_name = mac_name.get(mac)
250 logger.trace('Interface name for MAC {} not found'.format(mac))
252 interface['name'] = if_name
254 def _huge_page_check(self):
255 """Huge page check."""
256 huge_mnt = self._qemu_opt.get('huge_mnt')
257 mem_size = self._qemu_opt.get('mem_size')
258 # Check size of free huge pages
259 (_, output, _) = self._ssh.exec_command('grep Huge /proc/meminfo')
260 regex = re.compile(r'HugePages_Free:\s+(\d+)')
261 match = regex.search(output)
262 huge_free = int(match.group(1))
263 regex = re.compile(r'Hugepagesize:\s+(\d+)')
264 match = regex.search(output)
265 huge_size = int(match.group(1))
266 if (mem_size * 1024) > (huge_free * huge_size):
267 raise RuntimeError('Not enough free huge pages {0} kB, required '
268 '{1} MB'.format(huge_free * huge_size, mem_size))
269 # Check if huge pages mount point exist
271 (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
272 for line in output.splitlines():
273 # Try to find something like:
274 # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
276 if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
279 # If huge page mount point not exist create one
281 cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
283 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
284 if int(ret_code) != 0:
285 logger.debug('Mount huge pages failed {0}'.format(stderr))
286 raise RuntimeError('Mount huge pages failed on {0}'.format(
289 def qemu_start(self):
290 """Start QEMU and wait until VM boot.
292 :return: VM node info
294 .. note:: First set at least disk image and node to run QEMU on.
297 ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
298 self._qemu_opt.get('ssh_fwd_port'))
299 # Memory and huge pages
300 mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
301 'share=on -m {0} -numa node,memdev=mem'.format(
302 self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
303 self._huge_page_check()
304 # Setup QMP via unix socket
305 qmp = '-qmp unix:{0},server,nowait'.format(self.__QMP_SOCK)
306 # Setup QGA via chardev (unix socket) and virtio-serial channel
307 qga = '-chardev socket,path=/tmp/qga.sock,server,nowait,id=qga0 ' \
308 '-device virtio-serial ' \
309 '-device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0'
311 cmd = '{0} {1} {2} {3} {4} -hda {5} {6} {7}'.format(
312 self.__QEMU_BIN, self._qemu_opt.get('smp'), mem, ssh_fwd,
313 self._qemu_opt.get('options'),
314 self._qemu_opt.get('disk_image'), qmp, qga)
315 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300)
316 if int(ret_code) != 0:
317 logger.debug('QEMU start failed {0}'.format(stderr))
318 raise RuntimeError('QEMU start failed on {0}'.format(
320 logger.trace('QEMU running')
322 self._wait_until_vm_boot()
323 # Update interface names in VM node dict
324 self._update_vm_interfaces()
325 # Return VM node dict
329 """Quit the QEMU emulator."""
330 out = self._qemu_qmp_exec('quit')
331 err = out.get('error')
333 raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
334 self._node['host'], json.dumps(err)))
336 def qemu_system_powerdown(self):
337 """Power down the system (if supported)."""
338 out = self._qemu_qmp_exec('system_powerdown')
339 err = out.get('error')
341 raise RuntimeError('QEMU system powerdown failed on {0}, '
342 'error: {1}'.format(self._node['host'], json.dumps(err)))
344 def qemu_system_reset(self):
345 """Reset the system."""
346 out = self._qemu_qmp_exec('system_reset')
347 err = out.get('error')
349 raise RuntimeError('QEMU system reset failed on {0}, '
350 'error: {1}'.format(self._node['host'], json.dumps(err)))
353 """Kill qemu process."""
354 # TODO: add PID storage so that we can kill specific PID
355 # Note: in QEMU start phase there are 3 QEMU processes because we
357 self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
359 def qemu_clear_socks(self):
360 """Remove all sockets created by QEMU."""
361 for sock in self._socks:
362 cmd = 'rm -f {}'.format(sock)
363 self._ssh.exec_command_sudo(cmd)
365 def qemu_system_status(self):
366 """Return current VM status.
371 out = self._qemu_qmp_exec('query-status')
372 ret = out.get('return')
374 return ret.get('status')
376 err = out.get('error')
377 raise RuntimeError('QEMU query-status failed on {0}, '
378 'error: {1}'.format(self._node['host'], json.dumps(err)))
381 def build_qemu(node):
382 """Build QEMU from sources.
384 :param node: Node to build QEMU on.
390 (ret_code, stdout, stderr) = \
391 ssh.exec_command('sudo -Sn bash {0}/{1}/qemu_build.sh'.format(
392 Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH), 1000)
394 if 0 != int(ret_code):
395 logger.debug('QEMU build failed {0}'.format(stderr))
396 raise RuntimeError('QEMU build failed on {0}'.format(node['host']))