3 ##############################################################################
4 # Copyright (c) 2017 Tim Rozet (trozet@redhat.com) and others.
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 ##############################################################################
22 import apex.virtual.configure_vm as vm_lib
23 import apex.virtual.utils as virt_utils
24 import apex.builders.common_builder as c_builder
25 import apex.builders.overcloud_builder as oc_builder
26 import apex.builders.undercloud_builder as uc_builder
27 from apex import DeploySettings
28 from apex import Inventory
29 from apex import NetworkEnvironment
30 from apex import NetworkSettings
31 from apex.common import utils
32 from apex.common import constants
33 from apex.common import parsers
34 from apex.common.exceptions import ApexDeployException
35 from apex.network import jumphost
36 from apex.network import network_data
37 from apex.undercloud import undercloud as uc_lib
38 from apex.overcloud import config as oc_cfg
39 from apex.overcloud import deploy as oc_deploy
41 APEX_TEMP_DIR = tempfile.mkdtemp(prefix='apex_tmp')
42 SDN_IMAGE = 'overcloud-full-opendaylight.qcow2'
45 def deploy_quickstart(args, deploy_settings_file, network_settings_file,
50 def validate_cross_settings(deploy_settings, net_settings, inventory):
52 Used to validate compatibility across settings file.
53 :param deploy_settings: parsed settings for deployment
54 :param net_settings: parsed settings for network
55 :param inventory: parsed inventory file
59 if deploy_settings['deploy_options']['dataplane'] != 'ovs' and 'tenant' \
60 not in net_settings.enabled_network_list:
61 raise ApexDeployException("Setting a DPDK based dataplane requires"
62 "a dedicated NIC for tenant network")
64 if 'odl_vpp_routing_node' in deploy_settings['deploy_options']:
65 if deploy_settings['deploy_options']['dataplane'] != 'fdio':
66 raise ApexDeployException("odl_vpp_routing_node should only be set"
67 "when dataplane is set to fdio")
68 if deploy_settings['deploy_options'].get('dvr') is True:
69 raise ApexDeployException("odl_vpp_routing_node should only be set"
70 "when dvr is not enabled")
72 # TODO(trozet): add more checks here like RAM for ODL, etc
73 # check if odl_vpp_netvirt is true and vpp is set
74 # Check if fdio and nosdn:
75 # tenant_nic_mapping_controller_members" ==
76 # "$tenant_nic_mapping_compute_members
79 def build_vms(inventory, network_settings,
80 template_dir='/usr/share/opnfv-apex'):
82 Creates VMs and configures vbmc and host
84 :param network_settings:
88 for idx, node in enumerate(inventory['nodes']):
89 name = 'baremetal{}'.format(idx)
90 volume = name + ".qcow2"
91 volume_path = os.path.join(constants.LIBVIRT_VOLUME_PATH, volume)
92 # TODO(trozet): add error checking
95 baremetal_interfaces=network_settings.enabled_network_list,
96 memory=node['memory'], cpus=node['cpu'],
98 template_dir=template_dir)
99 virt_utils.host_setup({name: node['pm_port']})
102 def create_deploy_parser():
103 deploy_parser = argparse.ArgumentParser()
104 deploy_parser.add_argument('--debug', action='store_true', default=False,
105 help="Turn on debug messages")
106 deploy_parser.add_argument('-l', '--log-file',
107 default='./apex_deploy.log',
108 dest='log_file', help="Log file to log to")
109 deploy_parser.add_argument('-d', '--deploy-settings',
110 dest='deploy_settings_file',
112 help='File which contains Apex deploy settings')
113 deploy_parser.add_argument('-n', '--network-settings',
114 dest='network_settings_file',
116 help='File which contains Apex network '
118 deploy_parser.add_argument('-i', '--inventory-file',
119 dest='inventory_file',
121 help='Inventory file which contains POD '
123 deploy_parser.add_argument('-e', '--environment-file',
125 default='opnfv-environment.yaml',
126 help='Provide alternate base env file located '
128 deploy_parser.add_argument('-v', '--virtual', action='store_true',
131 help='Enable virtual deployment')
132 deploy_parser.add_argument('--interactive', action='store_true',
134 help='Enable interactive deployment mode which '
135 'requires user to confirm steps of '
137 deploy_parser.add_argument('--virtual-computes',
138 dest='virt_compute_nodes',
141 help='Number of Virtual Compute nodes to create'
142 ' and use during deployment (defaults to 1'
143 ' for noha and 2 for ha)')
144 deploy_parser.add_argument('--virtual-cpus',
148 help='Number of CPUs to use per Overcloud VM in'
149 ' a virtual deployment (defaults to 4)')
150 deploy_parser.add_argument('--virtual-default-ram',
151 dest='virt_default_ram',
154 help='Amount of default RAM to use per '
155 'Overcloud VM in GB (defaults to 8).')
156 deploy_parser.add_argument('--virtual-compute-ram',
157 dest='virt_compute_ram',
160 help='Amount of RAM to use per Overcloud '
161 'Compute VM in GB (defaults to 8). '
162 'Overrides --virtual-default-ram arg for '
164 deploy_parser.add_argument('--deploy-dir',
165 default='/usr/share/opnfv-apex',
166 help='Directory to deploy from which contains '
167 'base config files for deployment')
168 deploy_parser.add_argument('--image-dir',
169 default='/var/opt/opnfv/images',
170 help='Directory which contains '
171 'base disk images for deployment')
172 deploy_parser.add_argument('--lib-dir',
173 default='/usr/share/opnfv-apex',
174 help='Directory path for apex ansible '
175 'and third party libs')
176 deploy_parser.add_argument('--quickstart', action='store_true',
178 help='Use tripleo-quickstart to deploy')
179 deploy_parser.add_argument('--upstream', action='store_true',
181 help='Force deployment to use upstream '
183 deploy_parser.add_argument('--no-fetch', action='store_true',
185 help='Ignore fetching latest upstream and '
186 'use what is in cache')
190 def validate_deploy_args(args):
192 Validates arguments for deploy
197 logging.debug('Validating arguments for deployment')
198 if args.virtual and args.inventory_file is not None:
199 logging.error("Virtual enabled but inventory file also given")
200 raise ApexDeployException('You should not specify an inventory file '
201 'with virtual deployments')
203 args.inventory_file = os.path.join(APEX_TEMP_DIR,
204 'inventory-virt.yaml')
205 elif os.path.isfile(args.inventory_file) is False:
206 logging.error("Specified inventory file does not exist: {}".format(
207 args.inventory_file))
208 raise ApexDeployException('Specified inventory file does not exist')
210 for settings_file in (args.deploy_settings_file,
211 args.network_settings_file):
212 if os.path.isfile(settings_file) is False:
213 logging.error("Specified settings file does not "
214 "exist: {}".format(settings_file))
215 raise ApexDeployException('Specified settings file does not '
216 'exist: {}'.format(settings_file))
220 parser = create_deploy_parser()
221 args = parser.parse_args(sys.argv[1:])
222 # FIXME (trozet): this is only needed as a workaround for CI. Remove
224 if os.getenv('IMAGES', False):
225 args.image_dir = os.getenv('IMAGES')
227 log_level = logging.DEBUG
229 log_level = logging.INFO
230 os.makedirs(os.path.dirname(args.log_file), exist_ok=True)
231 formatter = '%(asctime)s %(levelname)s: %(message)s'
232 logging.basicConfig(filename=args.log_file,
234 datefmt='%m/%d/%Y %I:%M:%S %p',
236 console = logging.StreamHandler()
237 console.setLevel(log_level)
238 console.setFormatter(logging.Formatter(formatter))
239 logging.getLogger('').addHandler(console)
240 utils.install_ansible()
241 validate_deploy_args(args)
243 deploy_settings = DeploySettings(args.deploy_settings_file)
244 logging.info("Deploy settings are:\n {}".format(pprint.pformat(
246 net_settings = NetworkSettings(args.network_settings_file)
247 logging.info("Network settings are:\n {}".format(pprint.pformat(
249 os_version = deploy_settings['deploy_options']['os_version']
250 net_env_file = os.path.join(args.deploy_dir, constants.NET_ENV_FILE)
251 net_env = NetworkEnvironment(net_settings, net_env_file,
252 os_version=os_version)
253 net_env_target = os.path.join(APEX_TEMP_DIR, constants.NET_ENV_FILE)
254 utils.dump_yaml(dict(net_env), net_env_target)
256 # get global deploy params
257 ha_enabled = deploy_settings['global_params']['ha_enabled']
258 introspect = deploy_settings['global_params'].get('introspect', True)
261 if args.virt_compute_ram is None:
262 compute_ram = args.virt_default_ram
264 compute_ram = args.virt_compute_ram
265 if deploy_settings['deploy_options']['sdn_controller'] == \
266 'opendaylight' and args.virt_default_ram < 12:
268 logging.warning('RAM per controller is too low. OpenDaylight '
269 'requires at least 12GB per controller.')
270 logging.info('Increasing RAM per controller to 12GB')
271 elif args.virt_default_ram < 10:
273 logging.warning('RAM per controller is too low. nosdn '
274 'requires at least 10GB per controller.')
275 logging.info('Increasing RAM per controller to 10GB')
277 control_ram = args.virt_default_ram
278 if ha_enabled and args.virt_compute_nodes < 2:
279 logging.debug('HA enabled, bumping number of compute nodes to 2')
280 args.virt_compute_nodes = 2
281 virt_utils.generate_inventory(args.inventory_file, ha_enabled,
282 num_computes=args.virt_compute_nodes,
283 controller_ram=control_ram * 1024,
284 compute_ram=compute_ram * 1024,
287 inventory = Inventory(args.inventory_file, ha_enabled, args.virtual)
289 validate_cross_settings(deploy_settings, net_settings, inventory)
290 ds_opts = deploy_settings['deploy_options']
292 deploy_settings_file = os.path.join(APEX_TEMP_DIR,
293 'apex_deploy_settings.yaml')
294 utils.dump_yaml(utils.dict_objects_to_str(deploy_settings),
295 deploy_settings_file)
296 logging.info("File created: {}".format(deploy_settings_file))
297 network_settings_file = os.path.join(APEX_TEMP_DIR,
298 'apex_network_settings.yaml')
299 utils.dump_yaml(utils.dict_objects_to_str(net_settings),
300 network_settings_file)
301 logging.info("File created: {}".format(network_settings_file))
302 deploy_quickstart(args, deploy_settings_file, network_settings_file,
305 # TODO (trozet): add logic back from:
306 # Iedb75994d35b5dc1dd5d5ce1a57277c8f3729dfd (FDIO DVR)
308 'virsh_enabled_networks': net_settings.enabled_network_list
310 utils.run_ansible(ansible_args,
311 os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
312 'deploy_dependencies.yml'))
314 if 'external' in net_settings.enabled_network_list:
317 # create all overcloud VMs
318 build_vms(inventory, net_settings, args.deploy_dir)
320 # Attach interfaces to jumphost for baremetal deployment
321 jump_networks = ['admin']
323 jump_networks.append('external')
324 for network in jump_networks:
325 if network == 'external':
326 # TODO(trozet): enable vlan secondary external networks
327 iface = net_settings['networks'][network][0][
328 'installer_vm']['members'][0]
330 iface = net_settings['networks'][network]['installer_vm'][
332 bridge = "br-{}".format(network)
333 jumphost.attach_interface_to_ovs(bridge, iface, network)
334 instackenv_json = os.path.join(APEX_TEMP_DIR, 'instackenv.json')
335 with open(instackenv_json, 'w') as fh:
336 json.dump(inventory, fh)
338 # Create and configure undercloud
340 root_pw = constants.DEBUG_OVERCLOUD_PW
344 upstream = (os_version != constants.DEFAULT_OS_VERSION or
346 if os_version == 'master':
349 branch = "stable/{}".format(os_version)
351 logging.info("Deploying with upstream artifacts for OpenStack "
352 "{}".format(os_version))
353 args.image_dir = os.path.join(args.image_dir, os_version)
354 upstream_url = constants.UPSTREAM_RDO.replace(
355 constants.DEFAULT_OS_VERSION, os_version)
356 upstream_targets = ['overcloud-full.tar', 'undercloud.qcow2']
357 utils.fetch_upstream_and_unpack(args.image_dir, upstream_url,
359 fetch=not args.no_fetch)
360 sdn_image = os.path.join(args.image_dir, 'overcloud-full.qcow2')
361 # copy undercloud so we don't taint upstream fetch
362 uc_image = os.path.join(args.image_dir, 'undercloud_mod.qcow2')
363 uc_fetch_img = os.path.join(args.image_dir, 'undercloud.qcow2')
364 shutil.copyfile(uc_fetch_img, uc_image)
365 # prep undercloud with required packages
366 uc_builder.add_upstream_packages(uc_image)
367 # add patches from upstream to undercloud and overcloud
368 logging.info('Adding patches to undercloud')
369 patches = deploy_settings['global_params']['patches']
370 c_builder.add_upstream_patches(patches['undercloud'], uc_image,
371 APEX_TEMP_DIR, branch)
373 sdn_image = os.path.join(args.image_dir, SDN_IMAGE)
374 uc_image = 'undercloud.qcow2'
375 # patches are ignored in non-upstream deployments
376 patches = {'overcloud': [], 'undercloud': []}
377 # Create/Start Undercloud VM
378 undercloud = uc_lib.Undercloud(args.image_dir,
381 external_network=uc_external,
382 image_name=os.path.basename(uc_image),
383 os_version=os_version)
385 undercloud_admin_ip = net_settings['networks'][
386 constants.ADMIN_NETWORK]['installer_vm']['ip']
388 if upstream and ds_opts['containers']:
389 tag = constants.DOCKER_TAG
393 # Generate nic templates
394 for role in 'compute', 'controller':
395 oc_cfg.create_nic_template(net_settings, deploy_settings, role,
396 args.deploy_dir, APEX_TEMP_DIR)
398 undercloud.configure(net_settings, deploy_settings,
399 os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
400 'configure_undercloud.yml'),
401 APEX_TEMP_DIR, virtual_oc=args.virtual)
403 # Prepare overcloud-full.qcow2
404 logging.info("Preparing Overcloud for deployment...")
405 if os_version != 'ocata':
406 net_data_file = os.path.join(APEX_TEMP_DIR, 'network_data.yaml')
407 net_data = network_data.create_network_data(net_settings,
411 if upstream and args.env_file == 'opnfv-environment.yaml':
412 # Override the env_file if it is defaulted to opnfv
413 # opnfv env file will not work with upstream
414 args.env_file = 'upstream-environment.yaml'
415 opnfv_env = os.path.join(args.deploy_dir, args.env_file)
417 # TODO(trozet): Invoke with containers after Fraser migration
418 oc_deploy.prep_env(deploy_settings, net_settings, inventory,
419 opnfv_env, net_env_target, APEX_TEMP_DIR)
423 os.path.join(APEX_TEMP_DIR, os.path.basename(opnfv_env))
425 patched_containers = oc_deploy.prep_image(
426 deploy_settings, net_settings, sdn_image, APEX_TEMP_DIR,
427 root_pw=root_pw, docker_tag=tag, patches=patches['overcloud'],
430 oc_deploy.create_deploy_cmd(deploy_settings, net_settings, inventory,
431 APEX_TEMP_DIR, args.virtual,
432 os.path.basename(opnfv_env),
434 # Prepare undercloud with containers
435 docker_playbook = os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
436 'prepare_overcloud_containers.yml')
437 if ds_opts['containers']:
438 ceph_version = constants.CEPH_VERSION_MAP[ds_opts['os_version']]
439 ceph_docker_image = "ceph/daemon:tag-build-master-" \
440 "{}-centos-7".format(ceph_version)
441 logging.info("Preparing Undercloud with Docker containers")
442 if patched_containers:
443 oc_builder.archive_docker_patches(APEX_TEMP_DIR)
444 container_vars = dict()
445 container_vars['apex_temp_dir'] = APEX_TEMP_DIR
446 container_vars['patched_docker_services'] = list(
448 container_vars['container_tag'] = constants.DOCKER_TAG
449 container_vars['stackrc'] = 'source /home/stack/stackrc'
450 container_vars['upstream'] = upstream
451 container_vars['sdn'] = ds_opts['sdn_controller']
452 container_vars['undercloud_ip'] = undercloud_admin_ip
453 container_vars['os_version'] = os_version
454 container_vars['ceph_docker_image'] = ceph_docker_image
455 container_vars['sdn_env_file'] = \
456 oc_deploy.get_docker_sdn_file(ds_opts)
458 utils.run_ansible(container_vars, docker_playbook,
459 host=undercloud.ip, user='stack',
460 tmp_dir=APEX_TEMP_DIR)
461 logging.info("Container preparation complete")
463 logging.error("Unable to complete container prep on "
465 os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2'))
468 deploy_playbook = os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
469 'deploy_overcloud.yml')
470 virt_env = 'virtual-environment.yaml'
471 bm_env = 'baremetal-environment.yaml'
472 for p_env in virt_env, bm_env:
473 shutil.copyfile(os.path.join(args.deploy_dir, p_env),
474 os.path.join(APEX_TEMP_DIR, p_env))
476 # Start Overcloud Deployment
477 logging.info("Executing Overcloud Deployment...")
479 deploy_vars['virtual'] = args.virtual
480 deploy_vars['debug'] = args.debug
481 deploy_vars['aarch64'] = platform.machine() == 'aarch64'
482 deploy_vars['introspect'] = not (args.virtual or
483 deploy_vars['aarch64'] or
485 deploy_vars['dns_server_args'] = ''
486 deploy_vars['apex_temp_dir'] = APEX_TEMP_DIR
487 deploy_vars['apex_env_file'] = os.path.basename(opnfv_env)
488 deploy_vars['stackrc'] = 'source /home/stack/stackrc'
489 deploy_vars['overcloudrc'] = 'source /home/stack/overcloudrc'
490 deploy_vars['upstream'] = upstream
491 deploy_vars['os_version'] = os_version
492 deploy_vars['http_proxy'] = net_settings.get('http_proxy', '')
493 deploy_vars['https_proxy'] = net_settings.get('https_proxy', '')
494 for dns_server in net_settings['dns_servers']:
495 deploy_vars['dns_server_args'] += " --dns-nameserver {}".format(
498 utils.run_ansible(deploy_vars, deploy_playbook, host=undercloud.ip,
499 user='stack', tmp_dir=APEX_TEMP_DIR)
500 logging.info("Overcloud deployment complete")
502 logging.error("Deployment Failed. Please check log")
505 os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2'))
508 logging.info("Executing post deploy configuration")
509 jumphost.configure_bridges(net_settings)
510 nova_output = os.path.join(APEX_TEMP_DIR, 'nova_output')
511 deploy_vars['overcloud_nodes'] = parsers.parse_nova_output(
513 deploy_vars['SSH_OPTIONS'] = '-o StrictHostKeyChecking=no -o ' \
514 'GlobalKnownHostsFile=/dev/null -o ' \
515 'UserKnownHostsFile=/dev/null -o ' \
517 deploy_vars['external_network_cmds'] = \
518 oc_deploy.external_network_cmds(net_settings)
519 # TODO(trozet): just parse all ds_opts as deploy vars one time
520 deploy_vars['gluon'] = ds_opts['gluon']
521 deploy_vars['sdn'] = ds_opts['sdn_controller']
522 for dep_option in 'yardstick', 'dovetail', 'vsperf':
523 if dep_option in ds_opts:
524 deploy_vars[dep_option] = ds_opts[dep_option]
526 deploy_vars[dep_option] = False
527 deploy_vars['dataplane'] = ds_opts['dataplane']
528 overcloudrc = os.path.join(APEX_TEMP_DIR, 'overcloudrc')
529 if ds_opts['congress']:
530 deploy_vars['congress_datasources'] = \
531 oc_deploy.create_congress_cmds(overcloudrc)
532 deploy_vars['congress'] = True
534 deploy_vars['congress'] = False
535 deploy_vars['calipso'] = ds_opts.get('calipso', False)
536 deploy_vars['calipso_ip'] = undercloud_admin_ip
537 # overcloudrc.v3 removed and set as default in queens and later
538 if os_version == 'pike':
539 deploy_vars['overcloudrc_files'] = ['overcloudrc',
542 deploy_vars['overcloudrc_files'] = ['overcloudrc']
544 post_undercloud = os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
545 'post_deploy_undercloud.yml')
546 logging.info("Executing post deploy configuration undercloud playbook")
548 utils.run_ansible(deploy_vars, post_undercloud, host=undercloud.ip,
549 user='stack', tmp_dir=APEX_TEMP_DIR)
550 logging.info("Post Deploy Undercloud Configuration Complete")
552 logging.error("Post Deploy Undercloud Configuration failed. "
555 # Post deploy overcloud node configuration
556 # TODO(trozet): just parse all ds_opts as deploy vars one time
557 deploy_vars['sfc'] = ds_opts['sfc']
558 deploy_vars['vpn'] = ds_opts['vpn']
559 deploy_vars['l2gw'] = ds_opts.get('l2gw')
560 deploy_vars['sriov'] = ds_opts.get('sriov')
561 # TODO(trozet): pull all logs and store in tmp dir in overcloud
563 post_overcloud = os.path.join(args.lib_dir, constants.ANSIBLE_PATH,
564 'post_deploy_overcloud.yml')
565 # Run per overcloud node
566 for node, ip in deploy_vars['overcloud_nodes'].items():
567 logging.info("Executing Post deploy overcloud playbook on "
568 "node {}".format(node))
570 utils.run_ansible(deploy_vars, post_overcloud, host=ip,
571 user='heat-admin', tmp_dir=APEX_TEMP_DIR)
572 logging.info("Post Deploy Overcloud Configuration Complete "
573 "for node {}".format(node))
575 logging.error("Post Deploy Overcloud Configuration failed "
576 "for node {}. Please check log".format(node))
578 logging.info("Apex deployment complete")
579 logging.info("Undercloud IP: {}, please connect by doing "
580 "'opnfv-util undercloud'".format(undercloud.ip))
581 # TODO(trozet): add logging here showing controller VIP and horizon url
584 if __name__ == '__main__':