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