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