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