CSIT-494 Add multi-queue support for Qemu
[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
19 from robot.api import logger
20
21 from resources.libraries.python.ssh import SSH
22 from resources.libraries.python.constants import Constants
23 from resources.libraries.python.topology import NodeType
24
25
26 class QemuUtils(object):
27     """QEMU utilities."""
28
29     __QEMU_BIN = '/usr/bin/qemu-system-x86_64'
30     # QEMU Machine Protocol socket
31     __QMP_SOCK = '/tmp/qmp.sock'
32     # QEMU Guest Agent socket
33     __QGA_SOCK = '/tmp/qga.sock'
34
35     def __init__(self):
36         self._qemu_opt = {}
37         # Default 1 CPU.
38         self._qemu_opt['smp'] = '-smp 1,sockets=1,cores=1,threads=1'
39         # Daemonize the QEMU process after initialization. Default one
40         # management interface.
41         self._qemu_opt['options'] = '-cpu host -daemonize -enable-kvm ' \
42             '-machine pc,accel=kvm,usb=off,mem-merge=off ' \
43             '-net nic,macaddr=52:54:00:00:02:01 -balloon none'
44         self._qemu_opt['ssh_fwd_port'] = 10022
45         # Default serial console port
46         self._qemu_opt['serial_port'] = 4556
47         # Default 512MB virtual RAM
48         self._qemu_opt['mem_size'] = 512
49         # Default huge page mount point, required for Vhost-user interfaces.
50         self._qemu_opt['huge_mnt'] = '/mnt/huge'
51         # Default do not allocate huge pages.
52         self._qemu_opt['huge_allocate'] = False
53         # Default image for CSIT virl setup
54         self._qemu_opt['disk_image'] = '/var/lib/vm/vhost-nested.img'
55         # VM node info dict
56         self._vm_info = {
57             'type': NodeType.VM,
58             'port': 10022,
59             'username': 'cisco',
60             'password': 'cisco',
61             'interfaces': {},
62         }
63         # Virtio queue count
64         self._qemu_opt['queues'] = 1
65         self._vhost_id = 0
66         self._ssh = None
67         self._node = None
68         self._socks = [self.__QMP_SOCK, self.__QGA_SOCK]
69
70     def qemu_set_smp(self, cpus, cores, threads, sockets):
71         """Set SMP option for QEMU
72
73         :param cpus: Number of CPUs.
74         :param cores: Number of CPU cores on one socket.
75         :param threads: Number of threads on one CPU core.
76         :param sockets: Number of discrete sockets in the system.
77         :type cpus: int
78         :type cores: int
79         :type threads: int
80         :type sockets: int
81         """
82         self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format(
83             cpus, cores, threads, sockets)
84
85     def qemu_set_ssh_fwd_port(self, fwd_port):
86         """Set host port for guest SSH forwarding.
87
88         :param fwd_port: Port number on host for guest SSH forwarding.
89         :type fwd_port: int
90         """
91         self._qemu_opt['ssh_fwd_port'] = fwd_port
92         self._vm_info['port'] = fwd_port
93
94     def qemu_set_serial_port(self, port):
95         """Set serial console port.
96
97         :param port: Serial console port.
98         :type port: int
99         """
100         self._qemu_opt['serial_port'] = port
101
102     def qemu_set_mem_size(self, mem_size):
103         """Set virtual RAM size.
104
105         :param mem_size: RAM size in Mega Bytes.
106         :type mem_size: int
107         """
108         self._qemu_opt['mem_size'] = int(mem_size)
109
110     def qemu_set_huge_mnt(self, huge_mnt):
111         """Set hugefile mount point.
112
113         :param huge_mnt: System hugefile mount point.
114         :type huge_mnt: int
115         """
116         self._qemu_opt['huge_mnt'] = huge_mnt
117
118     def qemu_set_huge_allocate(self):
119         """Set flag to allocate more huge pages if needed."""
120         self._qemu_opt['huge_allocate'] = True
121
122     def qemu_set_disk_image(self, disk_image):
123         """Set disk image.
124
125         :param disk_image: Path of the disk image.
126         :type disk_image: str
127         """
128         self._qemu_opt['disk_image'] = disk_image
129
130     def qemu_set_affinity(self, *host_cpus):
131         """Set qemu affinity by getting thread PIDs via QMP and taskset to list
132         of CPU cores.
133
134         :param host_cpus: List of CPU cores.
135         :type host_cpus: list
136         """
137         qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
138
139         if len(qemu_cpus) != len(host_cpus):
140             logger.debug('Host CPU count {0}, Qemu Thread count {1}'.format(
141                 len(host_cpus), len(qemu_cpus)))
142             raise ValueError('Host CPU count must match Qemu Thread count')
143
144         for qemu_cpu, host_cpu in zip(qemu_cpus, host_cpus):
145             cmd = 'taskset -p {0} {1}'.format(hex(1 << int(host_cpu)),
146                                               qemu_cpu['thread_id'])
147             (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
148             if int(ret_code) != 0:
149                 logger.debug('Set affinity failed {0}'.format(stderr))
150                 raise RuntimeError('Set affinity failed on {0}'.format(
151                     self._node['host']))
152
153     def qemu_set_scheduler_policy(self):
154         """Set scheduler policy to SCHED_RR with priority 1 for all Qemu CPU
155         processes.
156
157        :raises RuntimeError: Set scheduler policy failed.
158         """
159         qemu_cpus = self._qemu_qmp_exec('query-cpus')['return']
160
161         for qemu_cpu in qemu_cpus:
162             cmd = 'chrt -r -p 1 {0}'.format(qemu_cpu['thread_id'])
163             (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
164             if int(ret_code) != 0:
165                 logger.debug('Set SCHED_RR failed {0}'.format(stderr))
166                 raise RuntimeError('Set SCHED_RR failed on {0}'.format(
167                     self._node['host']))
168
169     def qemu_set_node(self, node):
170         """Set node to run QEMU on.
171
172         :param node: Node to run QEMU on.
173         :type node: dict
174         """
175         self._node = node
176         self._ssh = SSH()
177         self._ssh.connect(node)
178         self._vm_info['host'] = node['host']
179
180     def qemu_add_vhost_user_if(self, socket, server=True, mac=None):
181         """Add Vhost-user interface.
182
183         :param socket: Path of the unix socket.
184         :param server: If True the socket shall be a listening socket.
185         :param mac: Vhost-user interface MAC address (optional, otherwise is
186             used autogenerated MAC 52:54:00:00:04:xx).
187         :type socket: str
188         :type server: bool
189         :type mac: str
190         """
191         self._vhost_id += 1
192         # Create unix socket character device.
193         chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id,
194                                                                 socket)
195         if server is True:
196             chardev += ',server'
197         self._qemu_opt['options'] += chardev
198         # Create Vhost-user network backend.
199         netdev = ' -netdev vhost-user,id=vhost{0},chardev=char{0},'\
200             'queues={1}'.format(self._vhost_id, self._qemu_opt['queues'])
201         self._qemu_opt['options'] += netdev
202         # If MAC is not specified use autogenerated 52:54:00:00:04:<vhost_id>
203         # e.g. vhost1 MAC is 52:54:00:00:04:01
204         if mac is None:
205             mac = '52:54:00:00:04:{0:02x}'.format(self._vhost_id)
206         extend_options = 'mq=on,csum=off,gso=off,guest_tso4=off,'\
207             'guest_tso6=off,guest_ecn=off,mrg_rxbuf=off'
208         # Create Virtio network device.
209         device = ' -device virtio-net-pci,netdev=vhost{0},mac={1},{2}'.format(
210             self._vhost_id, mac, extend_options)
211         self._qemu_opt['options'] += device
212         # Add interface MAC and socket to the node dict
213         if_data = {'mac_address': mac, 'socket': socket}
214         if_name = 'vhost{}'.format(self._vhost_id)
215         self._vm_info['interfaces'][if_name] = if_data
216         # Add socket to the socket list
217         self._socks.append(socket)
218
219     def _qemu_qmp_exec(self, cmd):
220         """Execute QMP command.
221
222         QMP is JSON based protocol which allows to control QEMU instance.
223
224         :param cmd: QMP command to execute.
225         :type cmd: str
226         :return: Command output in python representation of JSON format. The
227             { "return": {} } response is QMP's success response. An error
228             response will contain the "error" keyword instead of "return".
229         """
230         # To enter command mode, the qmp_capabilities command must be issued.
231         qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' + \
232             '{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc -U ' + \
233             self.__QMP_SOCK
234         (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd)
235         if int(ret_code) != 0:
236             logger.debug('QMP execute failed {0}'.format(stderr))
237             raise RuntimeError('QMP execute "{0}"'
238                                ' failed on {1}'.format(cmd, self._node['host']))
239         logger.trace(stdout)
240         # Skip capabilities negotiation messages.
241         out_list = stdout.splitlines()
242         if len(out_list) < 3:
243             raise RuntimeError('Invalid QMP output on {0}'.format(
244                 self._node['host']))
245         return json.loads(out_list[2])
246
247     def _qemu_qga_flush(self):
248         """Flush the QGA parser state
249         """
250         qga_cmd = 'printf "\xFF" | sudo -S nc ' \
251             '-q 1 -U ' + self.__QGA_SOCK
252         (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
253         if int(ret_code) != 0:
254             logger.debug('QGA execute failed {0}'.format(stderr))
255             raise RuntimeError('QGA execute "{0}" '
256                                'failed on {1}'.format(qga_cmd,
257                                                       self._node['host']))
258         logger.trace(stdout)
259         if not stdout:
260             return {}
261         return json.loads(stdout.split('\n', 1)[0])
262
263     def _qemu_qga_exec(self, cmd):
264         """Execute QGA command.
265
266         QGA provide access to a system-level agent via standard QMP commands.
267
268         :param cmd: QGA command to execute.
269         :type cmd: str
270         """
271         qga_cmd = 'echo "{ \\"execute\\": \\"' + cmd + '\\" }" | sudo -S nc ' \
272             '-q 1 -U ' + self.__QGA_SOCK
273         (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd)
274         if int(ret_code) != 0:
275             logger.debug('QGA execute failed {0}'.format(stderr))
276             raise RuntimeError('QGA execute "{0}"'
277                                ' failed on {1}'.format(cmd, self._node['host']))
278         logger.trace(stdout)
279         if not stdout:
280             return {}
281         return json.loads(stdout.split('\n', 1)[0])
282
283     def _wait_until_vm_boot(self, timeout=300):
284         """Wait until QEMU VM is booted.
285
286         Ping QEMU guest agent each 5s until VM booted or timeout.
287
288         :param timeout: Waiting timeout in seconds (optional, default 300s).
289         :type timeout: int
290         """
291         start = time()
292         while 1:
293             if time() - start > timeout:
294                 raise RuntimeError('timeout, VM {0} not booted on {1}'.format(
295                     self._qemu_opt['disk_image'], self._node['host']))
296             self._qemu_qga_flush()
297             out = self._qemu_qga_exec('guest-ping')
298             # Empty output - VM not booted yet
299             if not out:
300                 sleep(5)
301             # Non-error return - VM booted
302             elif out.get('return') is not None:
303                 break
304             # Skip error and wait
305             elif out.get('error') is not None:
306                 sleep(5)
307             else:
308                 raise RuntimeError('QGA guest-ping unexpected output {}'.format(
309                     out))
310         logger.trace('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'],
311                                                    self._node['host']))
312
313     def _update_vm_interfaces(self):
314         """Update interface names in VM node dict."""
315         # Send guest-network-get-interfaces command via QGA, output example:
316         # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"},
317         # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]}
318         out = self._qemu_qga_exec('guest-network-get-interfaces')
319         interfaces = out.get('return')
320         mac_name = {}
321         if not interfaces:
322             raise RuntimeError('Get VM {0} interface list failed on {1}'.format(
323                 self._qemu_opt['disk_image'], self._node['host']))
324         # Create MAC-name dict
325         for interface in interfaces:
326             if 'hardware-address' not in interface:
327                 continue
328             mac_name[interface['hardware-address']] = interface['name']
329         # Match interface by MAC and save interface name
330         for interface in self._vm_info['interfaces'].values():
331             mac = interface.get('mac_address')
332             if_name = mac_name.get(mac)
333             if if_name is None:
334                 logger.trace('Interface name for MAC {} not found'.format(mac))
335             else:
336                 interface['name'] = if_name
337
338     def _huge_page_check(self, allocate=False):
339         """Huge page check."""
340         huge_mnt = self._qemu_opt.get('huge_mnt')
341         mem_size = self._qemu_opt.get('mem_size')
342
343         # Get huge pages information
344         huge_size = self._get_huge_page_size()
345         huge_free = self._get_huge_page_free(huge_size)
346         huge_total = self._get_huge_page_total(huge_size)
347
348         # Check if memory reqested by qemu is available on host
349         if (mem_size * 1024) > (huge_free * huge_size):
350             # If we want to allocate hugepage dynamically
351             if allocate:
352                 mem_needed = abs((huge_free * huge_size) - (mem_size * 1024))
353                 huge_to_allocate = ((mem_needed / huge_size) * 2) + huge_total
354                 max_map_count = huge_to_allocate*4
355                 # Increase maximum number of memory map areas a process may have
356                 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/max_map_count'.format(
357                     max_map_count)
358                 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
359                 # Increase hugepage count
360                 cmd = 'echo "{0}" | sudo tee /proc/sys/vm/nr_hugepages'.format(
361                     huge_to_allocate)
362                 (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
363                 if int(ret_code) != 0:
364                     logger.debug('Mount huge pages failed {0}'.format(stderr))
365                     raise RuntimeError('Mount huge pages failed on {0}'.format(
366                         self._node['host']))
367             # If we do not want to allocate dynamicaly end with error
368             else:
369                 raise RuntimeError(
370                     'Not enough free huge pages: {0}, '
371                     '{1} MB'.format(huge_free, huge_free * huge_size)
372                 )
373         # Check if huge pages mount point exist
374         has_huge_mnt = False
375         (_, output, _) = self._ssh.exec_command('cat /proc/mounts')
376         for line in output.splitlines():
377             # Try to find something like:
378             # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0
379             mount = line.split()
380             if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt:
381                 has_huge_mnt = True
382                 break
383         # If huge page mount point not exist create one
384         if not has_huge_mnt:
385             cmd = 'mkdir -p {0}'.format(huge_mnt)
386             (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
387             if int(ret_code) != 0:
388                 logger.debug('Create mount dir failed: {0}'.format(stderr))
389                 raise RuntimeError('Create mount dir failed on {0}'.format(
390                     self._node['host']))
391             cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format(
392                 huge_mnt)
393             (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd)
394             if int(ret_code) != 0:
395                 logger.debug('Mount huge pages failed {0}'.format(stderr))
396                 raise RuntimeError('Mount huge pages failed on {0}'.format(
397                     self._node['host']))
398
399     def _get_huge_page_size(self):
400         """Get default size of huge pages in system.
401
402         :returns: Default size of free huge pages in system.
403         :rtype: int
404         :raises: RuntimeError if reading failed for three times.
405         """
406         # TODO: remove to dedicated library
407         cmd_huge_size = "grep Hugepagesize /proc/meminfo | awk '{ print $2 }'"
408         for _ in range(3):
409             (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_size)
410             if ret == 0:
411                 try:
412                     huge_size = int(out)
413                 except ValueError:
414                     logger.trace('Reading huge page size information failed')
415                 else:
416                     break
417         else:
418             raise RuntimeError('Getting huge page size information failed.')
419         return huge_size
420
421     def _get_huge_page_free(self, huge_size):
422         """Get total number of huge pages in system.
423
424         :param huge_size: Size of hugepages.
425         :type huge_size: int
426         :returns: Number of free huge pages in system.
427         :rtype: int
428         :raises: RuntimeError if reading failed for three times.
429         """
430         # TODO: add numa aware option
431         # TODO: remove to dedicated library
432         cmd_huge_free = 'cat /sys/kernel/mm/hugepages/hugepages-{0}kB/'\
433             'free_hugepages'.format(huge_size)
434         for _ in range(3):
435             (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_free)
436             if ret == 0:
437                 try:
438                     huge_free = int(out)
439                 except ValueError:
440                     logger.trace('Reading free huge pages information failed')
441                 else:
442                     break
443         else:
444             raise RuntimeError('Getting free huge pages information failed.')
445         return huge_free
446
447     def _get_huge_page_total(self, huge_size):
448         """Get total number of huge pages in system.
449
450         :param huge_size: Size of hugepages.
451         :type huge_size: int
452         :returns: Total number of huge pages in system.
453         :rtype: int
454         :raises: RuntimeError if reading failed for three times.
455         """
456         # TODO: add numa aware option
457         # TODO: remove to dedicated library
458         cmd_huge_total = 'cat /sys/kernel/mm/hugepages/hugepages-{0}kB/'\
459             'nr_hugepages'.format(huge_size)
460         for _ in range(3):
461             (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_total)
462             if ret == 0:
463                 try:
464                     huge_total = int(out)
465                 except ValueError:
466                     logger.trace('Reading total huge pages information failed')
467                 else:
468                     break
469         else:
470             raise RuntimeError('Getting total huge pages information failed.')
471         return huge_total
472
473     def qemu_start(self):
474         """Start QEMU and wait until VM boot.
475
476         :return: VM node info.
477         :rtype: dict
478         .. note:: First set at least node to run QEMU on.
479         .. warning:: Starts only one VM on the node.
480         """
481         # SSH forwarding
482         ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format(
483             self._qemu_opt.get('ssh_fwd_port'))
484         # Memory and huge pages
485         mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \
486             'share=on -m {0} -numa node,memdev=mem'.format(
487                 self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt'))
488
489         # By default check only if hugepages are availbale.
490         # If 'huge_allocate' is set to true try to allocate as well.
491         self._huge_page_check(allocate=self._qemu_opt.get('huge_allocate'))
492
493         # Setup QMP via unix socket
494         qmp = '-qmp unix:{0},server,nowait'.format(self.__QMP_SOCK)
495         # Setup serial console
496         serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \
497             'nowait -device isa-serial,chardev=gnc0'.format(
498                 self._qemu_opt.get('serial_port'))
499         # Setup QGA via chardev (unix socket) and isa-serial channel
500         qga = '-chardev socket,path=/tmp/qga.sock,server,nowait,id=qga0 ' \
501             '-device isa-serial,chardev=qga0'
502         # Graphic setup
503         graphic = '-monitor none -display none -vga none'
504         # Run QEMU
505         cmd = '{0} {1} {2} {3} {4} -hda {5} {6} {7} {8} {9}'.format(
506             self.__QEMU_BIN, self._qemu_opt.get('smp'), mem, ssh_fwd,
507             self._qemu_opt.get('options'),
508             self._qemu_opt.get('disk_image'), qmp, serial, qga, graphic)
509         (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300)
510         if int(ret_code) != 0:
511             logger.debug('QEMU start failed {0}'.format(stderr))
512             raise RuntimeError('QEMU start failed on {0}'.format(
513                 self._node['host']))
514         logger.trace('QEMU running')
515         # Wait until VM boot
516         self._wait_until_vm_boot()
517         # Update interface names in VM node dict
518         self._update_vm_interfaces()
519         # Return VM node dict
520         return self._vm_info
521
522     def qemu_quit(self):
523         """Quit the QEMU emulator."""
524         out = self._qemu_qmp_exec('quit')
525         err = out.get('error')
526         if err is not None:
527             raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format(
528                 self._node['host'], json.dumps(err)))
529
530     def qemu_system_powerdown(self):
531         """Power down the system (if supported)."""
532         out = self._qemu_qmp_exec('system_powerdown')
533         err = out.get('error')
534         if err is not None:
535             raise RuntimeError(
536                 'QEMU system powerdown failed on {0}, '
537                 'error: {1}'.format(self._node['host'], json.dumps(err))
538             )
539
540     def qemu_system_reset(self):
541         """Reset the system."""
542         out = self._qemu_qmp_exec('system_reset')
543         err = out.get('error')
544         if err is not None:
545             raise RuntimeError(
546                 'QEMU system reset failed on {0}, '
547                 'error: {1}'.format(self._node['host'], json.dumps(err)))
548
549     def qemu_kill(self):
550         """Kill qemu process."""
551         # TODO: add PID storage so that we can kill specific PID
552         # Note: in QEMU start phase there are 3 QEMU processes because we
553         # daemonize QEMU
554         self._ssh.exec_command_sudo('pkill -SIGKILL qemu')
555
556     def qemu_clear_socks(self):
557         """Remove all sockets created by QEMU."""
558         # If serial console port still open kill process
559         cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port'))
560         self._ssh.exec_command_sudo(cmd)
561         # Delete all created sockets
562         for sock in self._socks:
563             cmd = 'rm -f {}'.format(sock)
564             self._ssh.exec_command_sudo(cmd)
565
566     def qemu_system_status(self):
567         """Return current VM status.
568
569         VM should be in following status:
570
571             - debug: QEMU running on a debugger
572             - finish-migrate: paused to finish the migration process
573             - inmigrate: waiting for an incoming migration
574             - internal-error: internal error has occurred
575             - io-error: the last IOP has failed
576             - paused: paused
577             - postmigrate: paused following a successful migrate
578             - prelaunch: QEMU was started with -S and guest has not started
579             - restore-vm: paused to restore VM state
580             - running: actively running
581             - save-vm: paused to save the VM state
582             - shutdown: shut down (and -no-shutdown is in use)
583             - suspended: suspended (ACPI S3)
584             - watchdog: watchdog action has been triggered
585             - guest-panicked: panicked as a result of guest OS panic
586
587         :return: VM status.
588         :rtype: str
589         """
590         out = self._qemu_qmp_exec('query-status')
591         ret = out.get('return')
592         if ret is not None:
593             return ret.get('status')
594         else:
595             err = out.get('error')
596             raise RuntimeError(
597                 'QEMU query-status failed on {0}, '
598                 'error: {1}'.format(self._node['host'], json.dumps(err)))
599
600     @staticmethod
601     def build_qemu(node):
602         """Build QEMU from sources.
603
604         :param node: Node to build QEMU on.
605         :type node: dict
606         """
607         ssh = SSH()
608         ssh.connect(node)
609
610         (ret_code, stdout, stderr) = \
611             ssh.exec_command('sudo -Sn bash {0}/{1}/qemu_build.sh'.format(
612                 Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH), 1000)
613         logger.trace(stdout)
614         if int(ret_code) != 0:
615             logger.debug('QEMU build failed {0}'.format(stderr))
616             raise RuntimeError('QEMU build failed on {0}'.format(node['host']))