Adding python unittests for apex/virtual/*
[apex.git] / apex / deploy.py
1 #!/usr/bin/env python
2
3 ##############################################################################
4 # Copyright (c) 2017 Tim Rozet (trozet@redhat.com) and others.
5 #
6 # All rights reserved. This program and the accompanying materials
7 # are made available under the terms of the Apache License, Version 2.0
8 # which accompanies this distribution, and is available at
9 # http://www.apache.org/licenses/LICENSE-2.0
10 ##############################################################################
11
12 import argparse
13 import json
14 import logging
15 import os
16 import platform
17 import pprint
18 import shutil
19 import sys
20 import tempfile
21
22 import apex.virtual.configure_vm as vm_lib
23 import apex.virtual.utils as virt_utils
24 from apex import DeploySettings
25 from apex import Inventory
26 from apex import NetworkEnvironment
27 from apex import NetworkSettings
28 from apex.common import utils
29 from apex.common import constants
30 from apex.common import parsers
31 from apex.common.exceptions import ApexDeployException
32 from apex.network import jumphost
33 from apex.undercloud import undercloud as uc_lib
34 from apex.overcloud import config as oc_cfg
35 from apex.overcloud import overcloud_deploy
36
37 APEX_TEMP_DIR = tempfile.mkdtemp(prefix='apex_tmp')
38 ANSIBLE_PATH = 'ansible/playbooks'
39 SDN_IMAGE = 'overcloud-full-opendaylight.qcow2'
40
41
42 def deploy_quickstart(args, deploy_settings_file, network_settings_file,
43                       inventory_file=None):
44     pass
45
46
47 def validate_cross_settings(deploy_settings, net_settings, inventory):
48     """
49     Used to validate compatibility across settings file.
50     :param deploy_settings: parsed settings for deployment
51     :param net_settings: parsed settings for network
52     :param inventory: parsed inventory file
53     :return: None
54     """
55
56     if deploy_settings['deploy_options']['dataplane'] != 'ovs' and 'tenant' \
57             not in net_settings.enabled_network_list:
58         raise ApexDeployException("Setting a DPDK based dataplane requires"
59                                   "a dedicated NIC for tenant network")
60
61     # TODO(trozet): add more checks here like RAM for ODL, etc
62     # check if odl_vpp_netvirt is true and vpp is set
63     # Check if fdio and nosdn:
64     # tenant_nic_mapping_controller_members" ==
65     # "$tenant_nic_mapping_compute_members
66
67
68 def build_vms(inventory, network_settings,
69               template_dir='/usr/share/opnfv-apex'):
70     """
71     Creates VMs and configures vbmc and host
72     :param inventory:
73     :param network_settings:
74     :return:
75     """
76
77     for idx, node in enumerate(inventory['nodes']):
78         name = 'baremetal{}'.format(idx)
79         volume = name + ".qcow2"
80         volume_path = os.path.join(constants.LIBVIRT_VOLUME_PATH, volume)
81         # TODO(trozet): add error checking
82         vm_lib.create_vm(
83             name, volume_path,
84             baremetal_interfaces=network_settings.enabled_network_list,
85             memory=node['memory'], cpus=node['cpu'],
86             macs=node['mac'],
87             template_dir=template_dir)
88         virt_utils.host_setup({name: node['pm_port']})
89
90
91 def create_deploy_parser():
92     deploy_parser = argparse.ArgumentParser()
93     deploy_parser.add_argument('--debug', action='store_true', default=False,
94                                help="Turn on debug messages")
95     deploy_parser.add_argument('-l', '--log-file',
96                                default='./apex_deploy.log',
97                                dest='log_file', help="Log file to log to")
98     deploy_parser.add_argument('-d', '--deploy-settings',
99                                dest='deploy_settings_file',
100                                required=True,
101                                help='File which contains Apex deploy settings')
102     deploy_parser.add_argument('-n', '--network-settings',
103                                dest='network_settings_file',
104                                required=True,
105                                help='File which contains Apex network '
106                                     'settings')
107     deploy_parser.add_argument('-i', '--inventory-file',
108                                dest='inventory_file',
109                                default=None,
110                                help='Inventory file which contains POD '
111                                     'definition')
112     deploy_parser.add_argument('-e', '--environment-file',
113                                dest='env_file',
114                                default='opnfv-environment.yaml',
115                                help='Provide alternate base env file')
116     deploy_parser.add_argument('-v', '--virtual', action='store_true',
117                                default=False,
118                                dest='virtual',
119                                help='Enable virtual deployment')
120     deploy_parser.add_argument('--interactive', action='store_true',
121                                default=False,
122                                help='Enable interactive deployment mode which '
123                                     'requires user to confirm steps of '
124                                     'deployment')
125     deploy_parser.add_argument('--virtual-computes',
126                                dest='virt_compute_nodes',
127                                default=1,
128                                type=int,
129                                help='Number of Virtual Compute nodes to create'
130                                     ' and use during deployment (defaults to 1'
131                                     ' for noha and 2 for ha)')
132     deploy_parser.add_argument('--virtual-cpus',
133                                dest='virt_cpus',
134                                default=4,
135                                type=int,
136                                help='Number of CPUs to use per Overcloud VM in'
137                                     ' a virtual deployment (defaults to 4)')
138     deploy_parser.add_argument('--virtual-default-ram',
139                                dest='virt_default_ram',
140                                default=8,
141                                type=int,
142                                help='Amount of default RAM to use per '
143                                     'Overcloud VM in GB (defaults to 8).')
144     deploy_parser.add_argument('--virtual-compute-ram',
145                                dest='virt_compute_ram',
146                                default=None,
147                                type=int,
148                                help='Amount of RAM to use per Overcloud '
149                                     'Compute VM in GB (defaults to 8). '
150                                     'Overrides --virtual-default-ram arg for '
151                                     'computes')
152     deploy_parser.add_argument('--deploy-dir',
153                                default='/usr/share/opnfv-apex',
154                                help='Directory to deploy from which contains '
155                                     'base config files for deployment')
156     deploy_parser.add_argument('--image-dir',
157                                default='/var/opt/opnfv/images',
158                                help='Directory which contains '
159                                     'base disk images for deployment')
160     deploy_parser.add_argument('--lib-dir',
161                                default='/usr/share/opnfv-apex',
162                                help='Directory path for apex ansible '
163                                     'and third party libs')
164     deploy_parser.add_argument('--quickstart', action='store_true',
165                                default=False,
166                                help='Use tripleo-quickstart to deploy')
167     return deploy_parser
168
169
170 def validate_deploy_args(args):
171     """
172     Validates arguments for deploy
173     :param args:
174     :return: None
175     """
176
177     logging.debug('Validating arguments for deployment')
178     if args.virtual and args.inventory_file is not None:
179         logging.error("Virtual enabled but inventory file also given")
180         raise ApexDeployException('You should not specify an inventory file '
181                                   'with virtual deployments')
182     elif args.virtual:
183         args.inventory_file = os.path.join(APEX_TEMP_DIR,
184                                            'inventory-virt.yaml')
185     elif os.path.isfile(args.inventory_file) is False:
186         logging.error("Specified inventory file does not exist: {}".format(
187             args.inventory_file))
188         raise ApexDeployException('Specified inventory file does not exist')
189
190     for settings_file in (args.deploy_settings_file,
191                           args.network_settings_file):
192         if os.path.isfile(settings_file) is False:
193             logging.error("Specified settings file does not "
194                           "exist: {}".format(settings_file))
195             raise ApexDeployException('Specified settings file does not '
196                                       'exist: {}'.format(settings_file))
197
198
199 def main():
200     parser = create_deploy_parser()
201     args = parser.parse_args(sys.argv[1:])
202     # FIXME (trozet): this is only needed as a workaround for CI.  Remove
203     # when CI is changed
204     if os.getenv('IMAGES', False):
205         args.image_dir = os.getenv('IMAGES')
206     if args.debug:
207         log_level = logging.DEBUG
208     else:
209         log_level = logging.INFO
210     os.makedirs(os.path.dirname(args.log_file), exist_ok=True)
211     formatter = '%(asctime)s %(levelname)s: %(message)s'
212     logging.basicConfig(filename=args.log_file,
213                         format=formatter,
214                         datefmt='%m/%d/%Y %I:%M:%S %p',
215                         level=log_level)
216     console = logging.StreamHandler()
217     console.setLevel(log_level)
218     console.setFormatter(logging.Formatter(formatter))
219     logging.getLogger('').addHandler(console)
220     validate_deploy_args(args)
221     # Parse all settings
222     deploy_settings = DeploySettings(args.deploy_settings_file)
223     logging.info("Deploy settings are:\n {}".format(pprint.pformat(
224                  deploy_settings)))
225     net_settings = NetworkSettings(args.network_settings_file)
226     logging.info("Network settings are:\n {}".format(pprint.pformat(
227                  net_settings)))
228     net_env_file = os.path.join(args.deploy_dir, constants.NET_ENV_FILE)
229     net_env = NetworkEnvironment(net_settings, net_env_file)
230     net_env_target = os.path.join(APEX_TEMP_DIR, constants.NET_ENV_FILE)
231     utils.dump_yaml(dict(net_env), net_env_target)
232     ha_enabled = deploy_settings['global_params']['ha_enabled']
233     if args.virtual:
234         if args.virt_compute_ram is None:
235             compute_ram = args.virt_default_ram
236         else:
237             compute_ram = args.virt_compute_ram
238         if deploy_settings['deploy_options']['sdn_controller'] == \
239                 'opendaylight' and args.virt_default_ram < 12:
240             control_ram = 12
241             logging.warning('RAM per controller is too low.  OpenDaylight '
242                             'requires at least 12GB per controller.')
243             logging.info('Increasing RAM per controller to 12GB')
244         elif args.virt_default_ram < 10:
245             control_ram = 10
246             logging.warning('RAM per controller is too low.  nosdn '
247                             'requires at least 10GB per controller.')
248             logging.info('Increasing RAM per controller to 10GB')
249         else:
250             control_ram = args.virt_default_ram
251         if ha_enabled and args.virt_compute_nodes < 2:
252             logging.debug('HA enabled, bumping number of compute nodes to 2')
253             args.virt_compute_nodes = 2
254         virt_utils.generate_inventory(args.inventory_file, ha_enabled,
255                                       num_computes=args.virt_compute_nodes,
256                                       controller_ram=control_ram * 1024,
257                                       compute_ram=compute_ram * 1024,
258                                       vcpus=args.virt_cpus
259                                       )
260     inventory = Inventory(args.inventory_file, ha_enabled, args.virtual)
261
262     validate_cross_settings(deploy_settings, net_settings, inventory)
263
264     if args.quickstart:
265         deploy_settings_file = os.path.join(APEX_TEMP_DIR,
266                                             'apex_deploy_settings.yaml')
267         utils.dump_yaml(utils.dict_objects_to_str(deploy_settings),
268                         deploy_settings_file)
269         logging.info("File created: {}".format(deploy_settings_file))
270         network_settings_file = os.path.join(APEX_TEMP_DIR,
271                                              'apex_network_settings.yaml')
272         utils.dump_yaml(utils.dict_objects_to_str(net_settings),
273                         network_settings_file)
274         logging.info("File created: {}".format(network_settings_file))
275         deploy_quickstart(args, deploy_settings_file, network_settings_file,
276                           args.inventory_file)
277     else:
278         # TODO (trozet): add logic back from:
279         # Iedb75994d35b5dc1dd5d5ce1a57277c8f3729dfd (FDIO DVR)
280         ansible_args = {
281             'virsh_enabled_networks': net_settings.enabled_network_list
282         }
283         utils.run_ansible(ansible_args,
284                           os.path.join(args.lib_dir, 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, args.deploy_dir)
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                                        args.deploy_dir,
319                                        root_pw=root_pw,
320                                        external_network=uc_external)
321         undercloud.start()
322
323         # Generate nic templates
324         for role in 'compute', 'controller':
325             oc_cfg.create_nic_template(net_settings, deploy_settings, role,
326                                        args.deploy_dir, APEX_TEMP_DIR)
327         # Install Undercloud
328         undercloud.configure(net_settings,
329                              os.path.join(args.lib_dir, 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['aarch64'] = platform.machine() == 'aarch64'
358         deploy_vars['dns_server_args'] = ''
359         deploy_vars['apex_temp_dir'] = APEX_TEMP_DIR
360         deploy_vars['stackrc'] = 'source /home/stack/stackrc'
361         deploy_vars['overcloudrc'] = 'source /home/stack/overcloudrc'
362         for dns_server in net_settings['dns_servers']:
363             deploy_vars['dns_server_args'] += " --dns-nameserver {}".format(
364                 dns_server)
365         try:
366             utils.run_ansible(deploy_vars, deploy_playbook, host=undercloud.ip,
367                               user='stack', tmp_dir=APEX_TEMP_DIR)
368             logging.info("Overcloud deployment complete")
369         except Exception:
370             logging.error("Deployment Failed.  Please check log")
371             raise
372         finally:
373             os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2'))
374
375         # Post install
376         logging.info("Executing post deploy configuration")
377         jumphost.configure_bridges(net_settings)
378         nova_output = os.path.join(APEX_TEMP_DIR, 'nova_output')
379         deploy_vars['overcloud_nodes'] = parsers.parse_nova_output(
380             nova_output)
381         deploy_vars['SSH_OPTIONS'] = '-o StrictHostKeyChecking=no -o ' \
382                                      'GlobalKnownHostsFile=/dev/null -o ' \
383                                      'UserKnownHostsFile=/dev/null -o ' \
384                                      'LogLevel=error'
385         deploy_vars['external_network_cmds'] = \
386             overcloud_deploy.external_network_cmds(net_settings)
387         # TODO(trozet): just parse all ds_opts as deploy vars one time
388         ds_opts = deploy_settings['deploy_options']
389         deploy_vars['gluon'] = ds_opts['gluon']
390         deploy_vars['sdn'] = ds_opts['sdn_controller']
391         for dep_option in 'yardstick', 'dovetail', 'vsperf':
392             if dep_option in ds_opts:
393                 deploy_vars[dep_option] = ds_opts[dep_option]
394             else:
395                 deploy_vars[dep_option] = False
396         deploy_vars['dataplane'] = ds_opts['dataplane']
397         overcloudrc = os.path.join(APEX_TEMP_DIR, 'overcloudrc')
398         if ds_opts['congress']:
399             deploy_vars['congress_datasources'] = \
400                 overcloud_deploy.create_congress_cmds(overcloudrc)
401             deploy_vars['congress'] = True
402         else:
403             deploy_vars['congress'] = False
404         # TODO(trozet): this is probably redundant with getting external
405         # network info from undercloud.py
406         if 'external' in net_settings.enabled_network_list:
407             ext_cidr = net_settings['networks']['external'][0]['cidr']
408         else:
409             ext_cidr = net_settings['networks']['admin']['cidr']
410         deploy_vars['external_cidr'] = str(ext_cidr)
411         if ext_cidr.version == 6:
412             deploy_vars['external_network_ipv6'] = True
413         else:
414             deploy_vars['external_network_ipv6'] = False
415         post_undercloud = os.path.join(args.lib_dir, ANSIBLE_PATH,
416                                        'post_deploy_undercloud.yml')
417         logging.info("Executing post deploy configuration undercloud playbook")
418         try:
419             utils.run_ansible(deploy_vars, post_undercloud, host=undercloud.ip,
420                               user='stack', tmp_dir=APEX_TEMP_DIR)
421             logging.info("Post Deploy Undercloud Configuration Complete")
422         except Exception:
423             logging.error("Post Deploy Undercloud Configuration failed.  "
424                           "Please check log")
425             raise
426         # Post deploy overcloud node configuration
427         # TODO(trozet): just parse all ds_opts as deploy vars one time
428         deploy_vars['sfc'] = ds_opts['sfc']
429         deploy_vars['vpn'] = ds_opts['vpn']
430         # TODO(trozet): pull all logs and store in tmp dir in overcloud
431         # playbook
432         post_overcloud = os.path.join(args.lib_dir, ANSIBLE_PATH,
433                                       'post_deploy_overcloud.yml')
434         # Run per overcloud node
435         for node, ip in deploy_vars['overcloud_nodes'].items():
436             logging.info("Executing Post deploy overcloud playbook on "
437                          "node {}".format(node))
438             try:
439                 utils.run_ansible(deploy_vars, post_overcloud, host=ip,
440                                   user='heat-admin', tmp_dir=APEX_TEMP_DIR)
441                 logging.info("Post Deploy Overcloud Configuration Complete "
442                              "for node {}".format(node))
443             except Exception:
444                 logging.error("Post Deploy Overcloud Configuration failed "
445                               "for node {}. Please check log".format(node))
446                 raise
447         logging.info("Apex deployment complete")
448         logging.info("Undercloud IP: {}, please connect by doing "
449                      "'opnfv-util undercloud'".format(undercloud.ip))
450         # TODO(trozet): add logging here showing controller VIP and horizon url
451 if __name__ == '__main__':
452     main()