wrapping up deploy items for aarch
[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     validate_deploy_args(args)
238     # Parse all settings
239     deploy_settings = DeploySettings(args.deploy_settings_file)
240     logging.info("Deploy settings are:\n {}".format(pprint.pformat(
241                  deploy_settings)))
242     net_settings = NetworkSettings(args.network_settings_file)
243     logging.info("Network settings are:\n {}".format(pprint.pformat(
244                  net_settings)))
245     os_version = deploy_settings['deploy_options']['os_version']
246     net_env_file = os.path.join(args.deploy_dir, constants.NET_ENV_FILE)
247     net_env = NetworkEnvironment(net_settings, net_env_file,
248                                  os_version=os_version)
249     net_env_target = os.path.join(APEX_TEMP_DIR, constants.NET_ENV_FILE)
250     utils.dump_yaml(dict(net_env), net_env_target)
251
252     # get global deploy params
253     ha_enabled = deploy_settings['global_params']['ha_enabled']
254     introspect = deploy_settings['global_params'].get('introspect', True)
255
256     if args.virtual:
257         if args.virt_compute_ram is None:
258             compute_ram = args.virt_default_ram
259         else:
260             compute_ram = args.virt_compute_ram
261         if deploy_settings['deploy_options']['sdn_controller'] == \
262                 'opendaylight' and args.virt_default_ram < 12:
263             control_ram = 12
264             logging.warning('RAM per controller is too low.  OpenDaylight '
265                             'requires at least 12GB per controller.')
266             logging.info('Increasing RAM per controller to 12GB')
267         elif args.virt_default_ram < 10:
268             control_ram = 10
269             logging.warning('RAM per controller is too low.  nosdn '
270                             'requires at least 10GB per controller.')
271             logging.info('Increasing RAM per controller to 10GB')
272         else:
273             control_ram = args.virt_default_ram
274         if ha_enabled and args.virt_compute_nodes < 2:
275             logging.debug('HA enabled, bumping number of compute nodes to 2')
276             args.virt_compute_nodes = 2
277         virt_utils.generate_inventory(args.inventory_file, ha_enabled,
278                                       num_computes=args.virt_compute_nodes,
279                                       controller_ram=control_ram * 1024,
280                                       compute_ram=compute_ram * 1024,
281                                       vcpus=args.virt_cpus
282                                       )
283     inventory = Inventory(args.inventory_file, ha_enabled, args.virtual)
284
285     validate_cross_settings(deploy_settings, net_settings, inventory)
286     ds_opts = deploy_settings['deploy_options']
287     if args.quickstart:
288         deploy_settings_file = os.path.join(APEX_TEMP_DIR,
289                                             'apex_deploy_settings.yaml')
290         utils.dump_yaml(utils.dict_objects_to_str(deploy_settings),
291                         deploy_settings_file)
292         logging.info("File created: {}".format(deploy_settings_file))
293         network_settings_file = os.path.join(APEX_TEMP_DIR,
294                                              'apex_network_settings.yaml')
295         utils.dump_yaml(utils.dict_objects_to_str(net_settings),
296                         network_settings_file)
297         logging.info("File created: {}".format(network_settings_file))
298         deploy_quickstart(args, deploy_settings_file, network_settings_file,
299                           args.inventory_file)
300     else:
301         # TODO (trozet): add logic back from:
302         # Iedb75994d35b5dc1dd5d5ce1a57277c8f3729dfd (FDIO DVR)
303         ansible_args = {
304             'virsh_enabled_networks': net_settings.enabled_network_list
305         }
306         utils.run_ansible(ansible_args,
307                           os.path.join(args.lib_dir, ANSIBLE_PATH,
308                                        'deploy_dependencies.yml'))
309         uc_external = False
310         if 'external' in net_settings.enabled_network_list:
311             uc_external = True
312         if args.virtual:
313             # create all overcloud VMs
314             build_vms(inventory, net_settings, args.deploy_dir)
315         else:
316             # Attach interfaces to jumphost for baremetal deployment
317             jump_networks = ['admin']
318             if uc_external:
319                 jump_networks.append('external')
320             for network in jump_networks:
321                 if network == 'external':
322                     # TODO(trozet): enable vlan secondary external networks
323                     iface = net_settings['networks'][network][0][
324                         'installer_vm']['members'][0]
325                 else:
326                     iface = net_settings['networks'][network]['installer_vm'][
327                         'members'][0]
328                 bridge = "br-{}".format(network)
329                 jumphost.attach_interface_to_ovs(bridge, iface, network)
330         instackenv_json = os.path.join(APEX_TEMP_DIR, 'instackenv.json')
331         with open(instackenv_json, 'w') as fh:
332             json.dump(inventory, fh)
333
334         # Create and configure undercloud
335         if args.debug:
336             root_pw = constants.DEBUG_OVERCLOUD_PW
337         else:
338             root_pw = None
339
340         upstream = (os_version != constants.DEFAULT_OS_VERSION or
341                     args.upstream)
342         if os_version == 'master':
343             branch = 'master'
344         else:
345             branch = "stable/{}".format(os_version)
346         if upstream:
347             logging.info("Deploying with upstream artifacts for OpenStack "
348                          "{}".format(os_version))
349             args.image_dir = os.path.join(args.image_dir, os_version)
350             upstream_url = constants.UPSTREAM_RDO.replace(
351                 constants.DEFAULT_OS_VERSION, os_version)
352             upstream_targets = ['overcloud-full.tar', 'undercloud.qcow2']
353             utils.fetch_upstream_and_unpack(args.image_dir, upstream_url,
354                                             upstream_targets)
355             sdn_image = os.path.join(args.image_dir, 'overcloud-full.qcow2')
356             if ds_opts['sdn_controller'] == 'opendaylight':
357                 logging.info("Preparing upstream image with OpenDaylight")
358                 oc_builder.inject_opendaylight(
359                     odl_version=ds_opts['odl_version'],
360                     image=sdn_image,
361                     tmp_dir=APEX_TEMP_DIR
362                 )
363             # copy undercloud so we don't taint upstream fetch
364             uc_image = os.path.join(args.image_dir, 'undercloud_mod.qcow2')
365             uc_fetch_img = os.path.join(args.image_dir, 'undercloud.qcow2')
366             shutil.copyfile(uc_fetch_img, uc_image)
367             # prep undercloud with required packages
368             uc_builder.add_upstream_packages(uc_image)
369             # add patches from upstream to undercloud and overcloud
370             logging.info('Adding patches to undercloud')
371             patches = deploy_settings['global_params']['patches']
372             c_builder.add_upstream_patches(patches['undercloud'], uc_image,
373                                            APEX_TEMP_DIR, branch)
374             logging.info('Adding patches to overcloud')
375             c_builder.add_upstream_patches(patches['overcloud'], sdn_image,
376                                            APEX_TEMP_DIR, branch)
377         else:
378             sdn_image = os.path.join(args.image_dir, SDN_IMAGE)
379             uc_image = 'undercloud.qcow2'
380         undercloud = uc_lib.Undercloud(args.image_dir,
381                                        args.deploy_dir,
382                                        root_pw=root_pw,
383                                        external_network=uc_external,
384                                        image_name=os.path.basename(uc_image))
385         undercloud.start()
386
387         # Generate nic templates
388         for role in 'compute', 'controller':
389             oc_cfg.create_nic_template(net_settings, deploy_settings, role,
390                                        args.deploy_dir, APEX_TEMP_DIR)
391         # Install Undercloud
392         undercloud.configure(net_settings,
393                              os.path.join(args.lib_dir, ANSIBLE_PATH,
394                                           'configure_undercloud.yml'),
395                              APEX_TEMP_DIR)
396
397         # Prepare overcloud-full.qcow2
398         logging.info("Preparing Overcloud for deployment...")
399         if os_version != 'ocata':
400             net_data_file = os.path.join(APEX_TEMP_DIR, 'network_data.yaml')
401             net_data = network_data.create_network_data(net_settings,
402                                                         net_data_file)
403         else:
404             net_data = False
405         if upstream and args.env_file == 'opnfv-environment.yaml':
406             # Override the env_file if it is defaulted to opnfv
407             # opnfv env file will not work with upstream
408             args.env_file = 'upstream-environment.yaml'
409         opnfv_env = os.path.join(args.deploy_dir, args.env_file)
410         if not upstream:
411             oc_deploy.prep_env(deploy_settings, net_settings, inventory,
412                                opnfv_env, net_env_target, APEX_TEMP_DIR)
413             oc_deploy.prep_image(deploy_settings, sdn_image, APEX_TEMP_DIR,
414                                  root_pw=root_pw)
415         else:
416             shutil.copyfile(sdn_image, os.path.join(APEX_TEMP_DIR,
417                                                     'overcloud-full.qcow2'))
418             shutil.copyfile(
419                 opnfv_env,
420                 os.path.join(APEX_TEMP_DIR, os.path.basename(opnfv_env))
421             )
422
423         oc_deploy.create_deploy_cmd(deploy_settings, net_settings, inventory,
424                                     APEX_TEMP_DIR, args.virtual,
425                                     os.path.basename(opnfv_env),
426                                     net_data=net_data)
427         deploy_playbook = os.path.join(args.lib_dir, ANSIBLE_PATH,
428                                        'deploy_overcloud.yml')
429         virt_env = 'virtual-environment.yaml'
430         bm_env = 'baremetal-environment.yaml'
431         for p_env in virt_env, bm_env:
432             shutil.copyfile(os.path.join(args.deploy_dir, p_env),
433                             os.path.join(APEX_TEMP_DIR, p_env))
434
435         # Start Overcloud Deployment
436         logging.info("Executing Overcloud Deployment...")
437         deploy_vars = dict()
438         deploy_vars['virtual'] = args.virtual
439         deploy_vars['debug'] = args.debug
440         deploy_vars['aarch64'] = platform.machine() == 'aarch64'
441         deploy_vars['introspect'] = not (args.virtual or
442                                          deploy_vars['aarch64'] or
443                                          not introspect)
444         deploy_vars['dns_server_args'] = ''
445         deploy_vars['apex_temp_dir'] = APEX_TEMP_DIR
446         deploy_vars['apex_env_file'] = os.path.basename(opnfv_env)
447         deploy_vars['stackrc'] = 'source /home/stack/stackrc'
448         deploy_vars['overcloudrc'] = 'source /home/stack/overcloudrc'
449         deploy_vars['upstream'] = upstream
450         deploy_vars['os_version'] = os_version
451         for dns_server in net_settings['dns_servers']:
452             deploy_vars['dns_server_args'] += " --dns-nameserver {}".format(
453                 dns_server)
454         try:
455             utils.run_ansible(deploy_vars, deploy_playbook, host=undercloud.ip,
456                               user='stack', tmp_dir=APEX_TEMP_DIR)
457             logging.info("Overcloud deployment complete")
458         except Exception:
459             logging.error("Deployment Failed.  Please check log")
460             raise
461         finally:
462             os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2'))
463
464         # Post install
465         logging.info("Executing post deploy configuration")
466         jumphost.configure_bridges(net_settings)
467         nova_output = os.path.join(APEX_TEMP_DIR, 'nova_output')
468         deploy_vars['overcloud_nodes'] = parsers.parse_nova_output(
469             nova_output)
470         deploy_vars['SSH_OPTIONS'] = '-o StrictHostKeyChecking=no -o ' \
471                                      'GlobalKnownHostsFile=/dev/null -o ' \
472                                      'UserKnownHostsFile=/dev/null -o ' \
473                                      'LogLevel=error'
474         deploy_vars['external_network_cmds'] = \
475             oc_deploy.external_network_cmds(net_settings)
476         # TODO(trozet): just parse all ds_opts as deploy vars one time
477         deploy_vars['gluon'] = ds_opts['gluon']
478         deploy_vars['sdn'] = ds_opts['sdn_controller']
479         for dep_option in 'yardstick', 'dovetail', 'vsperf':
480             if dep_option in ds_opts:
481                 deploy_vars[dep_option] = ds_opts[dep_option]
482             else:
483                 deploy_vars[dep_option] = False
484         deploy_vars['dataplane'] = ds_opts['dataplane']
485         overcloudrc = os.path.join(APEX_TEMP_DIR, 'overcloudrc')
486         if ds_opts['congress']:
487             deploy_vars['congress_datasources'] = \
488                 oc_deploy.create_congress_cmds(overcloudrc)
489             deploy_vars['congress'] = True
490         else:
491             deploy_vars['congress'] = False
492         deploy_vars['calipso'] = ds_opts.get('calipso', False)
493         deploy_vars['calipso_ip'] = net_settings['networks']['admin'][
494             'installer_vm']['ip']
495         # TODO(trozet): this is probably redundant with getting external
496         # network info from undercloud.py
497         if 'external' in net_settings.enabled_network_list:
498             ext_cidr = net_settings['networks']['external'][0]['cidr']
499         else:
500             ext_cidr = net_settings['networks']['admin']['cidr']
501         deploy_vars['external_cidr'] = str(ext_cidr)
502         if ext_cidr.version == 6:
503             deploy_vars['external_network_ipv6'] = True
504         else:
505             deploy_vars['external_network_ipv6'] = False
506         post_undercloud = os.path.join(args.lib_dir, ANSIBLE_PATH,
507                                        'post_deploy_undercloud.yml')
508         logging.info("Executing post deploy configuration undercloud playbook")
509         try:
510             utils.run_ansible(deploy_vars, post_undercloud, host=undercloud.ip,
511                               user='stack', tmp_dir=APEX_TEMP_DIR)
512             logging.info("Post Deploy Undercloud Configuration Complete")
513         except Exception:
514             logging.error("Post Deploy Undercloud Configuration failed.  "
515                           "Please check log")
516             raise
517         # Post deploy overcloud node configuration
518         # TODO(trozet): just parse all ds_opts as deploy vars one time
519         deploy_vars['sfc'] = ds_opts['sfc']
520         deploy_vars['vpn'] = ds_opts['vpn']
521         # TODO(trozet): pull all logs and store in tmp dir in overcloud
522         # playbook
523         post_overcloud = os.path.join(args.lib_dir, ANSIBLE_PATH,
524                                       'post_deploy_overcloud.yml')
525         # Run per overcloud node
526         for node, ip in deploy_vars['overcloud_nodes'].items():
527             logging.info("Executing Post deploy overcloud playbook on "
528                          "node {}".format(node))
529             try:
530                 utils.run_ansible(deploy_vars, post_overcloud, host=ip,
531                                   user='heat-admin', tmp_dir=APEX_TEMP_DIR)
532                 logging.info("Post Deploy Overcloud Configuration Complete "
533                              "for node {}".format(node))
534             except Exception:
535                 logging.error("Post Deploy Overcloud Configuration failed "
536                               "for node {}. Please check log".format(node))
537                 raise
538         logging.info("Apex deployment complete")
539         logging.info("Undercloud IP: {}, please connect by doing "
540                      "'opnfv-util undercloud'".format(undercloud.ip))
541         # TODO(trozet): add logging here showing controller VIP and horizon url
542
543
544 if __name__ == '__main__':
545     main()