Adapt zrpcd to work for Carbon/Nitrogen
[apex.git] / apex / overcloud / deploy.py
1 ##############################################################################
2 # Copyright (c) 2017 Tim Rozet (trozet@redhat.com) and others.
3 #
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
9
10 import base64
11 import fileinput
12 import logging
13 import os
14 import shutil
15 import uuid
16 import struct
17 import time
18
19 from apex.common import constants as con
20 from apex.common.exceptions import ApexDeployException
21 from apex.common import parsers
22 from apex.virtual import utils as virt_utils
23 from cryptography.hazmat.primitives import serialization as \
24     crypto_serialization
25 from cryptography.hazmat.primitives.asymmetric import rsa
26 from cryptography.hazmat.backends import default_backend as \
27     crypto_default_backend
28
29
30 SDN_FILE_MAP = {
31     'opendaylight': {
32         'sfc': 'neutron-sfc-opendaylight.yaml',
33         'vpn': 'neutron-bgpvpn-opendaylight.yaml',
34         'gluon': 'gluon.yaml',
35         'vpp': {
36             'odl_vpp_netvirt': 'neutron-opendaylight-netvirt-vpp.yaml',
37             'dvr': 'neutron-opendaylight-fdio-dvr.yaml',
38             'default': 'neutron-opendaylight-honeycomb.yaml'
39         },
40         'default': 'neutron-opendaylight.yaml',
41     },
42     'onos': {
43         'sfc': 'neutron-onos-sfc.yaml',
44         'default': 'neutron-onos.yaml'
45     },
46     'ovn': 'neutron-ml2-ovn.yaml',
47     False: {
48         'vpp': 'neutron-ml2-vpp.yaml',
49         'dataplane': ('ovs_dpdk', 'neutron-ovs-dpdk.yaml')
50     }
51 }
52
53 OTHER_FILE_MAP = {
54     'tacker': 'enable_tacker.yaml',
55     'congress': 'enable_congress.yaml',
56     'barometer': 'enable_barometer.yaml',
57     'rt_kvm': 'enable_rt_kvm.yaml'
58 }
59
60 OVS_PERF_MAP = {
61     'HostCpusList': 'dpdk_cores',
62     'NeutronDpdkCoreList': 'pmd_cores',
63     'NeutronDpdkSocketMemory': 'socket_memory',
64     'NeutronDpdkMemoryChannels': 'memory_channels'
65 }
66
67 OVS_NSH_KMOD_RPM = "openvswitch-kmod-2.6.1-1.el7.centos.x86_64.rpm"
68 OVS_NSH_RPM = "openvswitch-2.6.1-1.el7.centos.x86_64.rpm"
69 ODL_NETVIRT_VPP_RPM = "/root/opendaylight-7.0.0-0.1.20170531snap665.el7" \
70                       ".noarch.rpm"
71
72
73 def build_sdn_env_list(ds, sdn_map, env_list=None):
74     if env_list is None:
75         env_list = list()
76     for k, v in sdn_map.items():
77         if ds['sdn_controller'] == k or (k in ds and ds[k] is True):
78             if isinstance(v, dict):
79                 env_list.extend(build_sdn_env_list(ds, v))
80             else:
81                 env_list.append(os.path.join(con.THT_ENV_DIR, v))
82         elif isinstance(v, tuple):
83                 if ds[k] == v[0]:
84                     env_list.append(os.path.join(con.THT_ENV_DIR, v[1]))
85     if len(env_list) == 0:
86         try:
87             env_list.append(os.path.join(
88                 con.THT_ENV_DIR, sdn_map['default']))
89         except KeyError:
90             logging.warning("Unable to find default file for SDN")
91
92     return env_list
93
94
95 def create_deploy_cmd(ds, ns, inv, tmp_dir,
96                       virtual, env_file='opnfv-environment.yaml'):
97
98     logging.info("Creating deployment command")
99     deploy_options = [env_file, 'network-environment.yaml']
100     ds_opts = ds['deploy_options']
101     deploy_options += build_sdn_env_list(ds_opts, SDN_FILE_MAP)
102
103     for k, v in OTHER_FILE_MAP.items():
104         if k in ds_opts and ds_opts[k]:
105             deploy_options.append(os.path.join(con.THT_ENV_DIR, v))
106
107     if ds_opts['ceph']:
108         prep_storage_env(ds, tmp_dir)
109         deploy_options.append(os.path.join(con.THT_ENV_DIR,
110                                            'storage-environment.yaml'))
111     if ds['global_params']['ha_enabled']:
112         deploy_options.append(os.path.join(con.THT_ENV_DIR,
113                                            'puppet-pacemaker.yaml'))
114
115     if virtual:
116         deploy_options.append('virtual-environment.yaml')
117     else:
118         deploy_options.append('baremetal-environment.yaml')
119
120     num_control, num_compute = inv.get_node_counts()
121     if num_control == 0 or num_compute == 0:
122         logging.error("Detected 0 control or compute nodes.  Control nodes: "
123                       "{}, compute nodes{}".format(num_control, num_compute))
124         raise ApexDeployException("Invalid number of control or computes")
125     elif num_control > 1 and not ds['global_params']['ha_enabled']:
126         num_control = 1
127     cmd = "openstack overcloud deploy --templates --timeout {} " \
128           "--libvirt-type kvm".format(con.DEPLOY_TIMEOUT)
129     # build cmd env args
130     for option in deploy_options:
131         cmd += " -e {}".format(option)
132     cmd += " --ntp-server {}".format(ns['ntp'][0])
133     cmd += " --control-scale {}".format(num_control)
134     cmd += " --compute-scale {}".format(num_compute)
135     cmd += ' --control-flavor control --compute-flavor compute'
136     logging.info("Deploy command set: {}".format(cmd))
137
138     with open(os.path.join(tmp_dir, 'deploy_command'), 'w') as fh:
139         fh.write(cmd)
140     return cmd
141
142
143 def prep_image(ds, img, tmp_dir, root_pw=None):
144     """
145     Locates sdn image and preps for deployment.
146     :param ds: deploy settings
147     :param img: sdn image
148     :param tmp_dir: dir to store modified sdn image
149     :param root_pw: password to configure for overcloud image
150     :return: None
151     """
152     # TODO(trozet): Come up with a better way to organize this logic in this
153     # function
154     logging.info("Preparing image: {} for deployment".format(img))
155     if not os.path.isfile(img):
156         logging.error("Missing SDN image {}".format(img))
157         raise ApexDeployException("Missing SDN image file: {}".format(img))
158
159     ds_opts = ds['deploy_options']
160     virt_cmds = list()
161     sdn = ds_opts['sdn_controller']
162     # we need this due to rhbz #1436021
163     # fixed in systemd-219-37.el7
164     if sdn is not False:
165         logging.info("Neutron openvswitch-agent disabled")
166         virt_cmds.extend([{
167             con.VIRT_RUN_CMD:
168                 "rm -f /etc/systemd/system/multi-user.target.wants/"
169                 "neutron-openvswitch-agent.service"},
170             {
171             con.VIRT_RUN_CMD:
172                 "rm -f /usr/lib/systemd/system/neutron-openvswitch-agent"
173                 ".service"
174         }])
175
176     if ds_opts['vpn']:
177         virt_cmds.append({con.VIRT_RUN_CMD: "chmod +x /etc/rc.d/rc.local"})
178         virt_cmds.append({
179             con.VIRT_RUN_CMD:
180                 "echo '/opt/quagga/etc/init.d/zrpcd start' > "
181                 "/opt/quagga/etc/init.d/zrpcd_start.sh"})
182         virt_cmds.append({
183             con.VIRT_RUN_CMD: "chmod +x /opt/quagga/etc/init.d/"
184                               "zrpcd_start.sh"})
185         virt_cmds.append({
186             con.VIRT_RUN_CMD: "sed -i '$a /opt/quagga/etc/"
187                               "init.d/zrpcd_start.sh' /etc/rc.local "})
188         logging.info("ZRPCD process started")
189
190     dataplane = ds_opts['dataplane']
191     if dataplane == 'ovs_dpdk' or dataplane == 'fdio':
192         logging.info("Enabling kernel modules for dpdk")
193         # file to module mapping
194         uio_types = {
195             os.path.join(tmp_dir, 'vfio_pci.modules'): 'vfio_pci',
196             os.path.join(tmp_dir, 'uio_pci_generic.modules'): 'uio_pci_generic'
197         }
198         for mod_file, mod in uio_types.items():
199             with open(mod_file, 'w') as fh:
200                 fh.write('#!/bin/bash\n')
201                 fh.write('exec /sbin/modprobe {}'.format(mod))
202                 fh.close()
203
204             virt_cmds.extend([
205                 {con.VIRT_UPLOAD: "{}:/etc/sysconfig/modules/".format(
206                     mod_file)},
207                 {con.VIRT_RUN_CMD: "chmod 0755 /etc/sysconfig/modules/"
208                                    "{}".format(os.path.basename(mod_file))}
209             ])
210     if root_pw:
211         pw_op = "password:{}".format(root_pw)
212         virt_cmds.append({con.VIRT_PW: pw_op})
213     if ds_opts['sfc'] and dataplane == 'ovs':
214         virt_cmds.extend([
215             {con.VIRT_RUN_CMD: "yum -y install "
216                                "/root/ovs/rpm/rpmbuild/RPMS/x86_64/"
217                                "{}".format(OVS_NSH_KMOD_RPM)},
218             {con.VIRT_RUN_CMD: "yum downgrade -y "
219                                "/root/ovs/rpm/rpmbuild/RPMS/x86_64/"
220                                "{}".format(OVS_NSH_RPM)}
221         ])
222     if dataplane == 'fdio':
223         # Patch neutron with using OVS external interface for router
224         # and add generic linux NS interface driver
225         virt_cmds.append(
226             {con.VIRT_RUN_CMD: "cd /usr/lib/python2.7/site-packages && patch "
227                                "-p1 < neutron-patch-NSDriver.patch"})
228         if sdn is False:
229             virt_cmds.extend([
230                 {con.VIRT_RUN_CMD: "yum remove -y vpp-lib"},
231                 {con.VIRT_RUN_CMD: "yum install -y "
232                                    "/root/nosdn_vpp_rpms/*.rpm"}
233             ])
234
235     if sdn == 'opendaylight':
236         if ds_opts['odl_version'] != con.DEFAULT_ODL_VERSION:
237             virt_cmds.extend([
238                 {con.VIRT_RUN_CMD: "yum -y remove opendaylight"},
239                 {con.VIRT_RUN_CMD: "rm -rf /etc/puppet/modules/opendaylight"},
240                 {con.VIRT_RUN_CMD: "cd /etc/puppet/modules && tar xzf "
241                                    "/root/puppet-opendaylight-"
242                                    "{}.tar.gz".format(ds_opts['odl_version'])}
243             ])
244             if ds_opts['odl_version'] == 'master':
245                 virt_cmds.extend([
246                     {con.VIRT_RUN_CMD: "rpm -ivh --nodeps /root/{}/*".format(
247                         ds_opts['odl_version'])}
248                 ])
249             else:
250                 virt_cmds.extend([
251                     {con.VIRT_RUN_CMD: "yum -y install /root/{}/*".format(
252                         ds_opts['odl_version'])}
253                 ])
254
255         elif sdn == 'opendaylight' and 'odl_vpp_netvirt' in ds_opts \
256                 and ds_opts['odl_vpp_netvirt']:
257             virt_cmds.extend([
258                 {con.VIRT_RUN_CMD: "yum -y remove opendaylight"},
259                 {con.VIRT_RUN_CMD: "yum -y install /root/{}/*".format(
260                     ODL_NETVIRT_VPP_RPM)}
261             ])
262
263     if sdn == 'ovn':
264         virt_cmds.extend([
265             {con.VIRT_RUN_CMD: "cd /root/ovs28 && yum update -y "
266                                "*openvswitch*"},
267             {con.VIRT_RUN_CMD: "cd /root/ovs28 && yum downgrade -y "
268                                "*openvswitch*"}
269         ])
270
271     tmp_oc_image = os.path.join(tmp_dir, 'overcloud-full.qcow2')
272     shutil.copyfile(img, tmp_oc_image)
273     logging.debug("Temporary overcloud image stored as: {}".format(
274         tmp_oc_image))
275     virt_utils.virt_customize(virt_cmds, tmp_oc_image)
276     logging.info("Overcloud image customization complete")
277
278
279 def make_ssh_key():
280     """
281     Creates public and private ssh keys with 1024 bit RSA encryption
282     :return: private, public key
283     """
284     key = rsa.generate_private_key(
285         backend=crypto_default_backend(),
286         public_exponent=65537,
287         key_size=1024
288     )
289
290     private_key = key.private_bytes(
291         crypto_serialization.Encoding.PEM,
292         crypto_serialization.PrivateFormat.PKCS8,
293         crypto_serialization.NoEncryption())
294     public_key = key.public_key().public_bytes(
295         crypto_serialization.Encoding.OpenSSH,
296         crypto_serialization.PublicFormat.OpenSSH
297     )
298     return private_key.decode('utf-8'), public_key.decode('utf-8')
299
300
301 def prep_env(ds, ns, inv, opnfv_env, net_env, tmp_dir):
302     """
303     Creates modified opnfv/network environments for deployment
304     :param ds: deploy settings
305     :param ns: network settings
306     :param inv: node inventory
307     :param opnfv_env: file path for opnfv-environment file
308     :param net_env: file path for network-environment file
309     :param tmp_dir: Apex tmp dir
310     :return:
311     """
312
313     logging.info("Preparing opnfv-environment and network-environment files")
314     ds_opts = ds['deploy_options']
315     tmp_opnfv_env = os.path.join(tmp_dir, os.path.basename(opnfv_env))
316     shutil.copyfile(opnfv_env, tmp_opnfv_env)
317     tenant_nic_map = ns['networks']['tenant']['nic_mapping']
318     tenant_ctrl_nic = tenant_nic_map['controller']['members'][0]
319     tenant_comp_nic = tenant_nic_map['compute']['members'][0]
320
321     # SSH keys
322     private_key, public_key = make_ssh_key()
323
324     # Make easier/faster variables to index in the file editor
325     if 'performance' in ds_opts:
326         perf = True
327         # vpp
328         if 'vpp' in ds_opts['performance']['Compute']:
329             perf_vpp_comp = ds_opts['performance']['Compute']['vpp']
330         else:
331             perf_vpp_comp = None
332         if 'vpp' in ds_opts['performance']['Controller']:
333             perf_vpp_ctrl = ds_opts['performance']['Controller']['vpp']
334         else:
335             perf_vpp_ctrl = None
336
337         # ovs
338         if 'ovs' in ds_opts['performance']['Compute']:
339             perf_ovs_comp = ds_opts['performance']['Compute']['ovs']
340         else:
341             perf_ovs_comp = None
342
343         # kernel
344         if 'kernel' in ds_opts['performance']['Compute']:
345             perf_kern_comp = ds_opts['performance']['Compute']['kernel']
346         else:
347             perf_kern_comp = None
348     else:
349         perf = False
350
351     # Modify OPNFV environment
352     # TODO: Change to build a dict and outputing yaml rather than parsing
353     for line in fileinput.input(tmp_opnfv_env, inplace=True):
354         line = line.strip('\n')
355         output_line = line
356         if 'CloudDomain' in line:
357             output_line = "  CloudDomain: {}".format(ns['domain_name'])
358         elif 'replace_private_key' in line:
359             output_line = "    private_key: |\n"
360             key_out = ''
361             for line in private_key.splitlines():
362                 key_out += "      {}\n".format(line)
363             output_line += key_out
364         elif 'replace_public_key' in line:
365             output_line = "    public_key: '{}'".format(public_key)
366
367         if ds_opts['sdn_controller'] == 'opendaylight' and \
368                 'odl_vpp_routing_node' in ds_opts:
369             if 'opendaylight::vpp_routing_node' in line:
370                 output_line = ("    opendaylight::vpp_routing_node: {}.{}"
371                                .format(ds_opts['odl_vpp_routing_node'],
372                                        ns['domain_name']))
373             elif 'ControllerExtraConfig' in line:
374                 output_line = ("  ControllerExtraConfig:\n    "
375                                "tripleo::profile::base::neutron::agents::"
376                                "honeycomb::interface_role_mapping:"
377                                " ['{}:tenant-interface]'"
378                                .format(tenant_ctrl_nic))
379             elif 'NovaComputeExtraConfig' in line:
380                 output_line = ("  NovaComputeExtraConfig:\n    "
381                                "tripleo::profile::base::neutron::agents::"
382                                "honeycomb::interface_role_mapping:"
383                                " ['{}:tenant-interface]'"
384                                .format(tenant_comp_nic))
385         elif not ds_opts['sdn_controller'] and ds_opts['dataplane'] == 'fdio':
386             if 'NeutronVPPAgentPhysnets' in line:
387                 output_line = ("  NeutronVPPAgentPhysnets: 'datacentre:{}'".
388                                format(tenant_ctrl_nic))
389         elif ds_opts['sdn_controller'] == 'opendaylight' and ds_opts.get(
390                 'dvr') is True:
391             if 'OS::TripleO::Services::NeutronDhcpAgent' in line:
392                 output_line = ''
393             elif 'NeutronDhcpAgentsPerNetwork' in line:
394                 num_control, num_compute = inv.get_node_counts()
395                 output_line = ("  NeutronDhcpAgentsPerNetwork: {}"
396                                .format(num_compute))
397             elif 'ComputeServices' in line:
398                 output_line = ("  ComputeServices:\n"
399                                "    - OS::TripleO::Services::NeutronDhcpAgent")
400
401         if perf:
402             for role in 'NovaCompute', 'Controller':
403                 if role == 'NovaCompute':
404                     perf_opts = perf_vpp_comp
405                 else:
406                     perf_opts = perf_vpp_ctrl
407                 cfg = "{}ExtraConfig".format(role)
408                 if cfg in line and perf_opts:
409                     perf_line = ''
410                     if 'main-core' in perf_opts:
411                         perf_line += ("\n    fdio::vpp_cpu_main_core: '{}'"
412                                       .format(perf_opts['main-core']))
413                     if 'corelist-workers' in perf_opts:
414                         perf_line += ("\n    "
415                                       "fdio::vpp_cpu_corelist_workers: '{}'"
416                                       .format(perf_opts['corelist-workers']))
417                     if perf_line:
418                         output_line = ("  {}:{}".format(cfg, perf_line))
419
420             # kernel args
421             # (FIXME) use compute's kernel settings for all nodes for now.
422             if 'ComputeKernelArgs' in line and perf_kern_comp:
423                 kernel_args = ''
424                 for k, v in perf_kern_comp.items():
425                     kernel_args += "{}={} ".format(k, v)
426                 if kernel_args:
427                     output_line = "  ComputeKernelArgs: '{}'".\
428                         format(kernel_args)
429             if ds_opts['dataplane'] == 'ovs_dpdk' and perf_ovs_comp:
430                 for k, v in OVS_PERF_MAP.items():
431                     if k in line and v in perf_ovs_comp:
432                         output_line = "  {}: '{}'".format(k, perf_ovs_comp[v])
433
434         print(output_line)
435
436     logging.info("opnfv-environment file written to {}".format(tmp_opnfv_env))
437
438     # Modify Network environment
439     for line in fileinput.input(net_env, inplace=True):
440         line = line.strip('\n')
441         if 'ComputeExtraConfigPre' in line and \
442                 ds_opts['dataplane'] == 'ovs_dpdk':
443             print('  OS::TripleO::ComputeExtraConfigPre: '
444                   './ovs-dpdk-preconfig.yaml')
445         elif ((perf and perf_kern_comp) or ds_opts.get('rt_kvm')) and \
446                 'resource_registry' in line:
447             print("resource_registry:\n"
448                   "  OS::TripleO::NodeUserData: first-boot.yaml")
449         elif perf and perf_kern_comp and \
450                 'NovaSchedulerDefaultFilters' in line:
451             print("  NovaSchedulerDefaultFilters: 'RamFilter,"
452                   "ComputeFilter,AvailabilityZoneFilter,"
453                   "ComputeCapabilitiesFilter,ImagePropertiesFilter,"
454                   "NUMATopologyFilter'")
455         else:
456             print(line)
457
458     logging.info("network-environment file written to {}".format(net_env))
459
460
461 def generate_ceph_key():
462     key = os.urandom(16)
463     header = struct.pack('<hiih', 1, int(time.time()), 0, len(key))
464     return base64.b64encode(header + key)
465
466
467 def prep_storage_env(ds, tmp_dir):
468     """
469     Creates storage environment file for deployment.  Source file is copied by
470     undercloud playbook to host.
471     :param ds:
472     :param tmp_dir:
473     :return:
474     """
475     ds_opts = ds['deploy_options']
476     storage_file = os.path.join(tmp_dir, 'storage-environment.yaml')
477     if not os.path.isfile(storage_file):
478         logging.error("storage-environment file is not in tmp directory: {}. "
479                       "Check if file was copied from "
480                       "undercloud".format(tmp_dir))
481         raise ApexDeployException("storage-environment file not copied from "
482                                   "undercloud")
483     for line in fileinput.input(storage_file, inplace=True):
484         line = line.strip('\n')
485         if 'CephClusterFSID' in line:
486             print("  CephClusterFSID: {}".format(str(uuid.uuid4())))
487         elif 'CephMonKey' in line:
488             print("  CephMonKey: {}".format(generate_ceph_key().decode(
489                 'utf-8')))
490         elif 'CephAdminKey' in line:
491             print("  CephAdminKey: {}".format(generate_ceph_key().decode(
492                 'utf-8')))
493         else:
494             print(line)
495     if 'ceph_device' in ds_opts and ds_opts['ceph_device']:
496         with open(storage_file, 'a') as fh:
497             fh.write('  ExtraConfig:\n')
498             fh.write("    ceph::profile::params::osds:{{{}:{{}}}}\n".format(
499                 ds_opts['ceph_device']
500             ))
501
502
503 def external_network_cmds(ns):
504     """
505     Generates external network openstack commands
506     :param ns: network settings
507     :return: list of commands to configure external network
508     """
509     if 'external' in ns.enabled_network_list:
510         net_config = ns['networks']['external'][0]
511         external = True
512         pool_start, pool_end = net_config['floating_ip_range']
513     else:
514         net_config = ns['networks']['admin']
515         external = False
516         pool_start, pool_end = ns['apex']['networks']['admin'][
517             'introspection_range']
518     nic_config = net_config['nic_mapping']
519     gateway = net_config['gateway']
520     cmds = list()
521     # create network command
522     if nic_config['compute']['vlan'] == 'native':
523         ext_type = 'flat'
524     else:
525         ext_type = "vlan --provider-segment {}".format(nic_config[
526                                                        'compute']['vlan'])
527     cmds.append("openstack network create external --project service "
528                 "--external --provider-network-type {} "
529                 "--provider-physical-network datacentre".format(ext_type))
530     # create subnet command
531     cidr = net_config['cidr']
532     subnet_cmd = "openstack subnet create external-subnet --project " \
533                  "service --network external --no-dhcp --gateway {} " \
534                  "--allocation-pool start={},end={} --subnet-range " \
535                  "{}".format(gateway, pool_start, pool_end, str(cidr))
536     if external and cidr.version == 6:
537         subnet_cmd += ' --ip-version 6 --ipv6-ra-mode slaac ' \
538                       '--ipv6-address-mode slaac'
539     cmds.append(subnet_cmd)
540     logging.debug("Neutron external network commands determined "
541                   "as: {}".format(cmds))
542     return cmds
543
544
545 def create_congress_cmds(overcloud_file):
546     drivers = ['nova', 'neutronv2', 'cinder', 'glancev2', 'keystone', 'doctor']
547     overcloudrc = parsers.parse_overcloudrc(overcloud_file)
548     logging.info("Creating congress commands")
549     try:
550         ds_cfg = [
551             "username={}".format(overcloudrc['OS_USERNAME']),
552             "tenant_name={}".format(overcloudrc['OS_PROJECT_NAME']),
553             "password={}".format(overcloudrc['OS_PASSWORD']),
554             "auth_url={}".format(overcloudrc['OS_AUTH_URL'])
555         ]
556     except KeyError:
557         logging.error("Unable to find all keys required for congress in "
558                       "overcloudrc: OS_USERNAME, OS_PROJECT_NAME, "
559                       "OS_PASSWORD, OS_AUTH_URL.  Please check overcloudrc "
560                       "file: {}".format(overcloud_file))
561         raise
562     cmds = list()
563     ds_cfg = '--config ' + ' --config '.join(ds_cfg)
564
565     for driver in drivers:
566         if driver == 'doctor':
567             cmd = "{} \"{}\"".format(driver, driver)
568         else:
569             cmd = "{} \"{}\" {}".format(driver, driver, ds_cfg)
570         if driver == 'nova':
571             cmd += ' --config api_version="2.34"'
572         logging.debug("Congress command created: {}".format(cmd))
573         cmds.append(cmd)
574     return cmds