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