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 ANSIBLE_PATH = 'ansible/playbooks'
43 SDN_IMAGE = 'overcloud-full-opendaylight.qcow2'
46 def deploy_quickstart(args, deploy_settings_file, network_settings_file,
51 def validate_cross_settings(deploy_settings, net_settings, inventory):
53 Used to validate compatibility across settings file.
54 :param deploy_settings: parsed settings for deployment
55 :param net_settings: parsed settings for network
56 :param inventory: parsed inventory file
60 if deploy_settings['deploy_options']['dataplane'] != 'ovs' and 'tenant' \
61 not in net_settings.enabled_network_list:
62 raise ApexDeployException("Setting a DPDK based dataplane requires"
63 "a dedicated NIC for tenant network")
65 if 'odl_vpp_routing_node' in deploy_settings['deploy_options']:
66 if deploy_settings['deploy_options']['dataplane'] != 'fdio':
67 raise ApexDeployException("odl_vpp_routing_node should only be set"
68 "when dataplane is set to fdio")
69 if deploy_settings['deploy_options'].get('dvr') is True:
70 raise ApexDeployException("odl_vpp_routing_node should only be set"
71 "when dvr is not enabled")
73 # TODO(trozet): add more checks here like RAM for ODL, etc
74 # check if odl_vpp_netvirt is true and vpp is set
75 # Check if fdio and nosdn:
76 # tenant_nic_mapping_controller_members" ==
77 # "$tenant_nic_mapping_compute_members
80 def build_vms(inventory, network_settings,
81 template_dir='/usr/share/opnfv-apex'):
83 Creates VMs and configures vbmc and host
85 :param network_settings:
89 for idx, node in enumerate(inventory['nodes']):
90 name = 'baremetal{}'.format(idx)
91 volume = name + ".qcow2"
92 volume_path = os.path.join(constants.LIBVIRT_VOLUME_PATH, volume)
93 # TODO(trozet): add error checking
96 baremetal_interfaces=network_settings.enabled_network_list,
97 memory=node['memory'], cpus=node['cpu'],
99 template_dir=template_dir)
100 virt_utils.host_setup({name: node['pm_port']})
103 def create_deploy_parser():
104 deploy_parser = argparse.ArgumentParser()
105 deploy_parser.add_argument('--debug', action='store_true', default=False,
106 help="Turn on debug messages")
107 deploy_parser.add_argument('-l', '--log-file',
108 default='./apex_deploy.log',
109 dest='log_file', help="Log file to log to")
110 deploy_parser.add_argument('-d', '--deploy-settings',
111 dest='deploy_settings_file',
113 help='File which contains Apex deploy settings')
114 deploy_parser.add_argument('-n', '--network-settings',
115 dest='network_settings_file',
117 help='File which contains Apex network '
119 deploy_parser.add_argument('-i', '--inventory-file',
120 dest='inventory_file',
122 help='Inventory file which contains POD '
124 deploy_parser.add_argument('-e', '--environment-file',
126 default='opnfv-environment.yaml',
127 help='Provide alternate base env file located '
129 deploy_parser.add_argument('-v', '--virtual', action='store_true',
132 help='Enable virtual deployment')
133 deploy_parser.add_argument('--interactive', action='store_true',
135 help='Enable interactive deployment mode which '
136 'requires user to confirm steps of '
138 deploy_parser.add_argument('--virtual-computes',
139 dest='virt_compute_nodes',
142 help='Number of Virtual Compute nodes to create'
143 ' and use during deployment (defaults to 1'
144 ' for noha and 2 for ha)')
145 deploy_parser.add_argument('--virtual-cpus',
149 help='Number of CPUs to use per Overcloud VM in'
150 ' a virtual deployment (defaults to 4)')
151 deploy_parser.add_argument('--virtual-default-ram',
152 dest='virt_default_ram',
155 help='Amount of default RAM to use per '
156 'Overcloud VM in GB (defaults to 8).')
157 deploy_parser.add_argument('--virtual-compute-ram',
158 dest='virt_compute_ram',
161 help='Amount of RAM to use per Overcloud '
162 'Compute VM in GB (defaults to 8). '
163 'Overrides --virtual-default-ram arg for '
165 deploy_parser.add_argument('--deploy-dir',
166 default='/usr/share/opnfv-apex',
167 help='Directory to deploy from which contains '
168 'base config files for deployment')
169 deploy_parser.add_argument('--image-dir',
170 default='/var/opt/opnfv/images',
171 help='Directory which contains '
172 'base disk images for deployment')
173 deploy_parser.add_argument('--lib-dir',
174 default='/usr/share/opnfv-apex',
175 help='Directory path for apex ansible '
176 'and third party libs')
177 deploy_parser.add_argument('--quickstart', action='store_true',
179 help='Use tripleo-quickstart to deploy')
180 deploy_parser.add_argument('--upstream', action='store_true',
182 help='Force deployment to use upstream '
184 deploy_parser.add_argument('--no-fetch', action='store_true',
186 help='Ignore fetching latest upstream and '
187 'use what is in cache')
191 def validate_deploy_args(args):
193 Validates arguments for deploy
198 logging.debug('Validating arguments for deployment')
199 if args.virtual and args.inventory_file is not None:
200 logging.error("Virtual enabled but inventory file also given")
201 raise ApexDeployException('You should not specify an inventory file '
202 'with virtual deployments')
204 args.inventory_file = os.path.join(APEX_TEMP_DIR,
205 'inventory-virt.yaml')
206 elif os.path.isfile(args.inventory_file) is False:
207 logging.error("Specified inventory file does not exist: {}".format(
208 args.inventory_file))
209 raise ApexDeployException('Specified inventory file does not exist')
211 for settings_file in (args.deploy_settings_file,
212 args.network_settings_file):
213 if os.path.isfile(settings_file) is False:
214 logging.error("Specified settings file does not "
215 "exist: {}".format(settings_file))
216 raise ApexDeployException('Specified settings file does not '
217 'exist: {}'.format(settings_file))
221 parser = create_deploy_parser()
222 args = parser.parse_args(sys.argv[1:])
223 # FIXME (trozet): this is only needed as a workaround for CI. Remove
225 if os.getenv('IMAGES', False):
226 args.image_dir = os.getenv('IMAGES')
228 log_level = logging.DEBUG
230 log_level = logging.INFO
231 os.makedirs(os.path.dirname(args.log_file), exist_ok=True)
232 formatter = '%(asctime)s %(levelname)s: %(message)s'
233 logging.basicConfig(filename=args.log_file,
235 datefmt='%m/%d/%Y %I:%M:%S %p',
237 console = logging.StreamHandler()
238 console.setLevel(log_level)
239 console.setFormatter(logging.Formatter(formatter))
240 logging.getLogger('').addHandler(console)
241 utils.install_ansible()
242 validate_deploy_args(args)
244 deploy_settings = DeploySettings(args.deploy_settings_file)
245 logging.info("Deploy settings are:\n {}".format(pprint.pformat(
247 net_settings = NetworkSettings(args.network_settings_file)
248 logging.info("Network settings are:\n {}".format(pprint.pformat(
250 os_version = deploy_settings['deploy_options']['os_version']
251 net_env_file = os.path.join(args.deploy_dir, constants.NET_ENV_FILE)
252 net_env = NetworkEnvironment(net_settings, net_env_file,
253 os_version=os_version)
254 net_env_target = os.path.join(APEX_TEMP_DIR, constants.NET_ENV_FILE)
255 utils.dump_yaml(dict(net_env), net_env_target)
257 # get global deploy params
258 ha_enabled = deploy_settings['global_params']['ha_enabled']
259 introspect = deploy_settings['global_params'].get('introspect', True)
262 if args.virt_compute_ram is None:
263 compute_ram = args.virt_default_ram
265 compute_ram = args.virt_compute_ram
266 if deploy_settings['deploy_options']['sdn_controller'] == \
267 'opendaylight' and args.virt_default_ram < 12:
269 logging.warning('RAM per controller is too low. OpenDaylight '
270 'requires at least 12GB per controller.')
271 logging.info('Increasing RAM per controller to 12GB')
272 elif args.virt_default_ram < 10:
274 logging.warning('RAM per controller is too low. nosdn '
275 'requires at least 10GB per controller.')
276 logging.info('Increasing RAM per controller to 10GB')
278 control_ram = args.virt_default_ram
279 if ha_enabled and args.virt_compute_nodes < 2:
280 logging.debug('HA enabled, bumping number of compute nodes to 2')
281 args.virt_compute_nodes = 2
282 virt_utils.generate_inventory(args.inventory_file, ha_enabled,
283 num_computes=args.virt_compute_nodes,
284 controller_ram=control_ram * 1024,
285 compute_ram=compute_ram * 1024,
288 inventory = Inventory(args.inventory_file, ha_enabled, args.virtual)
290 validate_cross_settings(deploy_settings, net_settings, inventory)
291 ds_opts = deploy_settings['deploy_options']
293 deploy_settings_file = os.path.join(APEX_TEMP_DIR,
294 'apex_deploy_settings.yaml')
295 utils.dump_yaml(utils.dict_objects_to_str(deploy_settings),
296 deploy_settings_file)
297 logging.info("File created: {}".format(deploy_settings_file))
298 network_settings_file = os.path.join(APEX_TEMP_DIR,
299 'apex_network_settings.yaml')
300 utils.dump_yaml(utils.dict_objects_to_str(net_settings),
301 network_settings_file)
302 logging.info("File created: {}".format(network_settings_file))
303 deploy_quickstart(args, deploy_settings_file, network_settings_file,
306 # TODO (trozet): add logic back from:
307 # Iedb75994d35b5dc1dd5d5ce1a57277c8f3729dfd (FDIO DVR)
309 'virsh_enabled_networks': net_settings.enabled_network_list
311 utils.run_ansible(ansible_args,
312 os.path.join(args.lib_dir, ANSIBLE_PATH,
313 'deploy_dependencies.yml'))
315 if 'external' in net_settings.enabled_network_list:
318 # create all overcloud VMs
319 build_vms(inventory, net_settings, args.deploy_dir)
321 # Attach interfaces to jumphost for baremetal deployment
322 jump_networks = ['admin']
324 jump_networks.append('external')
325 for network in jump_networks:
326 if network == 'external':
327 # TODO(trozet): enable vlan secondary external networks
328 iface = net_settings['networks'][network][0][
329 'installer_vm']['members'][0]
331 iface = net_settings['networks'][network]['installer_vm'][
333 bridge = "br-{}".format(network)
334 jumphost.attach_interface_to_ovs(bridge, iface, network)
335 instackenv_json = os.path.join(APEX_TEMP_DIR, 'instackenv.json')
336 with open(instackenv_json, 'w') as fh:
337 json.dump(inventory, fh)
339 # Create and configure undercloud
341 root_pw = constants.DEBUG_OVERCLOUD_PW
345 upstream = (os_version != constants.DEFAULT_OS_VERSION or
347 if os_version == 'master':
350 branch = "stable/{}".format(os_version)
352 logging.info("Deploying with upstream artifacts for OpenStack "
353 "{}".format(os_version))
354 args.image_dir = os.path.join(args.image_dir, os_version)
355 upstream_url = constants.UPSTREAM_RDO.replace(
356 constants.DEFAULT_OS_VERSION, os_version)
357 upstream_targets = ['overcloud-full.tar', 'undercloud.qcow2']
358 utils.fetch_upstream_and_unpack(args.image_dir, upstream_url,
360 fetch=not args.no_fetch)
361 sdn_image = os.path.join(args.image_dir, 'overcloud-full.qcow2')
362 # copy undercloud so we don't taint upstream fetch
363 uc_image = os.path.join(args.image_dir, 'undercloud_mod.qcow2')
364 uc_fetch_img = os.path.join(args.image_dir, 'undercloud.qcow2')
365 shutil.copyfile(uc_fetch_img, uc_image)
366 # prep undercloud with required packages
367 uc_builder.add_upstream_packages(uc_image)
368 # add patches from upstream to undercloud and overcloud
369 logging.info('Adding patches to undercloud')
370 patches = deploy_settings['global_params']['patches']
371 c_builder.add_upstream_patches(patches['undercloud'], uc_image,
372 APEX_TEMP_DIR, branch)
374 sdn_image = os.path.join(args.image_dir, SDN_IMAGE)
375 uc_image = 'undercloud.qcow2'
376 # patches are ignored in non-upstream deployments
377 patches = {'overcloud': [], 'undercloud': []}
378 # Create/Start Undercloud VM
379 undercloud = uc_lib.Undercloud(args.image_dir,
382 external_network=uc_external,
383 image_name=os.path.basename(uc_image),
384 os_version=os_version)
386 undercloud_admin_ip = net_settings['networks'][
387 constants.ADMIN_NETWORK]['installer_vm']['ip']
389 if upstream and ds_opts['containers']:
390 tag = constants.DOCKER_TAG
394 # Generate nic templates
395 for role in 'compute', 'controller':
396 oc_cfg.create_nic_template(net_settings, deploy_settings, role,
397 args.deploy_dir, APEX_TEMP_DIR)
399 undercloud.configure(net_settings, deploy_settings,
400 os.path.join(args.lib_dir, ANSIBLE_PATH,
401 'configure_undercloud.yml'),
402 APEX_TEMP_DIR, virtual_oc=args.virtual)
404 # Prepare overcloud-full.qcow2
405 logging.info("Preparing Overcloud for deployment...")
406 if os_version != 'ocata':
407 net_data_file = os.path.join(APEX_TEMP_DIR, 'network_data.yaml')
408 net_data = network_data.create_network_data(net_settings,
412 if upstream and args.env_file == 'opnfv-environment.yaml':
413 # Override the env_file if it is defaulted to opnfv
414 # opnfv env file will not work with upstream
415 args.env_file = 'upstream-environment.yaml'
416 opnfv_env = os.path.join(args.deploy_dir, args.env_file)
418 # TODO(trozet): Invoke with containers after Fraser migration
419 oc_deploy.prep_env(deploy_settings, net_settings, inventory,
420 opnfv_env, net_env_target, APEX_TEMP_DIR)
424 os.path.join(APEX_TEMP_DIR, os.path.basename(opnfv_env))
426 patched_containers = oc_deploy.prep_image(
427 deploy_settings, net_settings, sdn_image, APEX_TEMP_DIR,
428 root_pw=root_pw, docker_tag=tag, patches=patches['overcloud'],
431 oc_deploy.create_deploy_cmd(deploy_settings, net_settings, inventory,
432 APEX_TEMP_DIR, args.virtual,
433 os.path.basename(opnfv_env),
435 # Prepare undercloud with containers
436 docker_playbook = os.path.join(args.lib_dir, ANSIBLE_PATH,
437 'prepare_overcloud_containers.yml')
438 if ds_opts['containers']:
439 ceph_version = constants.CEPH_VERSION_MAP[ds_opts['os_version']]
440 ceph_docker_image = "ceph/daemon:tag-build-master-" \
441 "{}-centos-7".format(ceph_version)
442 logging.info("Preparing Undercloud with Docker containers")
443 if patched_containers:
444 oc_builder.archive_docker_patches(APEX_TEMP_DIR)
445 container_vars = dict()
446 container_vars['apex_temp_dir'] = APEX_TEMP_DIR
447 container_vars['patched_docker_services'] = list(
449 container_vars['container_tag'] = constants.DOCKER_TAG
450 container_vars['stackrc'] = 'source /home/stack/stackrc'
451 container_vars['upstream'] = upstream
452 container_vars['sdn'] = ds_opts['sdn_controller']
453 container_vars['undercloud_ip'] = undercloud_admin_ip
454 container_vars['os_version'] = os_version
455 container_vars['ceph_docker_image'] = ceph_docker_image
456 container_vars['sdn_env_file'] = \
457 oc_deploy.get_docker_sdn_file(ds_opts)
459 utils.run_ansible(container_vars, docker_playbook,
460 host=undercloud.ip, user='stack',
461 tmp_dir=APEX_TEMP_DIR)
462 logging.info("Container preparation complete")
464 logging.error("Unable to complete container prep on "
466 os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2'))
469 deploy_playbook = os.path.join(args.lib_dir, ANSIBLE_PATH,
470 'deploy_overcloud.yml')
471 virt_env = 'virtual-environment.yaml'
472 bm_env = 'baremetal-environment.yaml'
473 for p_env in virt_env, bm_env:
474 shutil.copyfile(os.path.join(args.deploy_dir, p_env),
475 os.path.join(APEX_TEMP_DIR, p_env))
477 # Start Overcloud Deployment
478 logging.info("Executing Overcloud Deployment...")
480 deploy_vars['virtual'] = args.virtual
481 deploy_vars['debug'] = args.debug
482 deploy_vars['aarch64'] = platform.machine() == 'aarch64'
483 deploy_vars['introspect'] = not (args.virtual or
484 deploy_vars['aarch64'] or
486 deploy_vars['dns_server_args'] = ''
487 deploy_vars['apex_temp_dir'] = APEX_TEMP_DIR
488 deploy_vars['apex_env_file'] = os.path.basename(opnfv_env)
489 deploy_vars['stackrc'] = 'source /home/stack/stackrc'
490 deploy_vars['overcloudrc'] = 'source /home/stack/overcloudrc'
491 deploy_vars['upstream'] = upstream
492 deploy_vars['os_version'] = os_version
493 deploy_vars['http_proxy'] = net_settings.get('http_proxy', '')
494 deploy_vars['https_proxy'] = net_settings.get('https_proxy', '')
495 for dns_server in net_settings['dns_servers']:
496 deploy_vars['dns_server_args'] += " --dns-nameserver {}".format(
499 utils.run_ansible(deploy_vars, deploy_playbook, host=undercloud.ip,
500 user='stack', tmp_dir=APEX_TEMP_DIR)
501 logging.info("Overcloud deployment complete")
503 logging.error("Deployment Failed. Please check log")
506 os.remove(os.path.join(APEX_TEMP_DIR, 'overcloud-full.qcow2'))
509 logging.info("Executing post deploy configuration")
510 jumphost.configure_bridges(net_settings)
511 nova_output = os.path.join(APEX_TEMP_DIR, 'nova_output')
512 deploy_vars['overcloud_nodes'] = parsers.parse_nova_output(
514 deploy_vars['SSH_OPTIONS'] = '-o StrictHostKeyChecking=no -o ' \
515 'GlobalKnownHostsFile=/dev/null -o ' \
516 'UserKnownHostsFile=/dev/null -o ' \
518 deploy_vars['external_network_cmds'] = \
519 oc_deploy.external_network_cmds(net_settings)
520 # TODO(trozet): just parse all ds_opts as deploy vars one time
521 deploy_vars['gluon'] = ds_opts['gluon']
522 deploy_vars['sdn'] = ds_opts['sdn_controller']
523 for dep_option in 'yardstick', 'dovetail', 'vsperf':
524 if dep_option in ds_opts:
525 deploy_vars[dep_option] = ds_opts[dep_option]
527 deploy_vars[dep_option] = False
528 deploy_vars['dataplane'] = ds_opts['dataplane']
529 overcloudrc = os.path.join(APEX_TEMP_DIR, 'overcloudrc')
530 if ds_opts['congress']:
531 deploy_vars['congress_datasources'] = \
532 oc_deploy.create_congress_cmds(overcloudrc)
533 deploy_vars['congress'] = True
535 deploy_vars['congress'] = False
536 deploy_vars['calipso'] = ds_opts.get('calipso', False)
537 deploy_vars['calipso_ip'] = undercloud_admin_ip
538 # overcloudrc.v3 removed and set as default in queens and later
539 if os_version == 'pike':
540 deploy_vars['overcloudrc_files'] = ['overcloudrc',
543 deploy_vars['overcloudrc_files'] = ['overcloudrc']
545 post_undercloud = os.path.join(args.lib_dir, ANSIBLE_PATH,
546 'post_deploy_undercloud.yml')
547 logging.info("Executing post deploy configuration undercloud playbook")
549 utils.run_ansible(deploy_vars, post_undercloud, host=undercloud.ip,
550 user='stack', tmp_dir=APEX_TEMP_DIR)
551 logging.info("Post Deploy Undercloud Configuration Complete")
553 logging.error("Post Deploy Undercloud Configuration failed. "
556 # Post deploy overcloud node configuration
557 # TODO(trozet): just parse all ds_opts as deploy vars one time
558 deploy_vars['sfc'] = ds_opts['sfc']
559 deploy_vars['vpn'] = ds_opts['vpn']
560 deploy_vars['l2gw'] = ds_opts.get('l2gw')
561 deploy_vars['sriov'] = ds_opts.get('sriov')
562 # TODO(trozet): pull all logs and store in tmp dir in overcloud
564 post_overcloud = os.path.join(args.lib_dir, ANSIBLE_PATH,
565 'post_deploy_overcloud.yml')
566 # Run per overcloud node
567 for node, ip in deploy_vars['overcloud_nodes'].items():
568 logging.info("Executing Post deploy overcloud playbook on "
569 "node {}".format(node))
571 utils.run_ansible(deploy_vars, post_overcloud, host=ip,
572 user='heat-admin', tmp_dir=APEX_TEMP_DIR)
573 logging.info("Post Deploy Overcloud Configuration Complete "
574 "for node {}".format(node))
576 logging.error("Post Deploy Overcloud Configuration failed "
577 "for node {}. Please check log".format(node))
579 logging.info("Apex deployment complete")
580 logging.info("Undercloud IP: {}, please connect by doing "
581 "'opnfv-util undercloud'".format(undercloud.ip))
582 # TODO(trozet): add logging here showing controller VIP and horizon url
585 if __name__ == '__main__':