1e384a6e80bcfbf0cbb004c14c003ea28900e2d0
[apex.git] / apex / deploy.py
1 #!/usr/bin/env python
2
3 ##############################################################################
4 # Copyright (c) 2017 Tim Rozet (trozet@redhat.com) and others.
5 #
6 # All rights reserved. This program and the accompanying materials
7 # are made available under the terms of the Apache License, Version 2.0
8 # which accompanies this distribution, and is available at
9 # http://www.apache.org/licenses/LICENSE-2.0
10 ##############################################################################
11
12 import argparse
13 import json
14 import logging
15 import os
16 import platform
17 import pprint
18 import shutil
19 import sys
20 import tempfile
21
22 import apex.virtual.configure_vm as vm_lib
23 import apex.virtual.utils as virt_utils
24 import apex.builders.common_builder as c_builder
25 import apex.builders.overcloud_builder as oc_builder
26 import apex.builders.undercloud_builder as uc_builder
27 from apex import DeploySettings
28 from apex import Inventory
29 from apex import NetworkEnvironment
30 from apex import NetworkSettings
31 from apex.common import utils
32 from apex.common import constants
33 from apex.common import parsers
34 from apex.common.exceptions import ApexDeployException
35 from apex.network import jumphost
36 from apex.network import network_data
37 from apex.undercloud import undercloud as uc_lib
38 from apex.overcloud import config as oc_cfg
39 from apex.overcloud import deploy as oc_deploy
40
41 APEX_TEMP_DIR = tempfile.mkdtemp(prefix='apex_tmp')
42 SDN_IMAGE = 'overcloud-full-opendaylight.qcow2'
43
44
45 def deploy_quickstart(args, deploy_settings_file, network_settings_file,
46                       inventory_file=None):
47     pass
48
49
50 def validate_cross_settings(deploy_settings, net_settings, inventory):
51     """
52     Used to validate compatibility across settings file.
53     :param deploy_settings: parsed settings for deployment
54     :param net_settings: parsed settings for network
55     :param inventory: parsed inventory file
56     :return: None
57     """
58
59     if deploy_settings['deploy_options']['dataplane'] != 'ovs' and 'tenant' \
60             not in net_settings.enabled_network_list:
61         raise ApexDeployException("Setting a DPDK based dataplane requires"
62                                   "a dedicated NIC for tenant network")
63
64     if 'odl_vpp_routing_node' in deploy_settings['deploy_options']:
65         if deploy_settings['deploy_options']['dataplane'] != 'fdio':
66             raise ApexDeployException("odl_vpp_routing_node should only be set"
67                                       "when dataplane is set to fdio")
68         if deploy_settings['deploy_options'].get('dvr') is True:
69             raise ApexDeployException("odl_vpp_routing_node should only be set"
70                                       "when dvr is not enabled")
71
72     # TODO(trozet): add more checks here like RAM for ODL, etc
73     # check if odl_vpp_netvirt is true and vpp is set
74     # Check if fdio and nosdn:
75     # tenant_nic_mapping_controller_members" ==
76     # "$tenant_nic_mapping_compute_members
77
78
79 def build_vms(inventory, network_settings,
80               template_dir='/usr/share/opnfv-apex'):
81     """
82     Creates VMs and configures vbmc and host
83     :param inventory:
84     :param network_settings:
85     :return:
86     """
87
88     for idx, node in enumerate(inventory['nodes']):
89         name = 'baremetal{}'.format(idx)
90         volume = name + ".qcow2"
91         volume_path = os.path.join(constants.LIBVIRT_VOLUME_PATH, volume)
92         # TODO(trozet): add error checking
93         vm_lib.create_vm(
94             name, volume_path,
95             baremetal_interfaces=network_settings.enabled_network_list,
96             memory=node['memory'], cpus=node['cpu'],
97             macs=node['mac'],
98             template_dir=template_dir)
99         virt_utils.host_setup({name: node['pm_port']})
100
101
102 def create_deploy_parser():
103     deploy_parser = argparse.ArgumentParser()
104     deploy_parser.add_argument('--debug', action='store_true', default=False,
105                                help="Turn on debug messages")
106     deploy_parser.add_argument('-l', '--log-file',
107                                default='./apex_deploy.log',
108                                dest='log_file', help="Log file to log to")
109     deploy_parser.add_argument('-d', '--deploy-settings',
110                                dest='deploy_settings_file',
111                                required=True,
112                                help='File which contains Apex deploy settings')
113     deploy_parser.add_argument('-n', '--network-settings',
114                                dest='network_settings_file',
115                                required=True,
116                                help='File which contains Apex network '
117                                     'settings')
118     deploy_parser.add_argument('-i', '--inventory-file',
119                                dest='inventory_file',
120                                default=None,
121                                help='Inventory file which contains POD '
122                                     'definition')
123     deploy_parser.add_argument('-e', '--environment-file',
124                                dest='env_file',
125                                default='opnfv-environment.yaml',
126                                help='Provide alternate base env file located '
127                                     'in deploy_dir')
128     deploy_parser.add_argument('-v', '--virtual', action='store_true',
129                                default=False,
130                                dest='virtual',
131                                help='Enable virtual deployment')
132     deploy_parser.add_argument('--interactive', action='store_true',
133                                default=False,
134                                help='Enable interactive deployment mode which '
135                                     'requires user to confirm steps of '
136                                     'deployment')
137     deploy_parser.add_argument('--virtual-computes',
138                                dest='virt_compute_nodes',
139                                default=1,
140                                type=int,
141                                help='Number of Virtual Compute nodes to create'
142                                     ' and use during deployment (defaults to 1'
143                                     ' for noha and 2 for ha)')
144     deploy_parser.add_argument('--virtual-cpus',
145                                dest='virt_cpus',
146                                default=4,
147                                type=int,
148                                help='Number of CPUs to use per Overcloud VM in'
149                                     ' a virtual deployment (defaults to 4)')
150     deploy_parser.add_argument('--virtual-default-ram',
151                                dest='virt_default_ram',
152                                default=8,
153                                type=int,
154                                help='Amount of default RAM to use per '
155                                     'Overcloud VM in GB (defaults to 8).')
156     deploy_parser.add_argument('--virtual-compute-ram',
157                                dest='virt_compute_ram',
158                                default=None,
159                                type=int,
160                                help='Amount of RAM to use per Overcloud '
161                                     'Compute VM in GB (defaults to 8). '
162                                     'Overrides --virtual-default-ram arg for '
163                                     'computes')
164     deploy_parser.add_argument('--deploy-dir',
165                                default='/usr/share/opnfv-apex',
166                                help='Directory to deploy from which contains '
167                                     'base config files for deployment')
168     deploy_parser.add_argument('--image-dir',
169                                default='/var/opt/opnfv/images',
170                                help='Directory which contains '
171                                     'base disk images for deployment')
172     deploy_parser.add_argument('--lib-dir',
173                                default='/usr/share/opnfv-apex',
174                                help='Directory path for apex ansible '
175                                     'and third party libs')
176     deploy_parser.add_argument('--quickstart', action='store_true',
177                                default=False,
178                                help='Use tripleo-quickstart to deploy')
179     deploy_parser.add_argument('--upstream', action='store_true',
180                                default=False,
181                                help='Force deployment to use upstream '
182                                     'artifacts')
183     deploy_parser.add_argument('--no-fetch', action='store_true',
184                                default=False,
185                                help='Ignore fetching latest upstream and '
186                                     'use what is in cache')
187     return deploy_parser
188
189
190 def validate_deploy_args(args):
191     """
192     Validates arguments for deploy
193     :param args:
194     :return: None
195     """
196
197     logging.debug('Validating arguments for deployment')
198     if args.virtual and args.inventory_file is not None:
199         logging.error("Virtual enabled but inventory file also given")
200         raise ApexDeployException('You should not specify an inventory file '
201                                   'with virtual deployments')
202     elif args.virtual:
203         args.inventory_file = os.path.join(APEX_TEMP_DIR,
204                                            'inventory-virt.yaml')
205     elif os.path.isfile(args.inventory_file) is False:
206         logging.error("Specified inventory file does not exist: {}".format(
207             args.inventory_file))
208         raise ApexDeployException('Specified inventory file does not exist')
209
210     for settings_file in (args.deploy_settings_file,
211                           args.network_settings_file):
212         if os.path.isfile(settings_file) is False:
213             logging.error("Specified settings file does not "
214                           "exist: {}".format(settings_file))
215             raise ApexDeployException('Specified settings file does not '
216                                       'exist: {}'.format(settings_file))
217
218
219 def main():
220     parser = create_deploy_parser()
221     args = parser.parse_args(sys.argv[1:])
222     # FIXME (trozet): this is only needed as a workaround for CI.  Remove
223     # when CI is changed
224     if os.getenv('IMAGES', False):
225         args.image_dir = os.getenv('IMAGES')
226     if args.debug:
227         log_level = logging.DEBUG
228     else:
229         log_level = logging.INFO
230     os.makedirs(os.path.dirname(args.log_file), exist_ok=True)
231     formatter = '%(asctime)s %(levelname)s: %(message)s'
232     logging.basicConfig(filename=args.log_file,
233                         format=formatter,
234                         datefmt='%m/%d/%Y %I:%M:%S %p',
235                         level=log_level)
236     console = logging.StreamHandler()
237     console.setLevel(log_level)
238     console.setFormatter(logging.Formatter(formatter))
239     logging.getLogger('').addHandler(console)
240     utils.install_ansible()
241     validate_deploy_args(args)
242     # Parse all settings
243     deploy_settings = DeploySettings(args.deploy_settings_file)
244     logging.info("Deploy settings are:\n {}".format(pprint.pformat(
245                  deploy_settings)))
246     net_settings = NetworkSettings(args.network_settings_file)
247     logging.info("Network settings are:\n {}".format(pprint.pformat(
248                  net_settings)))
249     os_version = deploy_settings['deploy_options']['os_version']
250     net_env_file = os.path.join(args.deploy_dir, constants.NET_ENV_FILE)
251     net_env = NetworkEnvironment(net_settings, net_env_file,
252                                  os_version=os_version)
253     net_env_target = os.path.join(APEX_TEMP_DIR, constants.NET_ENV_FILE)
254     utils.dump_yaml(dict(net_env), net_env_target)
255
256     # get global deploy params
257     ha_enabled = deploy_settings['global_params']['ha_enabled']
258     introspect = deploy_settings['global_params'].get('introspect', True)
259
260     if args.virtual:
261         if args.virt_compute_ram is None:
262             compute_ram = args.virt_default_ram
263         else:
264             compute_ram = args.virt_compute_ram
265         if deploy_settings['deploy_options']['sdn_controller'] == \
266                 'opendaylight' and args.virt_default_ram < 12:
267             control_ram = 12
268             logging.warning('RAM per controller is too low.  OpenDaylight '
269                             'requires at least 12GB per controller.')
270             logging.info('Increasing RAM per controller to 12GB')
271         elif args.virt_default_ram < 10:
272             control_ram = 10
273             logging.warning('RAM per controller is too low.  nosdn '
274                             'requires at least 10GB per controller.')
275             logging.info('Increasing RAM per controller to 10GB')
276         else:
277             control_ram = args.virt_default_ram
278         if ha_enabled and args.virt_compute_nodes < 2:
279             logging.debug('HA enabled, bumping number of compute nodes to 2')
280             args.virt_compute_nodes = 2
281         virt_utils.generate_inventory(args.inventory_file, ha_enabled,
282                                       num_computes=args.virt_compute_nodes,
283                                       controller_ram=control_ram * 1024,
284                                       compute_ram=compute_ram * 1024,
285                                       vcpus=args.virt_cpus
286                                       )
287     inventory = Inventory(args.inventory_file, ha_enabled, args.virtual)
288
289     validate_cross_settings(deploy_settings, net_settings, inventory)
290     ds_opts = deploy_settings['deploy_options']
291     if args.quickstart:
292         deploy_settings_file = os.path.join(APEX_TEMP_DIR,
293                                             'apex_deploy_settings.yaml')
294         utils.dump_yaml(utils.dict_objects_to_str(deploy_settings),
295                         deploy_settings_file)
296         logging.info("File created: {}".format(deploy_settings_file))
297         network_settings_file = os.path.join(APEX_TEMP_DIR,
298                                              'apex_network_settings.yaml')
299         utils.dump_yaml(utils.dict_objects_to_str(net_settings),
300                         network_settings_file)
301         logging.info("File created: {}".format(network_settings_file))
302         deploy_quickstart(args, deploy_settings_file, network_settings_file,
303                           args.inventory_file)
304     else:
305         # TODO (trozet): add logic back from:
306         # Iedb75994d35b5dc1dd5d5ce1a57277c8f3729dfd (FDIO DVR)
307         ansible_args = {
308             'virsh_enabled_networks': net_settings.enabled_network_list
309         }
310         utils.run_ansible(ansible_args,
311                           os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
312                                        'deploy_dependencies.yml'))
313         uc_external = False
314         if 'external' in net_settings.enabled_network_list:
315             uc_external = True
316         if args.virtual:
317             # create all overcloud VMs
318             build_vms(inventory, net_settings, args.deploy_dir)
319         else:
320             # Attach interfaces to jumphost for baremetal deployment
321             jump_networks = ['admin']
322             if uc_external:
323                 jump_networks.append('external')
324             for network in jump_networks:
325                 if network == 'external':
326                     # TODO(trozet): enable vlan secondary external networks
327                     iface = net_settings['networks'][network][0][
328                         'installer_vm']['members'][0]
329                 else:
330                     iface = net_settings['networks'][network]['installer_vm'][
331                         'members'][0]
332                 bridge = "br-{}".format(network)
333                 jumphost.attach_interface_to_ovs(bridge, iface, network)
334         instackenv_json = os.path.join(APEX_TEMP_DIR, 'instackenv.json')
335         with open(instackenv_json, 'w') as fh:
336             json.dump(inventory, fh)
337
338         # Create and configure undercloud
339         if args.debug:
340             root_pw = constants.DEBUG_OVERCLOUD_PW
341         else:
342             root_pw = None
343
344         upstream = (os_version != constants.DEFAULT_OS_VERSION or
345                     args.upstream)
346         if os_version == 'master':
347             branch = 'master'
348         else:
349             branch = "stable/{}".format(os_version)
350         if upstream:
351             logging.info("Deploying with upstream artifacts for OpenStack "
352                          "{}".format(os_version))
353             args.image_dir = os.path.join(args.image_dir, os_version)
354             upstream_url = constants.UPSTREAM_RDO.replace(
355                 constants.DEFAULT_OS_VERSION, os_version)
356             upstream_targets = ['overcloud-full.tar', 'undercloud.qcow2']
357             utils.fetch_upstream_and_unpack(args.image_dir, upstream_url,
358                                             upstream_targets,
359                                             fetch=not args.no_fetch)
360             sdn_image = os.path.join(args.image_dir, 'overcloud-full.qcow2')
361             # copy undercloud so we don't taint upstream fetch
362             uc_image = os.path.join(args.image_dir, 'undercloud_mod.qcow2')
363             uc_fetch_img = os.path.join(args.image_dir, 'undercloud.qcow2')
364             shutil.copyfile(uc_fetch_img, uc_image)
365             # prep undercloud with required packages
366             uc_builder.add_upstream_packages(uc_image)
367             # add patches from upstream to undercloud and overcloud
368             logging.info('Adding patches to undercloud')
369             patches = deploy_settings['global_params']['patches']
370             c_builder.add_upstream_patches(patches['undercloud'], uc_image,
371                                            APEX_TEMP_DIR, branch)
372         else:
373             sdn_image = os.path.join(args.image_dir, SDN_IMAGE)
374             uc_image = 'undercloud.qcow2'
375             # patches are ignored in non-upstream deployments
376             patches = {'overcloud': [], 'undercloud': []}
377         # Create/Start Undercloud VM
378         undercloud = uc_lib.Undercloud(args.image_dir,
379                                        args.deploy_dir,
380                                        root_pw=root_pw,
381                                        external_network=uc_external,
382                                        image_name=os.path.basename(uc_image),
383                                        os_version=os_version)
384         undercloud.start()
385         undercloud_admin_ip = net_settings['networks'][
386             constants.ADMIN_NETWORK]['installer_vm']['ip']
387
388         if upstream and ds_opts['containers']:
389             tag = constants.DOCKER_TAG
390         else:
391             tag = None
392
393         # Generate nic templates
394         for role in 'compute', 'controller':
395             oc_cfg.create_nic_template(net_settings, deploy_settings, role,
396                                        args.deploy_dir, APEX_TEMP_DIR)
397         # Install Undercloud
398         undercloud.configure(net_settings, deploy_settings,
399                              os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
400                                           'configure_undercloud.yml'),
401                              APEX_TEMP_DIR, virtual_oc=args.virtual)
402
403         # Prepare overcloud-full.qcow2
404         logging.info("Preparing Overcloud for deployment...")
405         if os_version != 'ocata':
406             net_data_file = os.path.join(APEX_TEMP_DIR, 'network_data.yaml')
407             net_data = network_data.create_network_data(net_settings,
408                                                         net_data_file)
409         else:
410             net_data = False
411         if upstream and args.env_file == 'opnfv-environment.yaml':
412             # Override the env_file if it is defaulted to opnfv
413             # opnfv env file will not work with upstream
414             args.env_file = 'upstream-environment.yaml'
415         opnfv_env = os.path.join(args.deploy_dir, args.env_file)
416         if not upstream:
417             # TODO(trozet): Invoke with containers after Fraser migration
418             oc_deploy.prep_env(deploy_settings, net_settings, inventory,
419                                opnfv_env, net_env_target, APEX_TEMP_DIR)
420         else:
421             shutil.copyfile(
422                 opnfv_env,
423                 os.path.join(APEX_TEMP_DIR, os.path.basename(opnfv_env))
424             )
425         patched_containers = oc_deploy.prep_image(
426             deploy_settings, net_settings, sdn_image, APEX_TEMP_DIR,
427             root_pw=root_pw, docker_tag=tag, patches=patches['overcloud'],
428             upstream=upstream)
429
430         oc_deploy.create_deploy_cmd(deploy_settings, net_settings, inventory,
431                                     APEX_TEMP_DIR, args.virtual,
432                                     os.path.basename(opnfv_env),
433                                     net_data=net_data)
434         # Prepare undercloud with containers
435         docker_playbook = os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
436                                        'prepare_overcloud_containers.yml')
437         if ds_opts['containers']:
438             ceph_version = constants.CEPH_VERSION_MAP[ds_opts['os_version']]
439             ceph_docker_image = "ceph/daemon:tag-build-master-" \
440                                 "{}-centos-7".format(ceph_version)
441             logging.info("Preparing Undercloud with Docker containers")
442             if patched_containers:
443                 oc_builder.archive_docker_patches(APEX_TEMP_DIR)
444             container_vars = dict()
445             container_vars['apex_temp_dir'] = APEX_TEMP_DIR
446             container_vars['patched_docker_services'] = list(
447                 patched_containers)
448             container_vars['container_tag'] = constants.DOCKER_TAG
449             container_vars['stackrc'] = 'source /home/stack/stackrc'
450             container_vars['upstream'] = upstream
451             container_vars['sdn'] = ds_opts['sdn_controller']
452             container_vars['undercloud_ip'] = undercloud_admin_ip
453             container_vars['os_version'] = os_version
454             container_vars['ceph_docker_image'] = ceph_docker_image
455             container_vars['sdn_env_file'] = \
456                 oc_deploy.get_docker_sdn_file(ds_opts)
457             try:
458                 utils.run_ansible(container_vars, docker_playbook,
459                                   host=undercloud.ip, user='stack',
460                                   tmp_dir=APEX_TEMP_DIR)
461                 logging.info("Container preparation complete")
462             except Exception:
463                 logging.error("Unable to complete container prep on "
464                               "Undercloud")
465                 os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2'))
466                 raise
467
468         deploy_playbook = os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
469                                        'deploy_overcloud.yml')
470         virt_env = 'virtual-environment.yaml'
471         bm_env = 'baremetal-environment.yaml'
472         for p_env in virt_env, bm_env:
473             shutil.copyfile(os.path.join(args.deploy_dir, p_env),
474                             os.path.join(APEX_TEMP_DIR, p_env))
475
476         # Start Overcloud Deployment
477         logging.info("Executing Overcloud Deployment...")
478         deploy_vars = dict()
479         deploy_vars['virtual'] = args.virtual
480         deploy_vars['debug'] = args.debug
481         deploy_vars['aarch64'] = platform.machine() == 'aarch64'
482         deploy_vars['introspect'] = not (args.virtual or
483                                          deploy_vars['aarch64'] or
484                                          not introspect)
485         deploy_vars['dns_server_args'] = ''
486         deploy_vars['apex_temp_dir'] = APEX_TEMP_DIR
487         deploy_vars['apex_env_file'] = os.path.basename(opnfv_env)
488         deploy_vars['stackrc'] = 'source /home/stack/stackrc'
489         deploy_vars['overcloudrc'] = 'source /home/stack/overcloudrc'
490         deploy_vars['upstream'] = upstream
491         deploy_vars['undercloud_ip'] = undercloud_admin_ip
492         deploy_vars['ha_enabled'] = ha_enabled
493         deploy_vars['os_version'] = os_version
494         deploy_vars['http_proxy'] = net_settings.get('http_proxy', '')
495         deploy_vars['https_proxy'] = net_settings.get('https_proxy', '')
496         for dns_server in net_settings['dns_servers']:
497             deploy_vars['dns_server_args'] += " --dns-nameserver {}".format(
498                 dns_server)
499         try:
500             utils.run_ansible(deploy_vars, deploy_playbook, host=undercloud.ip,
501                               user='stack', tmp_dir=APEX_TEMP_DIR)
502             logging.info("Overcloud deployment complete")
503         except Exception:
504             logging.error("Deployment Failed.  Please check log")
505             raise
506         finally:
507             os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2'))
508
509         # Post install
510         logging.info("Executing post deploy configuration")
511         jumphost.configure_bridges(net_settings)
512         nova_output = os.path.join(APEX_TEMP_DIR, 'nova_output')
513         deploy_vars['overcloud_nodes'] = parsers.parse_nova_output(
514             nova_output)
515         deploy_vars['SSH_OPTIONS'] = '-o StrictHostKeyChecking=no -o ' \
516                                      'GlobalKnownHostsFile=/dev/null -o ' \
517                                      'UserKnownHostsFile=/dev/null -o ' \
518                                      'LogLevel=error'
519         deploy_vars['external_network_cmds'] = \
520             oc_deploy.external_network_cmds(net_settings, deploy_settings)
521         # TODO(trozet): just parse all ds_opts as deploy vars one time
522         deploy_vars['gluon'] = ds_opts['gluon']
523         deploy_vars['sdn'] = ds_opts['sdn_controller']
524         for dep_option in 'yardstick', 'dovetail', 'vsperf':
525             if dep_option in ds_opts:
526                 deploy_vars[dep_option] = ds_opts[dep_option]
527             else:
528                 deploy_vars[dep_option] = False
529         deploy_vars['dataplane'] = ds_opts['dataplane']
530         overcloudrc = os.path.join(APEX_TEMP_DIR, 'overcloudrc')
531         if ds_opts['congress']:
532             deploy_vars['congress_datasources'] = \
533                 oc_deploy.create_congress_cmds(overcloudrc)
534             deploy_vars['congress'] = True
535         else:
536             deploy_vars['congress'] = False
537         deploy_vars['calipso'] = ds_opts.get('calipso', False)
538         deploy_vars['calipso_ip'] = undercloud_admin_ip
539         # overcloudrc.v3 removed and set as default in queens and later
540         if os_version == 'pike':
541             deploy_vars['overcloudrc_files'] = ['overcloudrc',
542                                                 'overcloudrc.v3']
543         else:
544             deploy_vars['overcloudrc_files'] = ['overcloudrc']
545
546         post_undercloud = os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
547                                        'post_deploy_undercloud.yml')
548         logging.info("Executing post deploy configuration undercloud playbook")
549         try:
550             utils.run_ansible(deploy_vars, post_undercloud, host=undercloud.ip,
551                               user='stack', tmp_dir=APEX_TEMP_DIR)
552             logging.info("Post Deploy Undercloud Configuration Complete")
553         except Exception:
554             logging.error("Post Deploy Undercloud Configuration failed.  "
555                           "Please check log")
556             raise
557         # Post deploy overcloud node configuration
558         # TODO(trozet): just parse all ds_opts as deploy vars one time
559         deploy_vars['sfc'] = ds_opts['sfc']
560         deploy_vars['vpn'] = ds_opts['vpn']
561         deploy_vars['l2gw'] = ds_opts.get('l2gw')
562         deploy_vars['sriov'] = ds_opts.get('sriov')
563         deploy_vars['tacker'] = ds_opts.get('tacker')
564         # TODO(trozet): pull all logs and store in tmp dir in overcloud
565         # playbook
566         post_overcloud = os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
567                                       'post_deploy_overcloud.yml')
568         # Run per overcloud node
569         for node, ip in deploy_vars['overcloud_nodes'].items():
570             logging.info("Executing Post deploy overcloud playbook on "
571                          "node {}".format(node))
572             try:
573                 utils.run_ansible(deploy_vars, post_overcloud, host=ip,
574                                   user='heat-admin', tmp_dir=APEX_TEMP_DIR)
575                 logging.info("Post Deploy Overcloud Configuration Complete "
576                              "for node {}".format(node))
577             except Exception:
578                 logging.error("Post Deploy Overcloud Configuration failed "
579                               "for node {}. Please check log".format(node))
580                 raise
581         logging.info("Apex deployment complete")
582         logging.info("Undercloud IP: {}, please connect by doing "
583                      "'opnfv-util undercloud'".format(undercloud.ip))
584         # TODO(trozet): add logging here showing controller VIP and horizon url
585
586
587 if __name__ == '__main__':
588     main()