# Copyright 2017 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import json import logging import time import uuid from heatclient.common import template_utils from heatclient import exc as heat_exc from swiftclient import exceptions as swiftexceptions from tripleo_common import constants from tripleo_common import update from tripleo_common.utils import plan as plan_utils from tripleo_common.utils import template as templates LOG = logging.getLogger(__name__) def stack_update(swift, heat, timeout, container=constants.DEFAULT_CONTAINER_NAME): # get the stack. Error if doesn't exist try: stack = heat.stacks.get(container) except heat_exc.HTTPNotFound: msg = "Error retrieving stack: %s" % container LOG.exception(msg) raise RuntimeError(msg) try: env = plan_utils.get_env(swift, container) except swiftexceptions.ClientException as err: err_msg = ("Error retrieving environment for plan %s: %s" % ( container, err)) LOG.exception(err_msg) raise RuntimeError(err_msg) update_env = { 'parameter_defaults': { 'DeployIdentifier': int(time.time()), }, } noop_env = { 'resource_registry': { 'OS::TripleO::DeploymentSteps': 'OS::Heat::None', }, } for output in stack.to_dict().get('outputs', {}): if output['output_key'] == 'RoleData': for role in output['output_value']: role_env = { "OS::TripleO::Tasks::%sPreConfig" % role: 'OS::Heat::None', "OS::TripleO::Tasks::%sPostConfig" % role: 'OS::Heat::None', } noop_env['resource_registry'].update(role_env) update_env.update(noop_env) template_utils.deep_update(env, update_env) try: plan_utils.put_env(swift, env) except swiftexceptions.ClientException as err: err_msg = ("Error updating environment for plan %s: %s" % ( container, err)) LOG.exception(err_msg) raise RuntimeError(err_msg) # process all plan files and create or update a stack processed_data = templates.process_templates( swift, heat, container ) stack_args = processed_data.copy() stack_args['timeout_mins'] = timeout LOG.info("Performing Heat stack update") LOG.info('updating stack: %s', stack.stack_name) return heat.stacks.update(stack.id, **stack_args) def _process_params(flattened, params): for item in params: if item not in flattened['parameters']: param_obj = {} for key, value in params.get(item).items(): camel_case_key = key[0].lower() + key[1:] param_obj[camel_case_key] = value param_obj['name'] = item flattened['parameters'][item] = param_obj return list(params) def _flat_it(flattened, name, data): key = str(uuid.uuid4()) value = {} value.update({ 'name': name, 'id': key }) if 'Type' in data: value['type'] = data['Type'] if 'Description' in data: value['description'] = data['Description'] if 'Parameters' in data: value['parameters'] = _process_params(flattened, data['Parameters']) if 'ParameterGroups' in data: value['parameter_groups'] = data['ParameterGroups'] if 'NestedParameters' in data: nested = data['NestedParameters'] nested_ids = [] for nested_key in nested.keys(): nested_data = _flat_it(flattened, nested_key, nested.get(nested_key)) # nested_data will always have one key (and only one) nested_ids.append(list(nested_data)[0]) value['resources'] = nested_ids flattened['resources'][key] = value return {key: value} def validate_stack_and_flatten_parameters(heat, processed_data, env): params = env.get('parameter_defaults') fields = { 'template': processed_data['template'], 'files': processed_data['files'], 'environment': processed_data['environment'], 'show_nested': True } processed_data = { 'heat_resource_tree': heat.stacks.validate(**fields), 'environment_parameters': params, } if processed_data['heat_resource_tree']: flattened = {'resources': {}, 'parameters': {}} _flat_it(flattened, 'Root', processed_data['heat_resource_tree']) processed_data['heat_resource_tree'] = flattened return processed_data def deploy_stack(swift, heat, container, skip_deploy_identifier=False, timeout_mins=240): try: stack = heat.stacks.get(container, resolve_outputs=False) except heat_exc.HTTPNotFound: stack = None stack_is_new = stack is None # update StackAction, DeployIdentifier parameters = dict() if not skip_deploy_identifier: parameters['DeployIdentifier'] = int(time.time()) else: parameters['DeployIdentifier'] = '' parameters['StackAction'] = 'CREATE' if stack_is_new else 'UPDATE' try: env = plan_utils.get_env(swift, container) except swiftexceptions.ClientException as err: err_msg = ("Error retrieving environment for plan %s: %s" % ( container, err)) LOG.exception(err_msg) raise RuntimeError(err_msg) set_tls_parameters(parameters, env) try: plan_utils.update_in_env(swift, env, 'parameter_defaults', parameters) except swiftexceptions.ClientException as err: err_msg = ("Error updating environment for plan %s: %s" % ( container, err)) LOG.exception(err_msg) raise RuntimeError(err_msg) if not stack_is_new: try: LOG.debug('Checking for compatible neutron mechanism drivers') msg = update.check_neutron_mechanism_drivers(env, stack, swift, container) if msg: raise RuntimeError(msg) except swiftexceptions.ClientException as err: err_msg = ("Error getting template %s: %s" % ( container, err)) LOG.exception(err_msg) raise RuntimeError(err_msg) # process all plan files and create or update a stack processed_data = templates.process_templates( swift, heat, container=container, prune_services=True ) stack_args = processed_data.copy() stack_args['timeout_mins'] = timeout_mins if stack_is_new: try: swift.copy_object( "%s-swift-rings" % container, "swift-rings.tar.gz", "%s-swift-rings/%s-%d" % ( container, "swift-rings.tar.gz", time.time())) swift.delete_object( "%s-swift-rings" % container, "swift-rings.tar.gz") except swiftexceptions.ClientException: pass LOG.info("Perfoming Heat stack create") try: return heat.stacks.create(**stack_args) except heat_exc.HTTPException as err: err_msg = "Error during stack creation: %s" % (err,) LOG.exception(err_msg) raise RuntimeError(err_msg) LOG.info("Performing Heat stack update") stack_args['existing'] = 'true' try: return heat.stacks.update(stack.id, **stack_args) except heat_exc.HTTPException as err: err_msg = "Error during stack update: %s" % (err,) LOG.exception(err_msg) raise RuntimeError(err_msg) def set_tls_parameters(parameters, env, local_ca_path=constants.LOCAL_CACERT_PATH): def get_camap(): return env['parameter_defaults'].get('CAMap', {}) def get_updated_camap_entry(entry_name, cacert, orig_camap): ca_map_entry = { entry_name: { 'content': cacert } } orig_camap.update(ca_map_entry) return orig_camap cacert_string = get_local_cacert(local_ca_path) if cacert_string: parameters['CAMap'] = get_updated_camap_entry( 'undercloud-ca', cacert_string, get_camap()) def get_local_cacert(local_ca_path): # Since the undercloud has TLS by default, we'll add the undercloud's # CA to be trusted by the overcloud. try: with open(local_ca_path, 'rb') as ca_file: return ca_file.read().decode('utf-8') except IOError: # If the file wasn't found it means that the undercloud's TLS # was explicitly disabled or another CA is being used. So we'll # let the user handle this. return None except Exception: raise def preview_stack_and_network_configs(heat, processed_data, container, role_name): # stacks.preview method raises validation message if stack is # already deployed. here renaming container to get preview data. container_temp = container + "-TEMP" fields = { 'template': processed_data['template'], 'files': processed_data['files'], 'environment': processed_data['environment'], 'stack_name': container_temp, } preview_data = heat.stacks.preview(**fields) try: stack = heat.stacks.get(container_temp) except heat_exc.HTTPNotFound: msg = "Error retrieving stack: %s" % container_temp LOG.exception(msg) raise RuntimeError(msg) return get_network_config(preview_data, stack.stack_name, role_name) def get_network_config(preview_data, stack_name, role_name): result = None if preview_data: for res in preview_data.resources: net_script = process_preview_list(res, stack_name, role_name) if net_script: ns_len = len(net_script) start_index = (net_script.find( "echo '{\"network_config\"", 0, ns_len) + 6) # In file network/scripts/run-os-net-config.sh end_str = "' > /etc/os-net-config/config.json" end_index = net_script.find(end_str, start_index, ns_len) if (end_index > start_index): net_config = net_script[start_index:end_index] if net_config: result = json.loads(net_config) break if not result: err_msg = ("Unable to determine network config for role '%s'." % role_name) LOG.exception(err_msg) raise RuntimeError(err_msg) return result def process_preview_list(res, stack_name, role_name): if type(res) == list: for item in res: out = process_preview_list(item, stack_name, role_name) if out: return out elif type(res) == dict: res_stack_name = stack_name + '-' + role_name if res['resource_name'] == "OsNetConfigImpl" and \ res['resource_identity'] and \ res_stack_name in res['resource_identity']['stack_name']: return res['properties']['config'] return None