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