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