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