CSIT-203: Expand LISP test
[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'] = '-cpu host -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 -balloon none'
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         # Affinity of qemu processes
55         self._qemu_opt['affinity'] = False
56         # VM node info dict
57         self._vm_info = {
58             'type': NodeType.VM,
59             'port': 10022,
60             'username': 'cisco',
61             'password': 'cisco',
62             'interfaces': {},
63         }
64         self._vhost_id = 0
65         self._ssh = None
66         self._node = None
67         self._socks = [self.__QMP_SOCK, self.__QGA_SOCK]
68
69     def qemu_set_smp(self, cpus, cores, threads, sockets):
70         """Set SMP option for QEMU
71
72         :param cpus: Number of CPUs.
73         :param cores: Number of CPU cores on one socket.
74         :param threads: Number of threads on one CPU core.
75         :param sockets: Number of discrete sockets in the system.
76         :type cpus: int
77         :type cores: int
78         :type threads: int
79         :type sockets: int
80         """
81         self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
82             cpus, cores, threads, sockets)
83
84     def qemu_set_ssh_fwd_port(self, fwd_port):
85         """Set host port for guest SSH forwarding.
86
87         :param fwd_port: Port number on host for guest SSH forwarding.
88         :type fwd_port: int
89         """
90         self._qemu_opt['ssh_fwd_port'] = fwd_port
91         self._vm_info['port'] = fwd_port
92
93     def qemu_set_serial_port(self, port):
94         """Set serial console port.
95
96         :param port: Serial console port.
97         :type port: int
98         """
99         self._qemu_opt['serial_port'] = port
100
101     def qemu_set_mem_size(self, mem_size):
102         """Set virtual RAM size.
103
104         :param mem_size: RAM size in Mega Bytes.
105         :type mem_size: int
106         """
107         self._qemu_opt['mem_size'] = int(mem_size)
108
109     def qemu_set_huge_mnt(self, huge_mnt):
110         """Set hugefile mount point.
111
112         :param huge_mnt: System hugefile mount point.
113         :type huge_mnt: int
114         """
115         self._qemu_opt['huge_mnt'] = huge_mnt
116
117     def qemu_set_disk_image(self, disk_image):
118         """Set disk image.
119
120         :param disk_image: Path of the disk image.
121         :type disk_image: str
122         """
123         self._qemu_opt['disk_image'] = disk_image
124
125     def qemu_set_affinity(self, mask):
126         """Set qemu affinity by taskset with cpu mask.
127
128        :param mask: Hex CPU mask.
129        :type mask: str
130         """
131         self._qemu_opt['affinity'] = mask
132
133     def qemu_set_node(self, node):
134         """Set node to run QEMU on.
135
136         :param node: Node to run QEMU on.
137         :type node: dict
138         """
139         self._node = node
140         self._ssh = SSH()
141         self._ssh.connect(node)
142         self._vm_info['host'] = node['host']
143
144     def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
145         """Add Vhost-user interface.
146
147         :param socket: Path of the unix socket.
148         :param server: If True the socket shall be a listening socket.
149         :param mac: Vhost-user interface MAC address (optional, otherwise is
150             used autogenerated MAC 52:54:00:00:04:xx).
151         :type socket: str
152         :type server: bool
153         :type mac: str
154         """
155         self._vhost_id += 1
156         # Create unix socket character device.
157         chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
158                                                                 socket)
159         if server is True:
160             chardev += ',server'
161         self._qemu_opt['options'] += chardev
162         # Create Vhost-user network backend.
163         netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0}'.format(
164             self._vhost_id)
165         self._qemu_opt['options'] += netdev
166         # If MAC is not specified use autogenerated 52:54:00:00:04:<vhost_id>
167         # e.g. vhost1 MAC is 52:54:00:00:04:01
168         if mac is None:
169             mac = '52:54:00:00:04:{0:02x}'.format(self._vhost_id)
170         extend_options = 'csum=off,gso=off,guest_tso4=off,guest_tso6=off,'\
171             'guest_ecn=off,mrg_rxbuf=off'
172         # Create Virtio network device.
173         device = ' -device virtio-net-pci,netdev=vhost{0},mac={1},{2}'.format(
174             self._vhost_id, mac, extend_options)
175         self._qemu_opt['options'] += device
176         # Add interface MAC and socket to the node dict
177         if_data = {'mac_address': mac, 'socket': socket}
178         if_name = 'vhost{}'.format(self._vhost_id)
179         self._vm_info['interfaces'][if_name] = if_data
180         # Add socket to the socket list
181         self._socks.append(socket)
182
183     def _qemu_qmp_exec(self, cmd):
184         """Execute QMP command.
185
186         QMP is JSON based protocol which allows to control QEMU instance.
187
188         :param cmd: QMP command to execute.
189         :type cmd: str
190         :return: Command output in python representation of JSON format. The
191             { "return": {} } response is QMP's success response. An error
192             response will contain the "error" keyword instead of "return".
193         """
194         # To enter command mode, the qmp_capabilities command must be issued.
195         qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' + \
196             '{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc -U ' + \
197             self.__QMP_SOCK
198         (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
199         if 0 != int(ret_code):
200             logger.debug('QMP execute failed {0}'.format(stderr))
201             raise RuntimeError('QMP execute "{0}" failed on {1}'.format(cmd,
202                 self._node['host']))
203         logger.trace(stdout)
204         # Skip capabilities negotiation messages.
205         out_list = stdout.splitlines()
206         if len(out_list) < 3:
207             raise RuntimeError('Invalid QMP output on {0}'.format(
208                 self._node['host']))
209         return json.loads(out_list[2])
210
211     def _qemu_qga_flush(self):
212         """Flush the QGA parser state
213         """
214         qga_cmd = 'printf "\xFF" | sudo -S nc ' \
215             '-q 1 -U ' + self.__QGA_SOCK
216         (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
217         if 0 != int(ret_code):
218             logger.debug('QGA execute failed {0}'.format(stderr))
219             raise RuntimeError('QGA execute "{0}" failed on {1}'.format(cmd,
220                 self._node['host']))
221         logger.trace(stdout)
222         if not stdout:
223             return {}
224         return json.loads(stdout.split('\n', 1)[0])
225
226     def _qemu_qga_exec(self, cmd):
227         """Execute QGA command.
228
229         QGA provide access to a system-level agent via standard QMP commands.
230
231         :param cmd: QGA command to execute.
232         :type cmd: str
233         """
234         qga_cmd = 'echo "{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc ' \
235             '-q 1 -U ' + self.__QGA_SOCK
236         (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
237         if 0 != int(ret_code):
238             logger.debug('QGA execute failed {0}'.format(stderr))
239             raise RuntimeError('QGA execute "{0}" failed on {1}'.format(cmd,
240                 self._node['host']))
241         logger.trace(stdout)
242         if not stdout:
243             return {}
244         return json.loads(stdout.split('\n', 1)[0])
245
246     def _wait_until_vm_boot(self, timeout=300):
247         """Wait until QEMU VM is booted.
248
249         Ping QEMU guest agent each 5s until VM booted or timeout.
250
251         :param timeout: Waiting timeout in seconds (optional, default 300s).
252         :type timeout: int
253         """
254         start = time()
255         while 1:
256             if time() - start > timeout:
257                 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
258                     self._qemu_opt['disk_image'], self._node['host']))
259             self._qemu_qga_flush()
260             out = self._qemu_qga_exec('guest-ping')
261             # Empty output - VM not booted yet
262             if not out:
263                 sleep(5)
264             # Non-error return - VM booted
265             elif out.get('return') is not None:
266                 break
267             # Skip error and wait
268             elif out.get('error') is not None:
269                 sleep(5)
270             else:
271                 raise RuntimeError('QGA guest-ping unexpected output {}'.format(
272                     out))
273         logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
274                                                    self._node['host']))
275
276     def _update_vm_interfaces(self):
277         """Update interface names in VM node dict."""
278         # Send guest-network-get-interfaces command via QGA, output example:
279         # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
280         # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
281         out = self._qemu_qga_exec('guest-network-get-interfaces')
282         interfaces = out.get('return')
283         mac_name = {}
284         if not interfaces:
285             raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
286                 self._qemu_opt['disk_image'], self._node['host']))
287         # Create MAC-name dict
288         for interface in interfaces:
289             if 'hardware-address' not in interface:
290                 continue
291             mac_name[interface['hardware-address']] = interface['name']
292         # Match interface by MAC and save interface name
293         for interface in self._vm_info['interfaces'].values():
294             mac = interface.get('mac_address')
295             if_name = mac_name.get(mac)
296             if if_name is None:
297                 logger.trace('Interface name for MAC {} not found'.format(mac))
298             else:
299                 interface['name'] = if_name
300
301     def _huge_page_check(self):
302         """Huge page check."""
303         huge_mnt = self._qemu_opt.get('huge_mnt')
304         mem_size = self._qemu_opt.get('mem_size')
305         # Check size of free huge pages
306         (_, output, _) = self._ssh.exec_command('grep Huge /proc/meminfo')
307         regex = re.compile(r'HugePages_Free:\s+(\d+)')
308         match = regex.search(output)
309         huge_free = int(match.group(1))
310         regex = re.compile(r'Hugepagesize:\s+(\d+)')
311         match = regex.search(output)
312         huge_size = int(match.group(1))
313         if (mem_size * 1024) > (huge_free * huge_size):
314             raise RuntimeError('Not enough free huge pages {0} kB, required '
315                 '{1} MB'.format(huge_free * huge_size, mem_size))
316         # Check if huge pages mount point exist
317         has_huge_mnt = False
318         (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
319         for line in output.splitlines():
320             # Try to find something like:
321             # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
322             mount = line.split()
323             if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
324                 has_huge_mnt = True
325                 break
326         # If huge page mount point not exist create one
327         if not has_huge_mnt:
328             cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
329                 huge_mnt)
330             (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
331             if int(ret_code) != 0:
332                 logger.debug('Mount huge pages failed {0}'.format(stderr))
333                 raise RuntimeError('Mount huge pages failed on {0}'.format(
334                     self._node['host']))
335
336     def qemu_start(self):
337         """Start QEMU and wait until VM boot.
338
339         :return: VM node info.
340         :rtype: dict
341         .. note:: First set at least node to run QEMU on.
342         .. warning:: Starts only one VM on the node.
343         """
344         # SSH forwarding
345         ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
346             self._qemu_opt.get('ssh_fwd_port'))
347         # Memory and huge pages
348         mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
349             'share=on -m {0} -numa node,memdev=mem'.format(
350             self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
351         self._huge_page_check()
352         # Setup QMP via unix socket
353         qmp = '-qmp unix:{0},server,nowait'.format(self.__QMP_SOCK)
354         # Setup serial console
355         serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \
356             'nowait -device isa-serial,chardev=gnc0'.format(
357             self._qemu_opt.get('serial_port'))
358         # Setup QGA via chardev (unix socket) and isa-serial channel
359         qga = '-chardev socket,path=/tmp/qga.sock,server,nowait,id=qga0 ' \
360             '-device isa-serial,chardev=qga0'
361         # Graphic setup
362         graphic = '-monitor none -display none -vga none'
363         qbin = 'taskset {0} {1}'.format(self._qemu_opt.get('affinity'),
364             self.__QEMU_BIN) if self._qemu_opt.get(
365             'affinity') else self.__QEMU_BIN
366         # Run QEMU
367         cmd = '{0} {1} {2} {3} {4} -hda {5} {6} {7} {8} {9}'.format(
368             qbin, self._qemu_opt.get('smp'), mem, ssh_fwd,
369             self._qemu_opt.get('options'),
370             self._qemu_opt.get('disk_image'), qmp, serial, qga, graphic)
371         (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300)
372         if int(ret_code) != 0:
373             logger.debug('QEMU start failed {0}'.format(stderr))
374             raise RuntimeError('QEMU start failed on {0}'.format(
375                 self._node['host']))
376         logger.trace('QEMU running')
377         # Wait until VM boot
378         self._wait_until_vm_boot()
379         # Update interface names in VM node dict
380         self._update_vm_interfaces()
381         # Return VM node dict
382         return self._vm_info
383
384     def qemu_quit(self):
385         """Quit the QEMU emulator."""
386         out = self._qemu_qmp_exec('quit')
387         err = out.get('error')
388         if err is not None:
389             raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
390                 self._node['host'], json.dumps(err)))
391
392     def qemu_system_powerdown(self):
393         """Power down the system (if supported)."""
394         out = self._qemu_qmp_exec('system_powerdown')
395         err = out.get('error')
396         if err is not None:
397             raise RuntimeError('QEMU system powerdown failed on {0}, '
398                 'error: {1}'.format(self._node['host'], json.dumps(err)))
399
400     def qemu_system_reset(self):
401         """Reset the system."""
402         out = self._qemu_qmp_exec('system_reset')
403         err = out.get('error')
404         if err is not None:
405             raise RuntimeError('QEMU system reset failed on {0}, '
406                 'error: {1}'.format(self._node['host'], json.dumps(err)))
407
408     def qemu_kill(self):
409         """Kill qemu process."""
410         # TODO: add PID storage so that we can kill specific PID
411         # Note: in QEMU start phase there are 3 QEMU processes because we
412         # daemonize QEMU
413         self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
414
415     def qemu_clear_socks(self):
416         """Remove all sockets created by QEMU."""
417         # If serial console port still open kill process
418         cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port'))
419         self._ssh.exec_command_sudo(cmd)
420         # Delete all created sockets
421         for sock in self._socks:
422             cmd = 'rm -f {}'.format(sock)
423             self._ssh.exec_command_sudo(cmd)
424
425     def qemu_system_status(self):
426         """Return current VM status.
427
428         VM should be in following status:
429
430             - debug: QEMU running on a debugger
431             - finish-migrate: paused to finish the migration process
432             - inmigrate: waiting for an incoming migration
433             - internal-error: internal error has occurred
434             - io-error: the last IOP has failed
435             - paused: paused
436             - postmigrate: paused following a successful migrate
437             - prelaunch: QEMU was started with -S and guest has not started
438             - restore-vm: paused to restore VM state
439             - running: actively running
440             - save-vm: paused to save the VM state
441             - shutdown: shut down (and -no-shutdown is in use)
442             - suspended: suspended (ACPI S3)
443             - watchdog: watchdog action has been triggered
444             - guest-panicked: panicked as a result of guest OS panic
445
446         :return: VM status.
447         :rtype: str
448         """
449         out = self._qemu_qmp_exec('query-status')
450         ret = out.get('return')
451         if ret is not None:
452             return ret.get('status')
453         else:
454             err = out.get('error')
455             raise RuntimeError('QEMU query-status failed on {0}, '
456                 'error: {1}'.format(self._node['host'], json.dumps(err)))
457
458     @staticmethod
459     def build_qemu(node):
460         """Build QEMU from sources.
461
462         :param node: Node to build QEMU on.
463         :type node: dict
464         """
465         ssh = SSH()
466         ssh.connect(node)
467
468         (ret_code, stdout, stderr) = \
469             ssh.exec_command('sudo -Sn bash {0}/{1}/qemu_build.sh'.format(
470                 Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH), 1000)
471         logger.trace(stdout)
472         if 0 != int(ret_code):
473             logger.debug('QEMU build failed {0}'.format(stderr))
474             raise RuntimeError('QEMU build failed on {0}'.format(node['host']))