Merge "Updates preliminary docs for Euphrates"
[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 pprint
17 import shutil
18 import sys
19 import tempfile
20
21 import apex.virtual.configure_vm as vm_lib
22 import apex.virtual.virtual_utils as virt_utils
23 from apex import DeploySettings
24 from apex import Inventory
25 from apex import NetworkEnvironment
26 from apex import NetworkSettings
27 from apex.common import utils
28 from apex.common import constants
29 from apex.common import parsers
30 from apex.common.exceptions import ApexDeployException
31 from apex.network import jumphost
32 from apex.undercloud import undercloud as uc_lib
33 from apex.overcloud import config as oc_cfg
34 from apex.overcloud import overcloud_deploy
35
36 APEX_TEMP_DIR = tempfile.mkdtemp()
37 ANSIBLE_PATH = 'ansible/playbooks'
38 SDN_IMAGE = 'overcloud-full-opendaylight.qcow2'
39
40
41 def deploy_quickstart(args, deploy_settings_file, network_settings_file,
42                       inventory_file=None):
43     pass
44
45
46 def validate_cross_settings(deploy_settings, net_settings, inventory):
47     """
48     Used to validate compatibility across settings file.
49     :param deploy_settings: parsed settings for deployment
50     :param net_settings: parsed settings for network
51     :param inventory: parsed inventory file
52     :return: None
53     """
54
55     if deploy_settings['deploy_options']['dataplane'] != 'ovs' and 'tenant' \
56             not in net_settings.enabled_network_list:
57         raise ApexDeployException("Setting a DPDK based dataplane requires"
58                                   "a dedicated NIC for tenant network")
59
60     # TODO(trozet): add more checks here like RAM for ODL, etc
61     # check if odl_vpp_netvirt is true and vpp is set
62     # Check if fdio and nosdn:
63     # tenant_nic_mapping_controller_members" ==
64     # "$tenant_nic_mapping_compute_members
65
66
67 def build_vms(inventory, network_settings,
68               template_dir='/usr/share/opnfv-apex'):
69     """
70     Creates VMs and configures vbmc and host
71     :param inventory:
72     :param network_settings:
73     :return:
74     """
75
76     for idx, node in enumerate(inventory['nodes']):
77         name = 'baremetal{}'.format(idx)
78         volume = name + ".qcow2"
79         volume_path = os.path.join(constants.LIBVIRT_VOLUME_PATH, volume)
80         # TODO(trozet): add back aarch64
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_address']],
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['dns_server_args'] = ''
361         deploy_vars['apex_temp_dir'] = APEX_TEMP_DIR
362         deploy_vars['stackrc'] = 'source /home/stack/stackrc'
363         deploy_vars['overcloudrc'] = 'source /home/stack/overcloudrc'
364         for dns_server in net_settings['dns_servers']:
365             deploy_vars['dns_server_args'] += " --dns-nameserver {}".format(
366                 dns_server)
367         try:
368             utils.run_ansible(deploy_vars, deploy_playbook, host=undercloud.ip,
369                               user='stack', tmp_dir=APEX_TEMP_DIR)
370             logging.info("Overcloud deployment complete")
371             os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2'))
372         except Exception:
373             logging.error("Deployment Failed.  Please check log")
374             raise
375
376         # Post install
377         logging.info("Executing post deploy configuration")
378         jumphost.configure_bridges(net_settings)
379         nova_output = os.path.join(APEX_TEMP_DIR, 'nova_output')
380         deploy_vars['overcloud_nodes'] = parsers.parse_nova_output(
381             nova_output)
382         deploy_vars['SSH_OPTIONS'] = '-o StrictHostKeyChecking=no -o ' \
383                                      'GlobalKnownHostsFile=/dev/null -o ' \
384                                      'UserKnownHostsFile=/dev/null -o ' \
385                                      'LogLevel=error'
386         deploy_vars['external_network_cmds'] = \
387             overcloud_deploy.external_network_cmds(net_settings)
388         # TODO(trozet): just parse all ds_opts as deploy vars one time
389         ds_opts = deploy_settings['deploy_options']
390         deploy_vars['gluon'] = ds_opts['gluon']
391         deploy_vars['sdn'] = ds_opts['sdn_controller']
392         for dep_option in 'yardstick', 'dovetail', 'vsperf':
393             if dep_option in ds_opts:
394                 deploy_vars[dep_option] = ds_opts[dep_option]
395             else:
396                 deploy_vars[dep_option] = False
397         deploy_vars['dataplane'] = ds_opts['dataplane']
398         overcloudrc = os.path.join(APEX_TEMP_DIR, 'overcloudrc')
399         if ds_opts['congress']:
400             deploy_vars['congress_datasources'] = \
401                 overcloud_deploy.create_congress_cmds(overcloudrc)
402             deploy_vars['congress'] = True
403         else:
404             deploy_vars['congress'] = False
405         # TODO(trozet): this is probably redundant with getting external
406         # network info from undercloud.py
407         if 'external' in net_settings.enabled_network_list:
408             ext_cidr = net_settings['networks']['external'][0]['cidr']
409         else:
410             ext_cidr = net_settings['networks']['admin']['cidr']
411         deploy_vars['external_cidr'] = str(ext_cidr)
412         if ext_cidr.version == 6:
413             deploy_vars['external_network_ipv6'] = True
414         else:
415             deploy_vars['external_network_ipv6'] = False
416         post_undercloud = os.path.join(args.lib_dir, ansible_path,
417                                        'post_deploy_undercloud.yml')
418         logging.info("Executing post deploy configuration undercloud playbook")
419         try:
420             utils.run_ansible(deploy_vars, post_undercloud, host=undercloud.ip,
421                               user='stack', tmp_dir=APEX_TEMP_DIR)
422             logging.info("Post Deploy Undercloud Configuration Complete")
423         except Exception:
424             logging.error("Post Deploy Undercloud Configuration failed.  "
425                           "Please check log")
426             raise
427         # Post deploy overcloud node configuration
428         # TODO(trozet): just parse all ds_opts as deploy vars one time
429         deploy_vars['sfc'] = ds_opts['sfc']
430         deploy_vars['vpn'] = ds_opts['vpn']
431         # TODO(trozet): pull all logs and store in tmp dir in overcloud
432         # playbook
433         post_overcloud = os.path.join(args.lib_dir, ansible_path,
434                                       'post_deploy_overcloud.yml')
435         # Run per overcloud node
436         for node, ip in deploy_vars['overcloud_nodes'].items():
437             logging.info("Executing Post deploy overcloud playbook on "
438                          "node {}".format(node))
439             try:
440                 utils.run_ansible(deploy_vars, post_overcloud, host=ip,
441                                   user='heat-admin', tmp_dir=APEX_TEMP_DIR)
442                 logging.info("Post Deploy Overcloud Configuration Complete "
443                              "for node {}".format(node))
444             except Exception:
445                 logging.error("Post Deploy Overcloud Configuration failed "
446                               "for node {}. Please check log".format(node))
447                 raise
448         logging.info("Apex deployment complete")
449         logging.info("Undercloud IP: {}, please connect by doing "
450                      "'opnfv-util undercloud'".format(undercloud.ip))
451         # TODO(trozet): add logging here showing controller VIP and horizon url
452 if __name__ == '__main__':
453     main()