Merge "Fix issues with 'Success_' stats reported by ProxBinSearchProfile"
[yardstick.git] / yardstick / benchmark / contexts / standalone / model.py
1 # Copyright (c) 2016-2017 Intel Corporation
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import os
16 import re
17 import time
18 import uuid
19 import random
20 import logging
21 import errno
22
23 from netaddr import IPNetwork
24 import xml.etree.ElementTree as ET
25
26 from yardstick import ssh
27 from yardstick.common import constants
28 from yardstick.common import exceptions
29 from yardstick.common import yaml_loader
30 from yardstick.network_services.utils import PciAddress
31 from yardstick.network_services.helpers.cpu import CpuSysCores
32
33
34 LOG = logging.getLogger(__name__)
35
36 VM_TEMPLATE = """
37 <domain type="kvm">
38   <name>{vm_name}</name>
39   <uuid>{random_uuid}</uuid>
40   <memory unit="MB">{memory}</memory>
41   <currentMemory unit="MB">{memory}</currentMemory>
42   <memoryBacking>
43     <hugepages />
44   </memoryBacking>
45   <vcpu cpuset='{cpuset}'>{vcpu}</vcpu>
46  {cputune}
47   <os>
48     <type arch="x86_64" machine="pc-i440fx-xenial">hvm</type>
49     <boot dev="hd" />
50   </os>
51   <features>
52     <acpi />
53     <apic />
54     <pae />
55   </features>
56   <cpu mode='host-passthrough'>
57     <topology cores="{cpu}" sockets="{socket}" threads="{threads}" />
58     <numa>
59        <cell id='0' cpus='{numa_cpus}' memory='{memory}' unit='MB' memAccess='shared'/>
60     </numa>
61   </cpu>
62   <clock offset="utc">
63     <timer name="rtc" tickpolicy="catchup" />
64     <timer name="pit" tickpolicy="delay" />
65     <timer name="hpet" present="no" />
66   </clock>
67   <on_poweroff>destroy</on_poweroff>
68   <on_reboot>restart</on_reboot>
69   <on_crash>restart</on_crash>
70   <devices>
71     <emulator>/usr/bin/kvm-spice</emulator>
72     <disk device="disk" type="file">
73       <driver name="qemu" type="qcow2" />
74       <source file="{vm_image}"/>
75       <target bus="virtio" dev="vda" />
76     </disk>
77     <graphics autoport="yes" listen="0.0.0.0" port="-1" type="vnc" />
78     <interface type="bridge">
79       <mac address='{mac_addr}'/>
80       <source bridge="br-int" />
81       <model type='virtio'/>
82     </interface>
83     <serial type='pty'>
84       <target port='0'/>
85     </serial>
86     <console type='pty'>
87       <target type='serial' port='0'/>
88     </console>
89   </devices>
90 </domain>
91 """
92
93 USER_DATA_TEMPLATE = """
94 cat > {user_file} <<EOF
95 #cloud-config
96 preserve_hostname: false
97 hostname: {host}
98 users:
99 {user_config}
100 EOF
101 """
102
103 WAIT_FOR_BOOT = 30
104
105
106 class Libvirt(object):
107     """ This class handles all the libvirt updates to lauch VM
108     """
109
110     @staticmethod
111     def check_if_vm_exists_and_delete(vm_name, connection):
112         cmd_template = "virsh list --name | grep -i %s"
113         status = connection.execute(cmd_template % vm_name)[0]
114         if status == 0:
115             LOG.info("VM '%s' is already present... destroying", vm_name)
116             connection.execute("virsh destroy %s" % vm_name)
117
118     @staticmethod
119     def virsh_create_vm(connection, cfg):
120         LOG.info('VM create, XML config: %s', cfg)
121         status, _, error = connection.execute('virsh create %s' % cfg)
122         if status:
123             raise exceptions.LibvirtCreateError(error=error)
124
125     @staticmethod
126     def virsh_destroy_vm(vm_name, connection):
127         LOG.info('VM destroy, VM name: %s', vm_name)
128         status, _, error = connection.execute('virsh destroy %s' % vm_name)
129         if status:
130             LOG.warning('Error destroying VM %s. Error: %s', vm_name, error)
131
132     @staticmethod
133     def _add_interface_address(interface, pci_address):
134         """Add a PCI 'address' XML node
135
136         <address type='pci' domain='0x0000' bus='0x00' slot='0x08'
137          function='0x0'/>
138
139         Refence: https://software.intel.com/en-us/articles/
140                  configure-sr-iov-network-virtual-functions-in-linux-kvm
141         """
142         vm_pci = ET.SubElement(interface, 'address')
143         vm_pci.set('type', 'pci')
144         vm_pci.set('domain', '0x{}'.format(pci_address.domain))
145         vm_pci.set('bus', '0x{}'.format(pci_address.bus))
146         vm_pci.set('slot', '0x{}'.format(pci_address.slot))
147         vm_pci.set('function', '0x{}'.format(pci_address.function))
148         return vm_pci
149
150     @classmethod
151     def add_ovs_interface(cls, vpath, port_num, vpci, vports_mac, xml_str):
152         """Add a DPDK OVS 'interface' XML node in 'devices' node
153
154         <devices>
155             <interface type='vhostuser'>
156                 <mac address='00:00:00:00:00:01'/>
157                 <source type='unix' path='/usr/local/var/run/openvswitch/
158                  dpdkvhostuser0' mode='client'/>
159                 <model type='virtio'/>
160                 <driver queues='4'>
161                     <host mrg_rxbuf='off'/>
162                 </driver>
163                 <address type='pci' domain='0x0000' bus='0x00' slot='0x03'
164                  function='0x0'/>
165             </interface>
166             ...
167         </devices>
168
169         Reference: http://docs.openvswitch.org/en/latest/topics/dpdk/
170                    vhost-user/
171         """
172
173         vhost_path = ('{0}/var/run/openvswitch/dpdkvhostuser{1}'.
174                       format(vpath, port_num))
175         root = ET.fromstring(xml_str)
176         pci_address = PciAddress(vpci.strip())
177         device = root.find('devices')
178
179         interface = ET.SubElement(device, 'interface')
180         interface.set('type', 'vhostuser')
181         mac = ET.SubElement(interface, 'mac')
182         mac.set('address', vports_mac)
183
184         source = ET.SubElement(interface, 'source')
185         source.set('type', 'unix')
186         source.set('path', vhost_path)
187         source.set('mode', 'client')
188
189         model = ET.SubElement(interface, 'model')
190         model.set('type', 'virtio')
191
192         driver = ET.SubElement(interface, 'driver')
193         driver.set('queues', '4')
194
195         host = ET.SubElement(driver, 'host')
196         host.set('mrg_rxbuf', 'off')
197
198         cls._add_interface_address(interface, pci_address)
199
200         return ET.tostring(root)
201
202     @classmethod
203     def add_sriov_interfaces(cls, vm_pci, vf_pci, vf_mac, xml_str):
204         """Add a SR-IOV 'interface' XML node in 'devices' node
205
206         <devices>
207            <interface type='hostdev' managed='yes'>
208              <source>
209                <address type='pci' domain='0x0000' bus='0x00' slot='0x03'
210                 function='0x0'/>
211              </source>
212              <mac address='52:54:00:6d:90:02'>
213              <address type='pci' domain='0x0000' bus='0x02' slot='0x04'
214               function='0x1'/>
215            </interface>
216            ...
217          </devices>
218
219         Reference: https://access.redhat.com/documentation/en-us/
220             red_hat_enterprise_linux/6/html/
221             virtualization_host_configuration_and_guest_installation_guide/
222             sect-virtualization_host_configuration_and_guest_installation_guide
223             -sr_iov-how_sr_iov_libvirt_works
224         """
225
226         root = ET.fromstring(xml_str)
227         device = root.find('devices')
228
229         interface = ET.SubElement(device, 'interface')
230         interface.set('managed', 'yes')
231         interface.set('type', 'hostdev')
232
233         mac = ET.SubElement(interface, 'mac')
234         mac.set('address', vf_mac)
235
236         source = ET.SubElement(interface, 'source')
237         pci_address = PciAddress(vf_pci.strip())
238         cls._add_interface_address(source, pci_address)
239
240         pci_vm_address = PciAddress(vm_pci.strip())
241         cls._add_interface_address(interface, pci_vm_address)
242
243         return ET.tostring(root)
244
245     @staticmethod
246     def create_snapshot_qemu(connection, index, base_image):
247         """Create the snapshot image for a VM using a base image
248
249         :param connection: SSH connection to the remote host
250         :param index: index of the VM to be spawn
251         :param base_image: path of the VM base image in the remote host
252         :return: snapshot image path
253         """
254         vm_image = '/var/lib/libvirt/images/%s.qcow2' % index
255         connection.execute('rm -- "%s"' % vm_image)
256         status, _, _ = connection.execute('test -r %s' % base_image)
257         if status:
258             if not os.access(base_image, os.R_OK):
259                 raise exceptions.LibvirtQemuImageBaseImageNotPresent(
260                     vm_image=vm_image, base_image=base_image)
261             # NOTE(ralonsoh): done in two steps to avoid root permission
262             # issues.
263             LOG.info('Copy %s from execution host to remote host', base_image)
264             file_name = os.path.basename(os.path.normpath(base_image))
265             connection.put_file(base_image, '/tmp/%s' % file_name)
266             status, _, error = connection.execute(
267                 'mv -- "/tmp/%s" "%s"' % (file_name, base_image))
268             if status:
269                 raise exceptions.LibvirtQemuImageCreateError(
270                     vm_image=vm_image, base_image=base_image, error=error)
271
272         LOG.info('Convert image %s to %s', base_image, vm_image)
273         qemu_cmd = ('qemu-img create -f qcow2 -o backing_file=%s %s' %
274                     (base_image, vm_image))
275         status, _, error = connection.execute(qemu_cmd)
276         if status:
277             raise exceptions.LibvirtQemuImageCreateError(
278                 vm_image=vm_image, base_image=base_image, error=error)
279         return vm_image
280
281     @classmethod
282     def build_vm_xml(cls, connection, flavor, vm_name, index, cdrom_img):
283         """Build the XML from the configuration parameters"""
284         memory = flavor.get('ram', '4096')
285         extra_spec = flavor.get('extra_specs', {})
286         cpu = extra_spec.get('hw:cpu_cores', '2')
287         socket = extra_spec.get('hw:cpu_sockets', '1')
288         threads = extra_spec.get('hw:cpu_threads', '2')
289         vcpu = int(cpu) * int(threads)
290         numa_cpus = '0-%s' % (vcpu - 1)
291         hw_socket = flavor.get('hw_socket', '0')
292         cpuset = Libvirt.pin_vcpu_for_perf(connection, hw_socket)
293
294         cputune = extra_spec.get('cputune', '')
295         mac = StandaloneContextHelper.get_mac_address(0x00)
296         image = cls.create_snapshot_qemu(connection, index,
297                                          flavor.get("images", None))
298         vm_xml = VM_TEMPLATE.format(
299             vm_name=vm_name,
300             random_uuid=uuid.uuid4(),
301             mac_addr=mac,
302             memory=memory, vcpu=vcpu, cpu=cpu,
303             numa_cpus=numa_cpus,
304             socket=socket, threads=threads,
305             vm_image=image, cpuset=cpuset, cputune=cputune)
306
307         # Add CD-ROM device
308         vm_xml = Libvirt.add_cdrom(cdrom_img, vm_xml)
309
310         return vm_xml, mac
311
312     @staticmethod
313     def update_interrupts_hugepages_perf(connection):
314         connection.execute("echo 1 > /sys/module/kvm/parameters/allow_unsafe_assigned_interrupts")
315         connection.execute("echo never > /sys/kernel/mm/transparent_hugepage/enabled")
316
317     @classmethod
318     def pin_vcpu_for_perf(cls, connection, socket='0'):
319         threads = ""
320         sys_obj = CpuSysCores(connection)
321         soc_cpu = sys_obj.get_core_socket()
322         sys_cpu = int(soc_cpu["cores_per_socket"])
323         socket = str(socket)
324         cores = "%s-%s" % (soc_cpu[socket][0], soc_cpu[socket][sys_cpu - 1])
325         if int(soc_cpu["thread_per_core"]) > 1:
326             threads = "%s-%s" % (soc_cpu[socket][sys_cpu], soc_cpu[socket][-1])
327         cpuset = "%s,%s" % (cores, threads)
328         return cpuset
329
330     @classmethod
331     def write_file(cls, file_name, xml_str):
332         """Dump a XML string to a file"""
333         root = ET.fromstring(xml_str)
334         et = ET.ElementTree(element=root)
335         et.write(file_name, encoding='utf-8', method='xml')
336
337     @classmethod
338     def add_cdrom(cls, file_path, xml_str):
339         """Add a CD-ROM disk XML node in 'devices' node
340
341         <devices>
342             <disk type='file' device='cdrom'>
343               <driver name='qemu' type='raw'/>
344               <source file='/var/lib/libvirt/images/data.img'/>
345               <target dev='hdb'/>
346               <readonly/>
347             </disk>
348             ...
349         </devices>
350         """
351
352         root = ET.fromstring(xml_str)
353         device = root.find('devices')
354
355         disk = ET.SubElement(device, 'disk')
356         disk.set('type', 'file')
357         disk.set('device', 'cdrom')
358
359         driver = ET.SubElement(disk, 'driver')
360         driver.set('name', 'qemu')
361         driver.set('type', 'raw')
362
363         source = ET.SubElement(disk, 'source')
364         source.set('file', file_path)
365
366         target = ET.SubElement(disk, 'target')
367         target.set('dev', 'hdb')
368
369         ET.SubElement(disk, 'readonly')
370         return ET.tostring(root)
371
372     @staticmethod
373     def gen_cdrom_image(connection, file_path, vm_name, vm_user, key_filename):
374         """Generate ISO image for CD-ROM """
375
376         user_config = ["    - name: {user_name}",
377                        "      ssh_authorized_keys:",
378                        "        - {pub_key_str}"]
379         if vm_user != "root":
380             user_config.append("      sudo: ALL=(ALL) NOPASSWD:ALL")
381
382         meta_data = "/tmp/meta-data"
383         user_data = "/tmp/user-data"
384         with open(".".join([key_filename, "pub"]), "r") as pub_key_file:
385             pub_key_str = pub_key_file.read().rstrip()
386         user_conf = os.linesep.join(user_config).format(pub_key_str=pub_key_str, user_name=vm_user)
387
388         cmd_lst = [
389             "touch %s" % meta_data,
390             USER_DATA_TEMPLATE.format(user_file=user_data, host=vm_name, user_config=user_conf),
391             "genisoimage -output {0} -volid cidata -joliet -r {1} {2}".format(file_path,
392                                                                               meta_data,
393                                                                               user_data),
394             "rm {0} {1}".format(meta_data, user_data),
395         ]
396         for cmd in cmd_lst:
397             LOG.info(cmd)
398             status, _, error = connection.execute(cmd)
399             if status:
400                 raise exceptions.LibvirtQemuImageCreateError(error=error)
401
402
403 class StandaloneContextHelper(object):
404     """ This class handles all the common code for standalone
405     """
406     def __init__(self):
407         self.file_path = None
408         super(StandaloneContextHelper, self).__init__()
409
410     @staticmethod
411     def install_req_libs(connection, extra_pkgs=None):
412         extra_pkgs = extra_pkgs or []
413         pkgs = ["qemu-kvm", "libvirt-bin", "bridge-utils", "numactl", "fping", "genisoimage"]
414         pkgs.extend(extra_pkgs)
415         cmd_template = "dpkg-query -W --showformat='${Status}\\n' \"%s\"|grep 'ok installed'"
416         for pkg in pkgs:
417             if connection.execute(cmd_template % pkg)[0]:
418                 connection.execute("apt-get update")
419                 connection.execute("apt-get -y install %s" % pkg)
420
421     @staticmethod
422     def get_kernel_module(connection, pci, driver):
423         if not driver:
424             out = connection.execute("lspci -k -s %s" % pci)[1]
425             driver = out.split("Kernel modules:").pop().strip()
426         return driver
427
428     @classmethod
429     def get_nic_details(cls, connection, networks, dpdk_devbind):
430         for key, ports in networks.items():
431             if key == "mgmt":
432                 continue
433
434             phy_ports = ports['phy_port']
435             phy_driver = ports.get('phy_driver', None)
436             driver = cls.get_kernel_module(connection, phy_ports, phy_driver)
437
438             # Make sure that ports are bound to kernel drivers e.g. i40e/ixgbe
439             bind_cmd = "{dpdk_devbind} --force -b {driver} {port}"
440             lshw_cmd = "lshw -c network -businfo | grep '{port}'"
441             link_show_cmd = "ip -s link show {interface}"
442
443             cmd = bind_cmd.format(dpdk_devbind=dpdk_devbind,
444                                   driver=driver, port=ports['phy_port'])
445             connection.execute(cmd)
446
447             out = connection.execute(lshw_cmd.format(port=phy_ports))[1]
448             interface = out.split()[1]
449
450             connection.execute(link_show_cmd.format(interface=interface))
451
452             ports.update({
453                 'interface': str(interface),
454                 'driver': driver
455             })
456         LOG.info(networks)
457
458         return networks
459
460     @staticmethod
461     def get_virtual_devices(connection, pci):
462         cmd = "cat /sys/bus/pci/devices/{0}/virtfn0/uevent"
463         output = connection.execute(cmd.format(pci))[1]
464
465         pattern = "PCI_SLOT_NAME=({})".format(PciAddress.PCI_PATTERN_STR)
466         m = re.search(pattern, output, re.MULTILINE)
467
468         pf_vfs = {}
469         if m:
470             pf_vfs = {pci: m.group(1).rstrip()}
471
472         LOG.info("pf_vfs:\n%s", pf_vfs)
473
474         return pf_vfs
475
476     def parse_pod_file(self, file_path, nfvi_role='Sriov'):
477         self.file_path = file_path
478         nodes = []
479         nfvi_host = []
480         try:
481             cfg = yaml_loader.read_yaml_file(self.file_path)
482         except IOError as io_error:
483             if io_error.errno != errno.ENOENT:
484                 raise
485             self.file_path = os.path.join(constants.YARDSTICK_ROOT_PATH,
486                                           file_path)
487             cfg = yaml_loader.read_yaml_file(self.file_path)
488
489         nodes.extend([node for node in cfg["nodes"] if str(node["role"]) != nfvi_role])
490         nfvi_host.extend([node for node in cfg["nodes"] if str(node["role"]) == nfvi_role])
491         if not nfvi_host:
492             raise("Node role is other than SRIOV")
493
494         host_mgmt = {'user': nfvi_host[0]['user'],
495                      'ip': str(IPNetwork(nfvi_host[0]['ip']).ip),
496                      'password': nfvi_host[0]['password'],
497                      'ssh_port': nfvi_host[0].get('ssh_port', 22),
498                      'key_filename': nfvi_host[0].get('key_filename')}
499
500         return [nodes, nfvi_host, host_mgmt]
501
502     @staticmethod
503     def get_mac_address(end=0x7f):
504         mac = [0x52, 0x54, 0x00,
505                random.randint(0x00, end),
506                random.randint(0x00, 0xff),
507                random.randint(0x00, 0xff)]
508         mac_address = ':'.join('%02x' % x for x in mac)
509         return mac_address
510
511     @staticmethod
512     def get_mgmt_ip(connection, mac, cidr, node):
513         mgmtip = None
514         times = 10
515         while not mgmtip and times:
516             connection.execute("fping -c 1 -g %s > /dev/null 2>&1" % cidr)
517             out = connection.execute("ip neighbor | grep '%s'" % mac)[1]
518             LOG.info("fping -c 1 -g %s > /dev/null 2>&1", cidr)
519             if out.strip():
520                 mgmtip = str(out.split(" ")[0]).strip()
521                 client = ssh.SSH.from_node(node, overrides={"ip": mgmtip})
522                 client.wait()
523                 break
524
525             time.sleep(WAIT_FOR_BOOT)  # FixMe: How to find if VM is booted?
526             times = times - 1
527         return mgmtip
528
529     @classmethod
530     def wait_for_vnfs_to_start(cls, connection, servers, nodes):
531         for node in nodes:
532             vnf = servers[node["name"]]
533             mgmtip = vnf["network_ports"]["mgmt"]["cidr"]
534             ip = cls.get_mgmt_ip(connection, node["mac"], mgmtip, node)
535             if ip:
536                 node["ip"] = ip
537         return nodes
538
539     @classmethod
540     def check_update_key(cls, connection, node, vm_name, id_name, cdrom_img):
541         # Generate public/private keys if private key file is not provided
542         user_name = node.get('user')
543         if not user_name:
544             node['user'] = 'root'
545             user_name = node.get('user')
546         if not node.get('key_filename'):
547             key_filename = ''.join(
548                 [constants.YARDSTICK_ROOT_PATH,
549                  'yardstick/resources/files/yardstick_key-',
550                  id_name])
551             ssh.SSH.gen_keys(key_filename)
552             node['key_filename'] = key_filename
553         # Update image with public key
554         key_filename = node.get('key_filename')
555         Libvirt.gen_cdrom_image(connection, cdrom_img, vm_name, user_name, key_filename)
556         return node
557
558
559 class Server(object):
560     """ This class handles geting vnf nodes
561     """
562
563     @staticmethod
564     def build_vnf_interfaces(vnf, ports):
565         interfaces = {}
566         index = 0
567
568         for key, vfs in vnf["network_ports"].items():
569             if key == "mgmt":
570                 mgmtip = str(IPNetwork(vfs['cidr']).ip)
571                 continue
572
573             vf = ports[vfs[0]]
574             ip = IPNetwork(vf['cidr'])
575             interfaces.update({
576                 key: {
577                     'vpci': vf['vpci'],
578                     'driver': "%svf" % vf['driver'],
579                     'local_mac': vf['mac'],
580                     'dpdk_port_num': index,
581                     'local_ip': str(ip.ip),
582                     'netmask': str(ip.netmask)
583                     },
584             })
585             index = index + 1
586
587         return mgmtip, interfaces
588
589     @classmethod
590     def generate_vnf_instance(cls, flavor, ports, ip, key, vnf, mac):
591         mgmtip, interfaces = cls.build_vnf_interfaces(vnf, ports)
592
593         result = {
594             "ip": mgmtip,
595             "mac": mac,
596             "host": ip,
597             "user": flavor.get('user', 'root'),
598             "interfaces": interfaces,
599             "routing_table": [],
600             # empty IPv6 routing table
601             "nd_route_tbl": [],
602             "name": key, "role": key
603         }
604
605         try:
606             result['key_filename'] = flavor['key_filename']
607         except KeyError:
608             pass
609
610         try:
611             result['password'] = flavor['password']
612         except KeyError:
613             pass
614         LOG.info(result)
615         return result
616
617
618 class OvsDeploy(object):
619     """ This class handles deploy of ovs dpdk
620     Configuration: ovs_dpdk
621     """
622
623     OVS_DEPLOY_SCRIPT = "ovs_deploy.bash"
624
625     def __init__(self, connection, bin_path, ovs_properties):
626         self.connection = connection
627         self.bin_path = bin_path
628         self.ovs_properties = ovs_properties
629
630     def prerequisite(self):
631         pkgs = ["git", "build-essential", "pkg-config", "automake",
632                 "autotools-dev", "libltdl-dev", "cmake", "libnuma-dev",
633                 "libpcap-dev"]
634         StandaloneContextHelper.install_req_libs(self.connection, pkgs)
635
636     def ovs_deploy(self):
637         ovs_deploy = os.path.join(constants.YARDSTICK_ROOT_PATH,
638                                   "yardstick/resources/scripts/install/",
639                                   self.OVS_DEPLOY_SCRIPT)
640         if os.path.isfile(ovs_deploy):
641             self.prerequisite()
642             remote_ovs_deploy = os.path.join(self.bin_path, self.OVS_DEPLOY_SCRIPT)
643             LOG.info(remote_ovs_deploy)
644             self.connection.put(ovs_deploy, remote_ovs_deploy)
645
646             http_proxy = os.environ.get('http_proxy', '')
647             ovs_details = self.ovs_properties.get("version", {})
648             ovs = ovs_details.get("ovs", "2.6.0")
649             dpdk = ovs_details.get("dpdk", "16.11.1")
650
651             cmd = "sudo -E %s --ovs='%s' --dpdk='%s' -p='%s'" % (remote_ovs_deploy,
652                                                                  ovs, dpdk, http_proxy)
653             exit_status, _, stderr = self.connection.execute(cmd)
654             if exit_status:
655                 raise exceptions.OVSDeployError(stderr=stderr)