Fixes removing inventory keys
[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.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.common import utils
29 from apex.common import constants
30 from apex.common import parsers
31 from apex.common.exceptions import ApexDeployException
32 from apex.network import jumphost
33 from apex.undercloud import undercloud as uc_lib
34 from apex.overcloud import config as oc_cfg
35 from apex.overcloud import overcloud_deploy
36
37 APEX_TEMP_DIR = tempfile.mkdtemp(prefix='apex_tmp')
38 ANSIBLE_PATH = 'ansible/playbooks'
39 SDN_IMAGE = 'overcloud-full-opendaylight.qcow2'
40
41
42 def deploy_quickstart(args, deploy_settings_file, network_settings_file,
43                       inventory_file=None):
44     pass
45
46
47 def validate_cross_settings(deploy_settings, net_settings, inventory):
48     """
49     Used to validate compatibility across settings file.
50     :param deploy_settings: parsed settings for deployment
51     :param net_settings: parsed settings for network
52     :param inventory: parsed inventory file
53     :return: None
54     """
55
56     if deploy_settings['deploy_options']['dataplane'] != 'ovs' and 'tenant' \
57             not in net_settings.enabled_network_list:
58         raise ApexDeployException("Setting a DPDK based dataplane requires"
59                                   "a dedicated NIC for tenant network")
60
61     # TODO(trozet): add more checks here like RAM for ODL, etc
62     # check if odl_vpp_netvirt is true and vpp is set
63     # Check if fdio and nosdn:
64     # tenant_nic_mapping_controller_members" ==
65     # "$tenant_nic_mapping_compute_members
66
67
68 def build_vms(inventory, network_settings,
69               template_dir='/usr/share/opnfv-apex'):
70     """
71     Creates VMs and configures vbmc and host
72     :param inventory:
73     :param network_settings:
74     :return:
75     """
76
77     for idx, node in enumerate(inventory['nodes']):
78         name = 'baremetal{}'.format(idx)
79         volume = name + ".qcow2"
80         volume_path = os.path.join(constants.LIBVIRT_VOLUME_PATH, volume)
81         # TODO(trozet): add error checking
82         vm_lib.create_vm(
83             name, volume_path,
84             baremetal_interfaces=network_settings.enabled_network_list,
85             memory=node['memory'], cpus=node['cpu'],
86             macs=node['mac'],
87             template_dir=template_dir)
88         virt_utils.host_setup({name: node['pm_port']})
89
90
91 def create_deploy_parser():
92     deploy_parser = argparse.ArgumentParser()
93     deploy_parser.add_argument('--debug', action='store_true', default=False,
94                                help="Turn on debug messages")
95     deploy_parser.add_argument('-l', '--log-file',
96                                default='./apex_deploy.log',
97                                dest='log_file', help="Log file to log to")
98     deploy_parser.add_argument('-d', '--deploy-settings',
99                                dest='deploy_settings_file',
100                                required=True,
101                                help='File which contains Apex deploy settings')
102     deploy_parser.add_argument('-n', '--network-settings',
103                                dest='network_settings_file',
104                                required=True,
105                                help='File which contains Apex network '
106                                     'settings')
107     deploy_parser.add_argument('-i', '--inventory-file',
108                                dest='inventory_file',
109                                default=None,
110                                help='Inventory file which contains POD '
111                                     'definition')
112     deploy_parser.add_argument('-e', '--environment-file',
113                                dest='env_file',
114                                default='opnfv-environment.yaml',
115                                help='Provide alternate base env file')
116     deploy_parser.add_argument('-v', '--virtual', action='store_true',
117                                default=False,
118                                dest='virtual',
119                                help='Enable virtual deployment')
120     deploy_parser.add_argument('--interactive', action='store_true',
121                                default=False,
122                                help='Enable interactive deployment mode which '
123                                     'requires user to confirm steps of '
124                                     'deployment')
125     deploy_parser.add_argument('--virtual-computes',
126                                dest='virt_compute_nodes',
127                                default=1,
128                                type=int,
129                                help='Number of Virtual Compute nodes to create'
130                                     ' and use during deployment (defaults to 1'
131                                     ' for noha and 2 for ha)')
132     deploy_parser.add_argument('--virtual-cpus',
133                                dest='virt_cpus',
134                                default=4,
135                                type=int,
136                                help='Number of CPUs to use per Overcloud VM in'
137                                     ' a virtual deployment (defaults to 4)')
138     deploy_parser.add_argument('--virtual-default-ram',
139                                dest='virt_default_ram',
140                                default=8,
141                                type=int,
142                                help='Amount of default RAM to use per '
143                                     'Overcloud VM in GB (defaults to 8).')
144     deploy_parser.add_argument('--virtual-compute-ram',
145                                dest='virt_compute_ram',
146                                default=None,
147                                type=int,
148                                help='Amount of RAM to use per Overcloud '
149                                     'Compute VM in GB (defaults to 8). '
150                                     'Overrides --virtual-default-ram arg for '
151                                     'computes')
152     deploy_parser.add_argument('--deploy-dir',
153                                default='/usr/share/opnfv-apex',
154                                help='Directory to deploy from which contains '
155                                     'base config files for deployment')
156     deploy_parser.add_argument('--image-dir',
157                                default='/var/opt/opnfv/images',
158                                help='Directory which contains '
159                                     'base disk images for deployment')
160     deploy_parser.add_argument('--lib-dir',
161                                default='/usr/share/opnfv-apex',
162                                help='Directory path for apex ansible '
163                                     'and third party libs')
164     deploy_parser.add_argument('--quickstart', action='store_true',
165                                default=False,
166                                help='Use tripleo-quickstart to deploy')
167     return deploy_parser
168
169
170 def validate_deploy_args(args):
171     """
172     Validates arguments for deploy
173     :param args:
174     :return: None
175     """
176
177     logging.debug('Validating arguments for deployment')
178     if args.virtual and args.inventory_file is not None:
179         logging.error("Virtual enabled but inventory file also given")
180         raise ApexDeployException('You should not specify an inventory file '
181                                   'with virtual deployments')
182     elif args.virtual:
183         args.inventory_file = os.path.join(APEX_TEMP_DIR,
184                                            'inventory-virt.yaml')
185     elif os.path.isfile(args.inventory_file) is False:
186         logging.error("Specified inventory file does not exist: {}".format(
187             args.inventory_file))
188         raise ApexDeployException('Specified inventory file does not exist')
189
190     for settings_file in (args.deploy_settings_file,
191                           args.network_settings_file):
192         if os.path.isfile(settings_file) is False:
193             logging.error("Specified settings file does not "
194                           "exist: {}".format(settings_file))
195             raise ApexDeployException('Specified settings file does not '
196                                       'exist: {}'.format(settings_file))
197
198
199 def main():
200     parser = create_deploy_parser()
201     args = parser.parse_args(sys.argv[1:])
202     # FIXME (trozet): this is only needed as a workaround for CI.  Remove
203     # when CI is changed
204     if os.getenv('IMAGES', False):
205         args.image_dir = os.getenv('IMAGES')
206     if args.debug:
207         log_level = logging.DEBUG
208     else:
209         log_level = logging.INFO
210     os.makedirs(os.path.dirname(args.log_file), exist_ok=True)
211     formatter = '%(asctime)s %(levelname)s: %(message)s'
212     logging.basicConfig(filename=args.log_file,
213                         format=formatter,
214                         datefmt='%m/%d/%Y %I:%M:%S %p',
215                         level=log_level)
216     console = logging.StreamHandler()
217     console.setLevel(log_level)
218     console.setFormatter(logging.Formatter(formatter))
219     logging.getLogger('').addHandler(console)
220     validate_deploy_args(args)
221     # Parse all settings
222     deploy_settings = DeploySettings(args.deploy_settings_file)
223     logging.info("Deploy settings are:\n {}".format(pprint.pformat(
224                  deploy_settings)))
225     net_settings = NetworkSettings(args.network_settings_file)
226     logging.info("Network settings are:\n {}".format(pprint.pformat(
227                  net_settings)))
228     net_env_file = os.path.join(args.deploy_dir, constants.NET_ENV_FILE)
229     net_env = NetworkEnvironment(net_settings, net_env_file)
230     net_env_target = os.path.join(APEX_TEMP_DIR, constants.NET_ENV_FILE)
231     utils.dump_yaml(dict(net_env), net_env_target)
232     ha_enabled = deploy_settings['global_params']['ha_enabled']
233     if args.virtual:
234         if args.virt_compute_ram is None:
235             compute_ram = args.virt_default_ram
236         else:
237             compute_ram = args.virt_compute_ram
238         if deploy_settings['deploy_options']['sdn_controller'] == \
239                 'opendaylight' and args.virt_default_ram < 12:
240             control_ram = 12
241             logging.warning('RAM per controller is too low.  OpenDaylight '
242                             'requires at least 12GB per controller.')
243             logging.info('Increasing RAM per controller to 12GB')
244         elif args.virt_default_ram < 10:
245             control_ram = 10
246             logging.warning('RAM per controller is too low.  nosdn '
247                             'requires at least 10GB per controller.')
248             logging.info('Increasing RAM per controller to 10GB')
249         else:
250             control_ram = args.virt_default_ram
251         if ha_enabled and args.virt_compute_nodes < 2:
252             logging.debug('HA enabled, bumping number of compute nodes to 2')
253             args.virt_compute_nodes = 2
254         virt_utils.generate_inventory(args.inventory_file, ha_enabled,
255                                       num_computes=args.virt_compute_nodes,
256                                       controller_ram=control_ram * 1024,
257                                       compute_ram=compute_ram * 1024,
258                                       vcpus=args.virt_cpus
259                                       )
260     inventory = Inventory(args.inventory_file, ha_enabled, args.virtual)
261
262     validate_cross_settings(deploy_settings, net_settings, inventory)
263
264     if args.quickstart:
265         deploy_settings_file = os.path.join(APEX_TEMP_DIR,
266                                             'apex_deploy_settings.yaml')
267         utils.dump_yaml(utils.dict_objects_to_str(deploy_settings),
268                         deploy_settings_file)
269         logging.info("File created: {}".format(deploy_settings_file))
270         network_settings_file = os.path.join(APEX_TEMP_DIR,
271                                              'apex_network_settings.yaml')
272         utils.dump_yaml(utils.dict_objects_to_str(net_settings),
273                         network_settings_file)
274         logging.info("File created: {}".format(network_settings_file))
275         deploy_quickstart(args, deploy_settings_file, network_settings_file,
276                           args.inventory_file)
277     else:
278         # TODO (trozet): add logic back from:
279         # Iedb75994d35b5dc1dd5d5ce1a57277c8f3729dfd (FDIO DVR)
280         ansible_args = {
281             'virsh_enabled_networks': net_settings.enabled_network_list
282         }
283         ansible_path = os.path.join(args.lib_dir, ANSIBLE_PATH)
284         utils.run_ansible(ansible_args,
285                           os.path.join(args.lib_dir,
286                                        ansible_path,
287                                        'deploy_dependencies.yml'))
288         uc_external = False
289         if 'external' in net_settings.enabled_network_list:
290             uc_external = True
291         if args.virtual:
292             # create all overcloud VMs
293             build_vms(inventory, net_settings, args.deploy_dir)
294         else:
295             # Attach interfaces to jumphost for baremetal deployment
296             jump_networks = ['admin']
297             if uc_external:
298                 jump_networks.append('external')
299             for network in jump_networks:
300                 if network == 'external':
301                     # TODO(trozet): enable vlan secondary external networks
302                     iface = net_settings['networks'][network][0][
303                         'installer_vm']['members'][0]
304                 else:
305                     iface = net_settings['networks'][network]['installer_vm'][
306                         'members'][0]
307                 bridge = "br-{}".format(network)
308                 jumphost.attach_interface_to_ovs(bridge, iface, network)
309         # Dump all settings out to temp bash files to be sourced
310         instackenv_json = os.path.join(APEX_TEMP_DIR, 'instackenv.json')
311         with open(instackenv_json, 'w') as fh:
312             json.dump(inventory, fh)
313
314         # Create and configure undercloud
315         if args.debug:
316             root_pw = constants.DEBUG_OVERCLOUD_PW
317         else:
318             root_pw = None
319         undercloud = uc_lib.Undercloud(args.image_dir,
320                                        args.deploy_dir,
321                                        root_pw=root_pw,
322                                        external_network=uc_external)
323         undercloud.start()
324
325         # Generate nic templates
326         for role in 'compute', 'controller':
327             oc_cfg.create_nic_template(net_settings, deploy_settings, role,
328                                        args.deploy_dir, APEX_TEMP_DIR)
329         # Install Undercloud
330         undercloud.configure(net_settings,
331                              os.path.join(args.lib_dir,
332                                           ansible_path,
333                                           'configure_undercloud.yml'),
334                              APEX_TEMP_DIR)
335
336         # Prepare overcloud-full.qcow2
337         logging.info("Preparing Overcloud for deployment...")
338         sdn_image = os.path.join(args.image_dir, SDN_IMAGE)
339         overcloud_deploy.prep_image(deploy_settings, sdn_image, APEX_TEMP_DIR,
340                                     root_pw=root_pw)
341         opnfv_env = os.path.join(args.deploy_dir, args.env_file)
342         overcloud_deploy.prep_env(deploy_settings, net_settings, opnfv_env,
343                                   net_env_target, APEX_TEMP_DIR)
344         overcloud_deploy.create_deploy_cmd(deploy_settings, net_settings,
345                                            inventory, APEX_TEMP_DIR,
346                                            args.virtual, args.env_file)
347         deploy_playbook = os.path.join(args.lib_dir, ansible_path,
348                                        'deploy_overcloud.yml')
349         virt_env = 'virtual-environment.yaml'
350         bm_env = 'baremetal-environment.yaml'
351         for p_env in virt_env, bm_env:
352             shutil.copyfile(os.path.join(args.deploy_dir, p_env),
353                             os.path.join(APEX_TEMP_DIR, p_env))
354
355         # Start Overcloud Deployment
356         logging.info("Executing Overcloud Deployment...")
357         deploy_vars = dict()
358         deploy_vars['virtual'] = args.virtual
359         deploy_vars['debug'] = args.debug
360         deploy_vars['aarch64'] = platform.machine() == 'aarch64'
361         deploy_vars['dns_server_args'] = ''
362         deploy_vars['apex_temp_dir'] = APEX_TEMP_DIR
363         deploy_vars['stackrc'] = 'source /home/stack/stackrc'
364         deploy_vars['overcloudrc'] = 'source /home/stack/overcloudrc'
365         for dns_server in net_settings['dns_servers']:
366             deploy_vars['dns_server_args'] += " --dns-nameserver {}".format(
367                 dns_server)
368         try:
369             utils.run_ansible(deploy_vars, deploy_playbook, host=undercloud.ip,
370                               user='stack', tmp_dir=APEX_TEMP_DIR)
371             logging.info("Overcloud deployment complete")
372         except Exception:
373             logging.error("Deployment Failed.  Please check log")
374             raise
375         finally:
376             os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2'))
377
378         # Post install
379         logging.info("Executing post deploy configuration")
380         jumphost.configure_bridges(net_settings)
381         nova_output = os.path.join(APEX_TEMP_DIR, 'nova_output')
382         deploy_vars['overcloud_nodes'] = parsers.parse_nova_output(
383             nova_output)
384         deploy_vars['SSH_OPTIONS'] = '-o StrictHostKeyChecking=no -o ' \
385                                      'GlobalKnownHostsFile=/dev/null -o ' \
386                                      'UserKnownHostsFile=/dev/null -o ' \
387                                      'LogLevel=error'
388         deploy_vars['external_network_cmds'] = \
389             overcloud_deploy.external_network_cmds(net_settings)
390         # TODO(trozet): just parse all ds_opts as deploy vars one time
391         ds_opts = deploy_settings['deploy_options']
392         deploy_vars['gluon'] = ds_opts['gluon']
393         deploy_vars['sdn'] = ds_opts['sdn_controller']
394         for dep_option in 'yardstick', 'dovetail', 'vsperf':
395             if dep_option in ds_opts:
396                 deploy_vars[dep_option] = ds_opts[dep_option]
397             else:
398                 deploy_vars[dep_option] = False
399         deploy_vars['dataplane'] = ds_opts['dataplane']
400         overcloudrc = os.path.join(APEX_TEMP_DIR, 'overcloudrc')
401         if ds_opts['congress']:
402             deploy_vars['congress_datasources'] = \
403                 overcloud_deploy.create_congress_cmds(overcloudrc)
404             deploy_vars['congress'] = True
405         else:
406             deploy_vars['congress'] = False
407         deploy_vars['calipso'] = ds_opts.get('calipso', False)
408         # TODO(trozet): this is probably redundant with getting external
409         # network info from undercloud.py
410         if 'external' in net_settings.enabled_network_list:
411             ext_cidr = net_settings['networks']['external'][0]['cidr']
412         else:
413             ext_cidr = net_settings['networks']['admin']['cidr']
414         deploy_vars['external_cidr'] = str(ext_cidr)
415         if ext_cidr.version == 6:
416             deploy_vars['external_network_ipv6'] = True
417         else:
418             deploy_vars['external_network_ipv6'] = False
419         post_undercloud = os.path.join(args.lib_dir, ansible_path,
420                                        'post_deploy_undercloud.yml')
421         logging.info("Executing post deploy configuration undercloud playbook")
422         try:
423             utils.run_ansible(deploy_vars, post_undercloud, host=undercloud.ip,
424                               user='stack', tmp_dir=APEX_TEMP_DIR)
425             logging.info("Post Deploy Undercloud Configuration Complete")
426         except Exception:
427             logging.error("Post Deploy Undercloud Configuration failed.  "
428                           "Please check log")
429             raise
430         # Post deploy overcloud node configuration
431         # TODO(trozet): just parse all ds_opts as deploy vars one time
432         deploy_vars['sfc'] = ds_opts['sfc']
433         deploy_vars['vpn'] = ds_opts['vpn']
434         # TODO(trozet): pull all logs and store in tmp dir in overcloud
435         # playbook
436         post_overcloud = os.path.join(args.lib_dir, ansible_path,
437                                       'post_deploy_overcloud.yml')
438         # Run per overcloud node
439         for node, ip in deploy_vars['overcloud_nodes'].items():
440             logging.info("Executing Post deploy overcloud playbook on "
441                          "node {}".format(node))
442             try:
443                 utils.run_ansible(deploy_vars, post_overcloud, host=ip,
444                                   user='heat-admin', tmp_dir=APEX_TEMP_DIR)
445                 logging.info("Post Deploy Overcloud Configuration Complete "
446                              "for node {}".format(node))
447             except Exception:
448                 logging.error("Post Deploy Overcloud Configuration failed "
449                               "for node {}. Please check log".format(node))
450                 raise
451         logging.info("Apex deployment complete")
452         logging.info("Undercloud IP: {}, please connect by doing "
453                      "'opnfv-util undercloud'".format(undercloud.ip))
454         # TODO(trozet): add logging here showing controller VIP and horizon url
455 if __name__ == '__main__':
456     main()