Fix pylint warnings in python libraries
[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 do not allocate huge pages.
53         self._qemu_opt['huge_allocate'] = False
54         # Default image for CSIT virl setup
55         self._qemu_opt['disk_image'] = '/var/lib/vm/vhost-nested.img'
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_huge_allocate(self):
118         """Set flag to allocate more huge pages if needed."""
119         self._qemu_opt['huge_allocate'] = True
120
121     def qemu_set_disk_image(self, disk_image):
122         """Set disk image.
123
124         :param disk_image: Path of the disk image.
125         :type disk_image: str
126         """
127         self._qemu_opt['disk_image'] = disk_image
128
129     def qemu_set_affinity(self, *host_cpus):
130         """Set qemu affinity by getting thread PIDs via QMP and taskset to list
131         of CPU cores.
132
133         :param host_cpus: List of CPU cores.
134         :type host_cpus: list
135         """
136         qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
137
138         if len(qemu_cpus) != len(host_cpus):
139             logger.debug('Host CPU count {0}, Qemu Thread count {1}'.format(
140                 len(host_cpus), len(qemu_cpus)))
141             raise ValueError('Host CPU count must match Qemu Thread count')
142
143         for qemu_cpu, host_cpu in zip(qemu_cpus, host_cpus):
144             cmd = 'taskset -p {0} {1}'.format(hex(1 << int(host_cpu)),
145                                               qemu_cpu['thread_id'])
146             (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
147             if int(ret_code) != 0:
148                 logger.debug('Set affinity failed {0}'.format(stderr))
149                 raise RuntimeError('Set affinity failed on {0}'.format(
150                     self._node['host']))
151
152     def qemu_set_node(self, node):
153         """Set node to run QEMU on.
154
155         :param node: Node to run QEMU on.
156         :type node: dict
157         """
158         self._node = node
159         self._ssh = SSH()
160         self._ssh.connect(node)
161         self._vm_info['host'] = node['host']
162
163     def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
164         """Add Vhost-user interface.
165
166         :param socket: Path of the unix socket.
167         :param server: If True the socket shall be a listening socket.
168         :param mac: Vhost-user interface MAC address (optional, otherwise is
169             used autogenerated MAC 52:54:00:00:04:xx).
170         :type socket: str
171         :type server: bool
172         :type mac: str
173         """
174         self._vhost_id += 1
175         # Create unix socket character device.
176         chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
177                                                                 socket)
178         if server is True:
179             chardev += ',server'
180         self._qemu_opt['options'] += chardev
181         # Create Vhost-user network backend.
182         netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0}'.format(
183             self._vhost_id)
184         self._qemu_opt['options'] += netdev
185         # If MAC is not specified use autogenerated 52:54:00:00:04:<vhost_id>
186         # e.g. vhost1 MAC is 52:54:00:00:04:01
187         if mac is None:
188             mac = '52:54:00:00:04:{0:02x}'.format(self._vhost_id)
189         extend_options = 'csum=off,gso=off,guest_tso4=off,guest_tso6=off,'\
190             'guest_ecn=off,mrg_rxbuf=off'
191         # Create Virtio network device.
192         device = ' -device virtio-net-pci,netdev=vhost{0},mac={1},{2}'.format(
193             self._vhost_id, mac, extend_options)
194         self._qemu_opt['options'] += device
195         # Add interface MAC and socket to the node dict
196         if_data = {'mac_address': mac, 'socket': socket}
197         if_name = 'vhost{}'.format(self._vhost_id)
198         self._vm_info['interfaces'][if_name] = if_data
199         # Add socket to the socket list
200         self._socks.append(socket)
201
202     def _qemu_qmp_exec(self, cmd):
203         """Execute QMP command.
204
205         QMP is JSON based protocol which allows to control QEMU instance.
206
207         :param cmd: QMP command to execute.
208         :type cmd: str
209         :return: Command output in python representation of JSON format. The
210             { "return": {} } response is QMP's success response. An error
211             response will contain the "error" keyword instead of "return".
212         """
213         # To enter command mode, the qmp_capabilities command must be issued.
214         qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' + \
215             '{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc -U ' + \
216             self.__QMP_SOCK
217         (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
218         if int(ret_code) != 0:
219             logger.debug('QMP execute failed {0}'.format(stderr))
220             raise RuntimeError('QMP execute "{0}"'
221                                ' failed on {1}'.format(cmd, self._node['host']))
222         logger.trace(stdout)
223         # Skip capabilities negotiation messages.
224         out_list = stdout.splitlines()
225         if len(out_list) < 3:
226             raise RuntimeError('Invalid QMP output on {0}'.format(
227                 self._node['host']))
228         return json.loads(out_list[2])
229
230     def _qemu_qga_flush(self):
231         """Flush the QGA parser state
232         """
233         qga_cmd = 'printf "\xFF" | sudo -S nc ' \
234             '-q 1 -U ' + self.__QGA_SOCK
235         (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
236         if int(ret_code) != 0:
237             logger.debug('QGA execute failed {0}'.format(stderr))
238             raise RuntimeError('QGA execute "{0}" '
239                                'failed on {1}'.format(cmd, self._node['host']))
240         logger.trace(stdout)
241         if not stdout:
242             return {}
243         return json.loads(stdout.split('\n', 1)[0])
244
245     def _qemu_qga_exec(self, cmd):
246         """Execute QGA command.
247
248         QGA provide access to a system-level agent via standard QMP commands.
249
250         :param cmd: QGA command to execute.
251         :type cmd: str
252         """
253         qga_cmd = 'echo "{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc ' \
254             '-q 1 -U ' + self.__QGA_SOCK
255         (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
256         if int(ret_code) != 0:
257             logger.debug('QGA execute failed {0}'.format(stderr))
258             raise RuntimeError('QGA execute "{0}"'
259                                ' failed on {1}'.format(cmd, self._node['host']))
260         logger.trace(stdout)
261         if not stdout:
262             return {}
263         return json.loads(stdout.split('\n', 1)[0])
264
265     def _wait_until_vm_boot(self, timeout=300):
266         """Wait until QEMU VM is booted.
267
268         Ping QEMU guest agent each 5s until VM booted or timeout.
269
270         :param timeout: Waiting timeout in seconds (optional, default 300s).
271         :type timeout: int
272         """
273         start = time()
274         while 1:
275             if time() - start > timeout:
276                 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
277                     self._qemu_opt['disk_image'], self._node['host']))
278             self._qemu_qga_flush()
279             out = self._qemu_qga_exec('guest-ping')
280             # Empty output - VM not booted yet
281             if not out:
282                 sleep(5)
283             # Non-error return - VM booted
284             elif out.get('return') is not None:
285                 break
286             # Skip error and wait
287             elif out.get('error') is not None:
288                 sleep(5)
289             else:
290                 raise RuntimeError('QGA guest-ping unexpected output {}'.format(
291                     out))
292         logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
293                                                    self._node['host']))
294
295     def _update_vm_interfaces(self):
296         """Update interface names in VM node dict."""
297         # Send guest-network-get-interfaces command via QGA, output example:
298         # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
299         # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
300         out = self._qemu_qga_exec('guest-network-get-interfaces')
301         interfaces = out.get('return')
302         mac_name = {}
303         if not interfaces:
304             raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
305                 self._qemu_opt['disk_image'], self._node['host']))
306         # Create MAC-name dict
307         for interface in interfaces:
308             if 'hardware-address' not in interface:
309                 continue
310             mac_name[interface['hardware-address']] = interface['name']
311         # Match interface by MAC and save interface name
312         for interface in self._vm_info['interfaces'].values():
313             mac = interface.get('mac_address')
314             if_name = mac_name.get(mac)
315             if if_name is None:
316                 logger.trace('Interface name for MAC {} not found'.format(mac))
317             else:
318                 interface['name'] = if_name
319
320     def _huge_page_check(self, allocate=False):
321         """Huge page check."""
322         huge_mnt = self._qemu_opt.get('huge_mnt')
323         mem_size = self._qemu_opt.get('mem_size')
324         # Check size of free huge pages
325         (_, output, _) = self._ssh.exec_command('grep Huge /proc/meminfo')
326         regex = re.compile(r'HugePages_Free:\s+(\d+)')
327         match = regex.search(output)
328         huge_free = int(match.group(1))
329         regex = re.compile(r'HugePages_Total:\s+(\d+)')
330         match = regex.search(output)
331         huge_total = int(match.group(1))
332         regex = re.compile(r'Hugepagesize:\s+(\d+)')
333         match = regex.search(output)
334         huge_size = int(match.group(1))
335         # Check if memory reqested by qemu is available on host
336         if (mem_size * 1024) > (huge_free * huge_size):
337             # If we want to allocate hugepage dynamically
338             if allocate:
339                 mem_needed = abs((huge_free * huge_size) - (mem_size * 1024))
340                 huge_to_allocate = ((mem_needed / huge_size) * 2) + huge_total
341                 max_map_count = huge_to_allocate*4
342                 # Increase maximum number of memory map areas a process may have
343                 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/max_map_count'.format(
344                     max_map_count)
345                 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
346                 # Increase hugepage count
347                 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/nr_hugepages'.format(
348                     huge_to_allocate)
349                 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
350                 if int(ret_code) != 0:
351                     logger.debug('Mount huge pages failed {0}'.format(stderr))
352                     raise RuntimeError('Mount huge pages failed on {0}'.format(
353                         self._node['host']))
354             # If we do not want to allocate dynamicaly end with error
355             else:
356                 raise RuntimeError(
357                     'Not enough free huge pages: {0}, '
358                     '{1} MB'.format(huge_free, huge_free * huge_size)
359                 )
360         # Check if huge pages mount point exist
361         has_huge_mnt = False
362         (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
363         for line in output.splitlines():
364             # Try to find something like:
365             # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
366             mount = line.split()
367             if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
368                 has_huge_mnt = True
369                 break
370         # If huge page mount point not exist create one
371         if not has_huge_mnt:
372             cmd = 'mkdir -p {0}'.format(huge_mnt)
373             (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
374             if int(ret_code) != 0:
375                 logger.debug('Create mount dir failed: {0}'.format(stderr))
376                 raise RuntimeError('Create mount dir failed on {0}'.format(
377                     self._node['host']))
378             cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
379                 huge_mnt)
380             (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
381             if int(ret_code) != 0:
382                 logger.debug('Mount huge pages failed {0}'.format(stderr))
383                 raise RuntimeError('Mount huge pages failed on {0}'.format(
384                     self._node['host']))
385
386     def qemu_start(self):
387         """Start QEMU and wait until VM boot.
388
389         :return: VM node info.
390         :rtype: dict
391         .. note:: First set at least node to run QEMU on.
392         .. warning:: Starts only one VM on the node.
393         """
394         # SSH forwarding
395         ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
396             self._qemu_opt.get('ssh_fwd_port'))
397         # Memory and huge pages
398         mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
399             'share=on -m {0} -numa node,memdev=mem'.format(
400                 self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
401
402         # By default check only if hugepages are availbale.
403         # If 'huge_allocate' is set to true try to allocate as well.
404         self._huge_page_check(allocate=self._qemu_opt.get('huge_allocate'))
405
406         # Setup QMP via unix socket
407         qmp = '-qmp unix:{0},server,nowait'.format(self.__QMP_SOCK)
408         # Setup serial console
409         serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \
410             'nowait -device isa-serial,chardev=gnc0'.format(
411                 self._qemu_opt.get('serial_port'))
412         # Setup QGA via chardev (unix socket) and isa-serial channel
413         qga = '-chardev socket,path=/tmp/qga.sock,server,nowait,id=qga0 ' \
414             '-device isa-serial,chardev=qga0'
415         # Graphic setup
416         graphic = '-monitor none -display none -vga none'
417         # Run QEMU
418         cmd = '{0} {1} {2} {3} {4} -hda {5} {6} {7} {8} {9}'.format(
419             self.__QEMU_BIN, self._qemu_opt.get('smp'), mem, ssh_fwd,
420             self._qemu_opt.get('options'),
421             self._qemu_opt.get('disk_image'), qmp, serial, qga, graphic)
422         (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300)
423         if int(ret_code) != 0:
424             logger.debug('QEMU start failed {0}'.format(stderr))
425             raise RuntimeError('QEMU start failed on {0}'.format(
426                 self._node['host']))
427         logger.trace('QEMU running')
428         # Wait until VM boot
429         self._wait_until_vm_boot()
430         # Update interface names in VM node dict
431         self._update_vm_interfaces()
432         # Return VM node dict
433         return self._vm_info
434
435     def qemu_quit(self):
436         """Quit the QEMU emulator."""
437         out = self._qemu_qmp_exec('quit')
438         err = out.get('error')
439         if err is not None:
440             raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
441                 self._node['host'], json.dumps(err)))
442
443     def qemu_system_powerdown(self):
444         """Power down the system (if supported)."""
445         out = self._qemu_qmp_exec('system_powerdown')
446         err = out.get('error')
447         if err is not None:
448             raise RuntimeError(
449                 'QEMU system powerdown failed on {0}, '
450                 'error: {1}'.format(self._node['host'], json.dumps(err))
451             )
452
453     def qemu_system_reset(self):
454         """Reset the system."""
455         out = self._qemu_qmp_exec('system_reset')
456         err = out.get('error')
457         if err is not None:
458             raise RuntimeError(
459                 'QEMU system reset failed on {0}, '
460                 'error: {1}'.format(self._node['host'], json.dumps(err)))
461
462     def qemu_kill(self):
463         """Kill qemu process."""
464         # TODO: add PID storage so that we can kill specific PID
465         # Note: in QEMU start phase there are 3 QEMU processes because we
466         # daemonize QEMU
467         self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
468
469     def qemu_clear_socks(self):
470         """Remove all sockets created by QEMU."""
471         # If serial console port still open kill process
472         cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port'))
473         self._ssh.exec_command_sudo(cmd)
474         # Delete all created sockets
475         for sock in self._socks:
476             cmd = 'rm -f {}'.format(sock)
477             self._ssh.exec_command_sudo(cmd)
478
479     def qemu_system_status(self):
480         """Return current VM status.
481
482         VM should be in following status:
483
484             - debug: QEMU running on a debugger
485             - finish-migrate: paused to finish the migration process
486             - inmigrate: waiting for an incoming migration
487             - internal-error: internal error has occurred
488             - io-error: the last IOP has failed
489             - paused: paused
490             - postmigrate: paused following a successful migrate
491             - prelaunch: QEMU was started with -S and guest has not started
492             - restore-vm: paused to restore VM state
493             - running: actively running
494             - save-vm: paused to save the VM state
495             - shutdown: shut down (and -no-shutdown is in use)
496             - suspended: suspended (ACPI S3)
497             - watchdog: watchdog action has been triggered
498             - guest-panicked: panicked as a result of guest OS panic
499
500         :return: VM status.
501         :rtype: str
502         """
503         out = self._qemu_qmp_exec('query-status')
504         ret = out.get('return')
505         if ret is not None:
506             return ret.get('status')
507         else:
508             err = out.get('error')
509             raise RuntimeError(
510                 'QEMU query-status failed on {0}, '
511                 'error: {1}'.format(self._node['host'], json.dumps(err)))
512
513     @staticmethod
514     def build_qemu(node):
515         """Build QEMU from sources.
516
517         :param node: Node to build QEMU on.
518         :type node: dict
519         """
520         ssh = SSH()
521         ssh.connect(node)
522
523         (ret_code, stdout, stderr) = \
524             ssh.exec_command('sudo -Sn bash {0}/{1}/qemu_build.sh'.format(
525                 Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH), 1000)
526         logger.trace(stdout)
527         if int(ret_code) != 0:
528             logger.debug('QEMU build failed {0}'.format(stderr))
529             raise RuntimeError('QEMU build failed on {0}'.format(node['host']))