Fixes external network with baremetal interface
[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                 if network == 'external':
295                     # TODO(trozet): enable vlan secondary external networks
296                     iface = net_settings['networks'][network][0][
297                         'installer_vm']['members'][0]
298                 else:
299                     iface = net_settings['networks'][network]['installer_vm'][
300                         'members'][0]
301                 bridge = "br-{}".format(network)
302                 jumphost.attach_interface_to_ovs(bridge, iface, network)
303         # Dump all settings out to temp bash files to be sourced
304         instackenv_json = os.path.join(APEX_TEMP_DIR, 'instackenv.json')
305         with open(instackenv_json, 'w') as fh:
306             json.dump(inventory, fh)
307
308         # Create and configure undercloud
309         if args.debug:
310             root_pw = constants.DEBUG_OVERCLOUD_PW
311         else:
312             root_pw = None
313         undercloud = uc_lib.Undercloud(args.image_dir,
314                                        root_pw=root_pw,
315                                        external_network=uc_external)
316         undercloud.start()
317
318         # Generate nic templates
319         for role in 'compute', 'controller':
320             oc_cfg.create_nic_template(net_settings, deploy_settings, role,
321                                        args.deploy_dir, APEX_TEMP_DIR)
322         # Install Undercloud
323         undercloud.configure(net_settings,
324                              os.path.join(args.lib_dir,
325                                           ansible_path,
326                                           'configure_undercloud.yml'),
327                              APEX_TEMP_DIR)
328
329         # Prepare overcloud-full.qcow2
330         logging.info("Preparing Overcloud for deployment...")
331         sdn_image = os.path.join(args.image_dir, SDN_IMAGE)
332         overcloud_deploy.prep_image(deploy_settings, sdn_image, APEX_TEMP_DIR,
333                                     root_pw=root_pw)
334         opnfv_env = os.path.join(args.deploy_dir, args.env_file)
335         overcloud_deploy.prep_env(deploy_settings, net_settings, opnfv_env,
336                                   net_env_target, APEX_TEMP_DIR)
337         overcloud_deploy.create_deploy_cmd(deploy_settings, net_settings,
338                                            inventory, APEX_TEMP_DIR,
339                                            args.virtual, args.env_file)
340         deploy_playbook = os.path.join(args.lib_dir, ansible_path,
341                                        'deploy_overcloud.yml')
342         virt_env = 'virtual-environment.yaml'
343         bm_env = 'baremetal-environment.yaml'
344         for p_env in virt_env, bm_env:
345             shutil.copyfile(os.path.join(args.deploy_dir, p_env),
346                             os.path.join(APEX_TEMP_DIR, p_env))
347
348         # Start Overcloud Deployment
349         logging.info("Executing Overcloud Deployment...")
350         deploy_vars = dict()
351         deploy_vars['virtual'] = args.virtual
352         deploy_vars['debug'] = args.debug
353         deploy_vars['dns_server_args'] = ''
354         deploy_vars['apex_temp_dir'] = APEX_TEMP_DIR
355         deploy_vars['stackrc'] = 'source /home/stack/stackrc'
356         deploy_vars['overcloudrc'] = 'source /home/stack/overcloudrc'
357         for dns_server in net_settings['dns_servers']:
358             deploy_vars['dns_server_args'] += " --dns-nameserver {}".format(
359                 dns_server)
360         try:
361             utils.run_ansible(deploy_vars, deploy_playbook, host=undercloud.ip,
362                               user='stack', tmp_dir=APEX_TEMP_DIR)
363             logging.info("Overcloud deployment complete")
364             os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2'))
365         except Exception:
366             logging.error("Deployment Failed.  Please check log")
367             raise
368
369         # Post install
370         logging.info("Executing post deploy configuration")
371         jumphost.configure_bridges(net_settings)
372         nova_output = os.path.join(APEX_TEMP_DIR, 'nova_output')
373         deploy_vars['overcloud_nodes'] = parsers.parse_nova_output(
374             nova_output)
375         deploy_vars['SSH_OPTIONS'] = '-o StrictHostKeyChecking=no -o ' \
376                                      'GlobalKnownHostsFile=/dev/null -o ' \
377                                      'UserKnownHostsFile=/dev/null -o ' \
378                                      'LogLevel=error'
379         deploy_vars['external_network_cmds'] = \
380             overcloud_deploy.external_network_cmds(net_settings)
381         # TODO(trozet): just parse all ds_opts as deploy vars one time
382         ds_opts = deploy_settings['deploy_options']
383         deploy_vars['gluon'] = ds_opts['gluon']
384         deploy_vars['sdn'] = ds_opts['sdn_controller']
385         for dep_option in 'yardstick', 'dovetail', 'vsperf':
386             if dep_option in ds_opts:
387                 deploy_vars[dep_option] = ds_opts[dep_option]
388             else:
389                 deploy_vars[dep_option] = False
390         deploy_vars['dataplane'] = ds_opts['dataplane']
391         overcloudrc = os.path.join(APEX_TEMP_DIR, 'overcloudrc')
392         if ds_opts['congress']:
393             deploy_vars['congress_datasources'] = \
394                 overcloud_deploy.create_congress_cmds(overcloudrc)
395             deploy_vars['congress'] = True
396         else:
397             deploy_vars['congress'] = False
398         # TODO(trozet): this is probably redundant with getting external
399         # network info from undercloud.py
400         if 'external' in net_settings.enabled_network_list:
401             ext_cidr = net_settings['networks']['external'][0]['cidr']
402         else:
403             ext_cidr = net_settings['networks']['admin']['cidr']
404         deploy_vars['external_cidr'] = str(ext_cidr)
405         if ext_cidr.version == 6:
406             deploy_vars['external_network_ipv6'] = True
407         else:
408             deploy_vars['external_network_ipv6'] = False
409         post_undercloud = os.path.join(args.lib_dir, ansible_path,
410                                        'post_deploy_undercloud.yml')
411         logging.info("Executing post deploy configuration undercloud playbook")
412         try:
413             utils.run_ansible(deploy_vars, post_undercloud, host=undercloud.ip,
414                               user='stack', tmp_dir=APEX_TEMP_DIR)
415             logging.info("Post Deploy Undercloud Configuration Complete")
416         except Exception:
417             logging.error("Post Deploy Undercloud Configuration failed.  "
418                           "Please check log")
419             raise
420         # Post deploy overcloud node configuration
421         # TODO(trozet): just parse all ds_opts as deploy vars one time
422         deploy_vars['sfc'] = ds_opts['sfc']
423         deploy_vars['vpn'] = ds_opts['vpn']
424         # TODO(trozet): pull all logs and store in tmp dir in overcloud
425         # playbook
426         post_overcloud = os.path.join(args.lib_dir, ansible_path,
427                                       'post_deploy_overcloud.yml')
428         # Run per overcloud node
429         for node, ip in deploy_vars['overcloud_nodes'].items():
430             logging.info("Executing Post deploy overcloud playbook on "
431                          "node {}".format(node))
432             try:
433                 utils.run_ansible(deploy_vars, post_overcloud, host=ip,
434                                   user='heat-admin', tmp_dir=APEX_TEMP_DIR)
435                 logging.info("Post Deploy Overcloud Configuration Complete "
436                              "for node {}".format(node))
437             except Exception:
438                 logging.error("Post Deploy Overcloud Configuration failed "
439                               "for node {}. Please check log".format(node))
440                 raise
441         logging.info("Apex deployment complete")
442         logging.info("Undercloud IP: {}, please connect by doing "
443                      "'opnfv-util undercloud'".format(undercloud.ip))
444         # TODO(trozet): add logging here showing controller VIP and horizon url
445 if __name__ == '__main__':
446     main()