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