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