5485d150aa490d5e20a1d433f959c94a36a6dbc6
[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 from apex import DeploySettings
25 from apex import Inventory
26 from apex import NetworkEnvironment
27 from apex import NetworkSettings
28 from apex.builders import common_builder as c_builder
29 from apex.builders import overcloud_builder as oc_builder
30 from apex.builders import undercloud_builder as uc_builder
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     return deploy_parser
185
186
187 def validate_deploy_args(args):
188     """
189     Validates arguments for deploy
190     :param args:
191     :return: None
192     """
193
194     logging.debug('Validating arguments for deployment')
195     if args.virtual and args.inventory_file is not None:
196         logging.error("Virtual enabled but inventory file also given")
197         raise ApexDeployException('You should not specify an inventory file '
198                                   'with virtual deployments')
199     elif args.virtual:
200         args.inventory_file = os.path.join(APEX_TEMP_DIR,
201                                            'inventory-virt.yaml')
202     elif os.path.isfile(args.inventory_file) is False:
203         logging.error("Specified inventory file does not exist: {}".format(
204             args.inventory_file))
205         raise ApexDeployException('Specified inventory file does not exist')
206
207     for settings_file in (args.deploy_settings_file,
208                           args.network_settings_file):
209         if os.path.isfile(settings_file) is False:
210             logging.error("Specified settings file does not "
211                           "exist: {}".format(settings_file))
212             raise ApexDeployException('Specified settings file does not '
213                                       'exist: {}'.format(settings_file))
214
215
216 def main():
217     parser = create_deploy_parser()
218     args = parser.parse_args(sys.argv[1:])
219     # FIXME (trozet): this is only needed as a workaround for CI.  Remove
220     # when CI is changed
221     if os.getenv('IMAGES', False):
222         args.image_dir = os.getenv('IMAGES')
223     if args.debug:
224         log_level = logging.DEBUG
225     else:
226         log_level = logging.INFO
227     os.makedirs(os.path.dirname(args.log_file), exist_ok=True)
228     formatter = '%(asctime)s %(levelname)s: %(message)s'
229     logging.basicConfig(filename=args.log_file,
230                         format=formatter,
231                         datefmt='%m/%d/%Y %I:%M:%S %p',
232                         level=log_level)
233     console = logging.StreamHandler()
234     console.setLevel(log_level)
235     console.setFormatter(logging.Formatter(formatter))
236     logging.getLogger('').addHandler(console)
237     validate_deploy_args(args)
238     # Parse all settings
239     deploy_settings = DeploySettings(args.deploy_settings_file)
240     logging.info("Deploy settings are:\n {}".format(pprint.pformat(
241                  deploy_settings)))
242     net_settings = NetworkSettings(args.network_settings_file)
243     logging.info("Network settings are:\n {}".format(pprint.pformat(
244                  net_settings)))
245     os_version = deploy_settings['deploy_options']['os_version']
246     net_env_file = os.path.join(args.deploy_dir, constants.NET_ENV_FILE)
247     net_env = NetworkEnvironment(net_settings, net_env_file,
248                                  os_version=os_version)
249     net_env_target = os.path.join(APEX_TEMP_DIR, constants.NET_ENV_FILE)
250     utils.dump_yaml(dict(net_env), net_env_target)
251     ha_enabled = deploy_settings['global_params']['ha_enabled']
252     if args.virtual:
253         if args.virt_compute_ram is None:
254             compute_ram = args.virt_default_ram
255         else:
256             compute_ram = args.virt_compute_ram
257         if deploy_settings['deploy_options']['sdn_controller'] == \
258                 'opendaylight' and args.virt_default_ram < 12:
259             control_ram = 12
260             logging.warning('RAM per controller is too low.  OpenDaylight '
261                             'requires at least 12GB per controller.')
262             logging.info('Increasing RAM per controller to 12GB')
263         elif args.virt_default_ram < 10:
264             control_ram = 10
265             logging.warning('RAM per controller is too low.  nosdn '
266                             'requires at least 10GB per controller.')
267             logging.info('Increasing RAM per controller to 10GB')
268         else:
269             control_ram = args.virt_default_ram
270         if ha_enabled and args.virt_compute_nodes < 2:
271             logging.debug('HA enabled, bumping number of compute nodes to 2')
272             args.virt_compute_nodes = 2
273         virt_utils.generate_inventory(args.inventory_file, ha_enabled,
274                                       num_computes=args.virt_compute_nodes,
275                                       controller_ram=control_ram * 1024,
276                                       compute_ram=compute_ram * 1024,
277                                       vcpus=args.virt_cpus
278                                       )
279     inventory = Inventory(args.inventory_file, ha_enabled, args.virtual)
280
281     validate_cross_settings(deploy_settings, net_settings, inventory)
282     ds_opts = deploy_settings['deploy_options']
283     if args.quickstart:
284         deploy_settings_file = os.path.join(APEX_TEMP_DIR,
285                                             'apex_deploy_settings.yaml')
286         utils.dump_yaml(utils.dict_objects_to_str(deploy_settings),
287                         deploy_settings_file)
288         logging.info("File created: {}".format(deploy_settings_file))
289         network_settings_file = os.path.join(APEX_TEMP_DIR,
290                                              'apex_network_settings.yaml')
291         utils.dump_yaml(utils.dict_objects_to_str(net_settings),
292                         network_settings_file)
293         logging.info("File created: {}".format(network_settings_file))
294         deploy_quickstart(args, deploy_settings_file, network_settings_file,
295                           args.inventory_file)
296     else:
297         # TODO (trozet): add logic back from:
298         # Iedb75994d35b5dc1dd5d5ce1a57277c8f3729dfd (FDIO DVR)
299         ansible_args = {
300             'virsh_enabled_networks': net_settings.enabled_network_list
301         }
302         utils.run_ansible(ansible_args,
303                           os.path.join(args.lib_dir, ANSIBLE_PATH,
304                                        'deploy_dependencies.yml'))
305         uc_external = False
306         if 'external' in net_settings.enabled_network_list:
307             uc_external = True
308         if args.virtual:
309             # create all overcloud VMs
310             build_vms(inventory, net_settings, args.deploy_dir)
311         else:
312             # Attach interfaces to jumphost for baremetal deployment
313             jump_networks = ['admin']
314             if uc_external:
315                 jump_networks.append('external')
316             for network in jump_networks:
317                 if network == 'external':
318                     # TODO(trozet): enable vlan secondary external networks
319                     iface = net_settings['networks'][network][0][
320                         'installer_vm']['members'][0]
321                 else:
322                     iface = net_settings['networks'][network]['installer_vm'][
323                         'members'][0]
324                 bridge = "br-{}".format(network)
325                 jumphost.attach_interface_to_ovs(bridge, iface, network)
326         instackenv_json = os.path.join(APEX_TEMP_DIR, 'instackenv.json')
327         with open(instackenv_json, 'w') as fh:
328             json.dump(inventory, fh)
329
330         # Create and configure undercloud
331         if args.debug:
332             root_pw = constants.DEBUG_OVERCLOUD_PW
333         else:
334             root_pw = None
335
336         upstream = (os_version != constants.DEFAULT_OS_VERSION or
337                     args.upstream)
338         if os_version == 'master':
339             branch = 'master'
340         else:
341             branch = "stable/{}".format(os_version)
342         if upstream:
343             logging.info("Deploying with upstream artifacts for OpenStack "
344                          "{}".format(os_version))
345             args.image_dir = os.path.join(args.image_dir, os_version)
346             upstream_url = constants.UPSTREAM_RDO.replace(
347                 constants.DEFAULT_OS_VERSION, os_version)
348             upstream_targets = ['overcloud-full.tar', 'undercloud.qcow2']
349             utils.fetch_upstream_and_unpack(args.image_dir, upstream_url,
350                                             upstream_targets)
351             sdn_image = os.path.join(args.image_dir, 'overcloud-full.qcow2')
352             if ds_opts['sdn_controller'] == 'opendaylight':
353                 logging.info("Preparing upstream image with OpenDaylight")
354                 oc_builder.inject_opendaylight(
355                     odl_version=ds_opts['odl_version'],
356                     image=sdn_image,
357                     tmp_dir=APEX_TEMP_DIR
358                 )
359             # copy undercloud so we don't taint upstream fetch
360             uc_image = os.path.join(args.image_dir, 'undercloud_mod.qcow2')
361             uc_fetch_img = os.path.join(args.image_dir, 'undercloud.qcow2')
362             shutil.copyfile(uc_fetch_img, uc_image)
363             # prep undercloud with required packages
364             uc_builder.add_upstream_packages(uc_image)
365             # add patches from upstream to undercloud and overcloud
366             logging.info('Adding patches to undercloud')
367             patches = deploy_settings['global_params']['patches']
368             c_builder.add_upstream_patches(patches['undercloud'], uc_image,
369                                            APEX_TEMP_DIR, branch)
370             logging.info('Adding patches to overcloud')
371             c_builder.add_upstream_patches(patches['overcloud'], sdn_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         undercloud = uc_lib.Undercloud(args.image_dir,
377                                        args.deploy_dir,
378                                        root_pw=root_pw,
379                                        external_network=uc_external,
380                                        image_name=os.path.basename(uc_image))
381         undercloud.start()
382
383         # Generate nic templates
384         for role in 'compute', 'controller':
385             oc_cfg.create_nic_template(net_settings, deploy_settings, role,
386                                        args.deploy_dir, APEX_TEMP_DIR)
387         # Install Undercloud
388         undercloud.configure(net_settings,
389                              os.path.join(args.lib_dir, ANSIBLE_PATH,
390                                           'configure_undercloud.yml'),
391                              APEX_TEMP_DIR)
392
393         # Prepare overcloud-full.qcow2
394         logging.info("Preparing Overcloud for deployment...")
395         if os_version != 'ocata':
396             net_data_file = os.path.join(APEX_TEMP_DIR, 'network_data.yaml')
397             net_data = network_data.create_network_data(net_settings,
398                                                         net_data_file)
399         else:
400             net_data = False
401         if upstream and args.env_file == 'opnfv-environment.yaml':
402             # Override the env_file if it is defaulted to opnfv
403             # opnfv env file will not work with upstream
404             args.env_file = 'upstream-environment.yaml'
405         opnfv_env = os.path.join(args.deploy_dir, args.env_file)
406         if not upstream:
407             oc_deploy.prep_env(deploy_settings, net_settings, inventory,
408                                opnfv_env, net_env_target, APEX_TEMP_DIR)
409             oc_deploy.prep_image(deploy_settings, sdn_image, APEX_TEMP_DIR,
410                                  root_pw=root_pw)
411         else:
412             shutil.copyfile(sdn_image, os.path.join(APEX_TEMP_DIR,
413                                                     'overcloud-full.qcow2'))
414             shutil.copyfile(
415                 opnfv_env,
416                 os.path.join(APEX_TEMP_DIR, os.path.basename(opnfv_env))
417             )
418
419         oc_deploy.create_deploy_cmd(deploy_settings, net_settings, inventory,
420                                     APEX_TEMP_DIR, args.virtual,
421                                     os.path.basename(opnfv_env),
422                                     net_data=net_data)
423         deploy_playbook = os.path.join(args.lib_dir, ANSIBLE_PATH,
424                                        'deploy_overcloud.yml')
425         virt_env = 'virtual-environment.yaml'
426         bm_env = 'baremetal-environment.yaml'
427         for p_env in virt_env, bm_env:
428             shutil.copyfile(os.path.join(args.deploy_dir, p_env),
429                             os.path.join(APEX_TEMP_DIR, p_env))
430
431         # Start Overcloud Deployment
432         logging.info("Executing Overcloud Deployment...")
433         deploy_vars = dict()
434         deploy_vars['virtual'] = args.virtual
435         deploy_vars['debug'] = args.debug
436         deploy_vars['aarch64'] = platform.machine() == 'aarch64'
437         deploy_vars['dns_server_args'] = ''
438         deploy_vars['apex_temp_dir'] = APEX_TEMP_DIR
439         deploy_vars['apex_env_file'] = os.path.basename(opnfv_env)
440         deploy_vars['stackrc'] = 'source /home/stack/stackrc'
441         deploy_vars['overcloudrc'] = 'source /home/stack/overcloudrc'
442         deploy_vars['upstream'] = upstream
443         deploy_vars['os_version'] = os_version
444         for dns_server in net_settings['dns_servers']:
445             deploy_vars['dns_server_args'] += " --dns-nameserver {}".format(
446                 dns_server)
447         try:
448             utils.run_ansible(deploy_vars, deploy_playbook, host=undercloud.ip,
449                               user='stack', tmp_dir=APEX_TEMP_DIR)
450             logging.info("Overcloud deployment complete")
451         except Exception:
452             logging.error("Deployment Failed.  Please check log")
453             raise
454         finally:
455             os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2'))
456
457         # Post install
458         logging.info("Executing post deploy configuration")
459         jumphost.configure_bridges(net_settings)
460         nova_output = os.path.join(APEX_TEMP_DIR, 'nova_output')
461         deploy_vars['overcloud_nodes'] = parsers.parse_nova_output(
462             nova_output)
463         deploy_vars['SSH_OPTIONS'] = '-o StrictHostKeyChecking=no -o ' \
464                                      'GlobalKnownHostsFile=/dev/null -o ' \
465                                      'UserKnownHostsFile=/dev/null -o ' \
466                                      'LogLevel=error'
467         deploy_vars['external_network_cmds'] = \
468             oc_deploy.external_network_cmds(net_settings)
469         # TODO(trozet): just parse all ds_opts as deploy vars one time
470         deploy_vars['gluon'] = ds_opts['gluon']
471         deploy_vars['sdn'] = ds_opts['sdn_controller']
472         for dep_option in 'yardstick', 'dovetail', 'vsperf':
473             if dep_option in ds_opts:
474                 deploy_vars[dep_option] = ds_opts[dep_option]
475             else:
476                 deploy_vars[dep_option] = False
477         deploy_vars['dataplane'] = ds_opts['dataplane']
478         overcloudrc = os.path.join(APEX_TEMP_DIR, 'overcloudrc')
479         if ds_opts['congress']:
480             deploy_vars['congress_datasources'] = \
481                 oc_deploy.create_congress_cmds(overcloudrc)
482             deploy_vars['congress'] = True
483         else:
484             deploy_vars['congress'] = False
485         deploy_vars['calipso'] = ds_opts.get('calipso', False)
486         deploy_vars['calipso_ip'] = net_settings['networks']['admin'][
487             'installer_vm']['ip']
488         # TODO(trozet): this is probably redundant with getting external
489         # network info from undercloud.py
490         if 'external' in net_settings.enabled_network_list:
491             ext_cidr = net_settings['networks']['external'][0]['cidr']
492         else:
493             ext_cidr = net_settings['networks']['admin']['cidr']
494         deploy_vars['external_cidr'] = str(ext_cidr)
495         if ext_cidr.version == 6:
496             deploy_vars['external_network_ipv6'] = True
497         else:
498             deploy_vars['external_network_ipv6'] = False
499         post_undercloud = os.path.join(args.lib_dir, ANSIBLE_PATH,
500                                        'post_deploy_undercloud.yml')
501         logging.info("Executing post deploy configuration undercloud playbook")
502         try:
503             utils.run_ansible(deploy_vars, post_undercloud, host=undercloud.ip,
504                               user='stack', tmp_dir=APEX_TEMP_DIR)
505             logging.info("Post Deploy Undercloud Configuration Complete")
506         except Exception:
507             logging.error("Post Deploy Undercloud Configuration failed.  "
508                           "Please check log")
509             raise
510         # Post deploy overcloud node configuration
511         # TODO(trozet): just parse all ds_opts as deploy vars one time
512         deploy_vars['sfc'] = ds_opts['sfc']
513         deploy_vars['vpn'] = ds_opts['vpn']
514         # TODO(trozet): pull all logs and store in tmp dir in overcloud
515         # playbook
516         post_overcloud = os.path.join(args.lib_dir, ANSIBLE_PATH,
517                                       'post_deploy_overcloud.yml')
518         # Run per overcloud node
519         for node, ip in deploy_vars['overcloud_nodes'].items():
520             logging.info("Executing Post deploy overcloud playbook on "
521                          "node {}".format(node))
522             try:
523                 utils.run_ansible(deploy_vars, post_overcloud, host=ip,
524                                   user='heat-admin', tmp_dir=APEX_TEMP_DIR)
525                 logging.info("Post Deploy Overcloud Configuration Complete "
526                              "for node {}".format(node))
527             except Exception:
528                 logging.error("Post Deploy Overcloud Configuration failed "
529                               "for node {}. Please check log".format(node))
530                 raise
531         logging.info("Apex deployment complete")
532         logging.info("Undercloud IP: {}, please connect by doing "
533                      "'opnfv-util undercloud'".format(undercloud.ip))
534         # TODO(trozet): add logging here showing controller VIP and horizon url
535
536
537 if __name__ == '__main__':
538     main()