Add temporary fix for CSIT-90 by flushing QGA's input buffer
[csit.git] / resources / libraries / python / QemuUtils.py
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:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
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.
13
14 """QEMU utilities library."""
15
16 from time import time, sleep
17 import json
18 import re
19
20 from robot.api import logger
21
22 from resources.libraries.python.ssh import SSH
23 from resources.libraries.python.constants import Constants
24 from resources.libraries.python.topology import NodeType
25
26
27 class QemuUtils(object):
28     """QEMU utilities."""
29
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'
35
36     def __init__(self):
37         self._qemu_opt = {}
38         # Default 1 CPU.
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'
54         # VM node info dict
55         self._vm_info = {
56             'type': NodeType.VM,
57             'port': 10022,
58             'username': 'cisco',
59             'password': 'cisco',
60             'interfaces': {},
61         }
62         self._vhost_id = 0
63         self._ssh = None
64         self._node = None
65         self._socks = [self.__QMP_SOCK, self.__QGA_SOCK]
66
67     def qemu_set_smp(self, cpus, cores, threads, sockets):
68         """Set SMP option for QEMU
69
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.
74         :type cpus: int
75         :type cores: int
76         :type threads: int
77         :type sockets: int
78         """
79         self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
80             cpus, cores, threads, sockets)
81
82     def qemu_set_ssh_fwd_port(self, fwd_port):
83         """Set host port for guest SSH forwarding.
84
85         :param fwd_port: Port number on host for guest SSH forwarding.
86         :type fwd_port: int
87         """
88         self._qemu_opt['ssh_fwd_port'] = fwd_port
89         self._vm_info['port'] = fwd_port
90
91     def qemu_set_serial_port(self, port):
92         """Set serial console port.
93
94         :param port: Serial console port.
95         :type port: int
96         """
97         self._qemu_opt['serial_port'] = port
98
99     def qemu_set_mem_size(self, mem_size):
100         """Set virtual RAM size.
101
102         :param mem_size: RAM size in Mega Bytes.
103         :type mem_size: int
104         """
105         self._qemu_opt['mem_size'] = mem_size
106
107     def qemu_set_huge_mnt(self, huge_mnt):
108         """Set hugefile mount point.
109
110         :param huge_mnt: System hugefile mount point.
111         :type huge_mnt: int
112         """
113         self._qemu_opt['huge_mnt'] = huge_mnt
114
115     def qemu_set_disk_image(self, disk_image):
116         """Set disk image.
117
118         :param disk_image: Path of the disk image.
119         :type disk_image: str
120         """
121         self._qemu_opt['disk_image'] = disk_image
122
123     def qemu_set_node(self, node):
124         """Set node to run QEMU on.
125
126         :param node: Node to run QEMU on.
127         :type node: dict
128         """
129         self._node = node
130         self._ssh = SSH()
131         self._ssh.connect(node)
132         self._vm_info['host'] = node['host']
133
134     def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
135         """Add Vhost-user interface.
136
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).
141         :type socket: str
142         :type server: bool
143         :type mac: str
144         """
145         self._vhost_id += 1
146         # Create unix socket character device.
147         chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
148                                                                 socket)
149         if server is True:
150             chardev += ',server'
151         self._qemu_opt['options'] += chardev
152         # Create Vhost-user network backend.
153         netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0}'.format(
154             self._vhost_id)
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
158         if mac is None:
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(
162             self._vhost_id, mac)
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)
170
171     def _qemu_qmp_exec(self, cmd):
172         """Execute QMP command.
173
174         QMP is JSON based protocol which allows to control QEMU instance.
175
176         :param cmd: QMP command to execute.
177         :type cmd: str
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".
181         """
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 ' + \
185             self.__QMP_SOCK
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,
190                 self._node['host']))
191         logger.trace(stdout)
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(
196                 self._node['host']))
197         return json.loads(out_list[2])
198
199     def _qemu_qga_flush(self):
200         """Flush the QGA parser state
201         """
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,
208                 self._node['host']))
209         logger.trace(stdout)
210         if not stdout:
211             return {}
212         return json.loads(stdout.split('\n', 1)[0])
213
214     def _qemu_qga_exec(self, cmd):
215         """Execute QGA command.
216
217         QGA provide access to a system-level agent via standard QMP commands.
218
219         :param cmd: QGA command to execute.
220         :type cmd: str
221         """
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,
228                 self._node['host']))
229         logger.trace(stdout)
230         if not stdout:
231             return {}
232         return json.loads(stdout.split('\n', 1)[0])
233
234     def _wait_until_vm_boot(self, timeout=300):
235         """Wait until QEMU VM is booted.
236
237         Ping QEMU guest agent each 5s until VM booted or timeout.
238
239         :param timeout: Waiting timeout in seconds (optional, default 300s).
240         :type timeout: int
241         """
242         start = time()
243         while 1:
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
250             if not out:
251                 sleep(5)
252             # Non-error return - VM booted
253             elif out.get('return') is not None:
254                 break
255             # Skip error and wait
256             elif out.get('error') is not None:
257                 sleep(5)
258             else:
259                 raise RuntimeError('QGA guest-ping unexpected output {}'.format(
260                     out))
261         logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
262                                                    self._node['host']))
263
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')
271         mac_name = {}
272         if not interfaces:
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:
278                 continue
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)
284             if if_name is None:
285                 logger.trace('Interface name for MAC {} not found'.format(mac))
286             else:
287                 interface['name'] = if_name
288
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
305         has_huge_mnt = False
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
310             mount = line.split()
311             if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
312                 has_huge_mnt = True
313                 break
314         # If huge page mount point not exist create one
315         if not has_huge_mnt:
316             cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
317                 huge_mnt)
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(
322                     self._node['host']))
323
324     def qemu_start(self):
325         """Start QEMU and wait until VM boot.
326
327         :return: VM node info.
328         :rtype: dict
329         .. note:: First set at least node to run QEMU on.
330         .. warning:: Starts only one VM on the node.
331         """
332         # SSH forwarding
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'
349         # Graphic setup
350         graphic = '-monitor none -display none -vga none'
351         # Run QEMU
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(
360                 self._node['host']))
361         logger.trace('QEMU running')
362         # Wait until VM boot
363         self._wait_until_vm_boot()
364         # Update interface names in VM node dict
365         self._update_vm_interfaces()
366         # Return VM node dict
367         return self._vm_info
368
369     def qemu_quit(self):
370         """Quit the QEMU emulator."""
371         out = self._qemu_qmp_exec('quit')
372         err = out.get('error')
373         if err is not None:
374             raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
375                 self._node['host'], json.dumps(err)))
376
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')
381         if err is not None:
382             raise RuntimeError('QEMU system powerdown failed on {0}, '
383                 'error: {1}'.format(self._node['host'], json.dumps(err)))
384
385     def qemu_system_reset(self):
386         """Reset the system."""
387         out = self._qemu_qmp_exec('system_reset')
388         err = out.get('error')
389         if err is not None:
390             raise RuntimeError('QEMU system reset failed on {0}, '
391                 'error: {1}'.format(self._node['host'], json.dumps(err)))
392
393     def qemu_kill(self):
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
397         # daemonize QEMU
398         self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
399
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)
409
410     def qemu_system_status(self):
411         """Return current VM status.
412
413         VM should be in following status:
414
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
420             - paused: paused
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
430
431         :return: VM status.
432         :rtype: str
433         """
434         out = self._qemu_qmp_exec('query-status')
435         ret = out.get('return')
436         if ret is not None:
437             return ret.get('status')
438         else:
439             err = out.get('error')
440             raise RuntimeError('QEMU query-status failed on {0}, '
441                 'error: {1}'.format(self._node['host'], json.dumps(err)))
442
443     @staticmethod
444     def build_qemu(node):
445         """Build QEMU from sources.
446
447         :param node: Node to build QEMU on.
448         :type node: dict
449         """
450         ssh = SSH()
451         ssh.connect(node)
452
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)
456         logger.trace(stdout)
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']))