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