Files
tripleo-common/tripleo_common/utils/config.py
James Slagle 60f25d2bcd Don't set deployments as hostvars
This patch updates config-download to not create a vars file under
group_vars for the server deployments. These variables end up as
hostvars in Ansible, which has caused many escaping problems since
hostvars are run through the templating engine.

In combination with I06c9b3256567cc57d599bd4d9af617c718d2314f, these
escaping problems should be eliminated.

Additionally, by not using hostvars (set_fact), we can save memory since
a hostvar is set on a host and persisted in memory. With many hosts and
large deployments (such as all hiera data), this would have eventually
been an issue.

Instead, an Ansible lookup is used in deployments.yaml to read
deployment data, which are now written to individual files by
config-download. This also improves readability instead of having one
big file of all deployment data.

Change-Id: Ie09fc64cf85eb532f31684f1c9808b942672ff41
Closes-Bug: #1760990
2018-04-09 15:46:03 -04:00

342 lines
14 KiB
Python

# Copyright 2016 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 os
import re
import shutil
import six
import tempfile
import warnings
import yaml
import jinja2
from tripleo_common import constants
warnings.filterwarnings('once')
class Config(object):
def __init__(self, orchestration_client):
self.log = logging.getLogger(__name__ + ".Config")
self.client = orchestration_client
self.stack_outputs = {}
def get_server_names(self):
servers = {}
role_node_id_map = self.stack_outputs.get('ServerIdData', {})
role_net_hostname_map = self.stack_outputs.get(
'RoleNetHostnameMap', {})
for role, hostnames in role_net_hostname_map.items():
if hostnames:
names = hostnames.get(constants.HOST_NETWORK) or []
shortnames = [n.split(".%s." % constants.HOST_NETWORK)[0]
for n in names]
for idx, name in enumerate(shortnames):
if 'server_ids' in role_node_id_map:
server_id = role_node_id_map['server_ids'][role][idx]
servers[server_id] = name
return servers
def get_deployment_data(self, stack,
nested_depth=constants.NESTED_DEPTH):
deployments = self.client.resources.list(
stack,
nested_depth=nested_depth,
filters=dict(name=constants.TRIPLEO_DEPLOYMENT_RESOURCE),
with_detail=True)
# Sort by creation time
deployments = sorted(deployments, key=lambda d: d.creation_time)
return deployments
def get_role_from_server_id(self, stack, server_id):
server_id_data = self.stack_outputs.get('ServerIdData', {}
).get('server_ids', {})
for k, v in server_id_data.items():
if server_id in v:
return k
def get_config_dict(self, deployment):
if '/' in deployment.attributes['value']['deployment']:
deployment_stack_id = \
deployment.attributes['value']['deployment'].split('/')[-1]
deployment_resource_id = self.client.resources.get(
deployment_stack_id,
'TripleOSoftwareDeployment').physical_resource_id
else:
deployment_resource_id = \
deployment.attributes['value']['deployment']
deployment_rsrc = self.client.software_deployments.get(
deployment_resource_id)
config = self.client.software_configs.get(
deployment_rsrc.config_id)
return config.to_dict()
def get_jinja_env(self, tmp_path):
templates_path = os.path.join(
os.path.dirname(__file__), '..', 'templates')
self._mkdir(os.path.join(tmp_path, 'templates'))
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(templates_path))
env.trim_blocks = True
return env, templates_path
def get_role_config(self):
role_config = self.stack_outputs.get('RoleConfig', {})
# RoleConfig can exist as a stack output but have a value of None
return role_config or {}
@staticmethod
def _open_file(path):
return os.fdopen(os.open(path,
os.O_WRONLY | os.O_CREAT, 0o600),
'w')
def _write_playbook_get_tasks(self, tasks, role, filepath):
playbook = []
def get_key(task):
whenexpr = task.get('when', None)
if whenexpr is None:
return ''
if not isinstance(whenexpr, list):
whenexpr = [whenexpr]
for w in whenexpr:
# make \|int optional incase forgotten; use only step digit:
match = re.search('step(\|int)? == ([0-9]+)', "%s" % w)
if match:
matches = len(match.groups())
return match.group(matches)
return ''
sorted_tasks = sorted(tasks, key=get_key)
playbook.append({'name': '%s playbook' % role,
'hosts': role,
'tasks': sorted_tasks})
with self._open_file(filepath) as conf_file:
yaml.safe_dump(playbook, conf_file, default_flow_style=False)
return sorted_tasks
def _mkdir(self, dirname):
if not os.path.exists(dirname):
try:
os.makedirs(dirname, 0o700)
except OSError as e:
message = 'Failed to create: %s, error: %s' % (dirname,
str(e))
raise OSError(message)
def download_config(self, name, config_dir, config_type=None):
# Get the stack object
stack = self.client.stacks.get(name)
self.stack_outputs = {i['output_key']: i['output_value']
for i in stack.outputs}
# Create config directory
self._mkdir(config_dir)
tmp_path = tempfile.mkdtemp(prefix='tripleo-',
suffix='-config',
dir=config_dir)
self.log.info("Generating configuration under the directory: "
"%s" % tmp_path)
# Get role data:
role_data = self.stack_outputs.get('RoleData', {})
for role_name, role in six.iteritems(role_data):
role_path = os.path.join(tmp_path, role_name)
self._mkdir(role_path)
for config in config_type or role.keys():
if config in constants.EXTERNAL_TASKS:
# external tasks are collected globally, not per-role
continue
elif config == 'step_config':
filepath = os.path.join(role_path, 'step_config.pp')
with self._open_file(filepath) as step_config:
step_config.write(role[config])
else:
if 'upgrade_tasks' in config:
filepath = os.path.join(role_path, '%s_playbook.yaml' %
config)
data = self._write_playbook_get_tasks(
role[config], role_name, filepath)
else:
try:
data = role[config]
except KeyError as e:
message = 'Invalid key: %s, error: %s' % (config,
str(e))
raise KeyError(message)
filepath = os.path.join(role_path, '%s.yaml' % config)
with self._open_file(filepath) as conf_file:
yaml.safe_dump(data,
conf_file,
default_flow_style=False)
role_config = self.get_role_config()
for config_name, config in six.iteritems(role_config):
conf_path = os.path.join(tmp_path, config_name + ".yaml")
with self._open_file(conf_path) as conf_file:
if isinstance(config, list) or isinstance(config, dict):
yaml.safe_dump(config, conf_file, default_flow_style=False)
else:
conf_file.write(config)
# Get deployment data
self.log.info("Getting deployment data from Heat...")
deployments_data = self.get_deployment_data(name)
# server_deployments is a dict of server name to a list of deployments
# (dicts) associated with that server
server_deployments = {}
# server_names is a dict of server id to server_name for easier lookup
server_names = self.get_server_names()
server_ids = dict([(v, k) for (k, v) in server_names.items()])
# role_deployment_names is a dict of role names to deployment names for
# that role. The deployment names are further separated in their own
# dict with keys of pre_deployment/post_deployment.
role_deployment_names = {}
# server_roles is a dict of server name to server role for easier
# lookup
server_roles = {}
for deployment in deployments_data:
server_id = deployment.attributes['value']['server']
config_dict = self.get_config_dict(deployment)
# deployment_name should be set via the name property on the
# Deployment resources in the templates, however, if it's None
# or empty string, default to the name of the parent_resource.
deployment_name = deployment.attributes['value'].get(
'name') or deployment.parent_resource
if not deployment_name:
message = "The deployment name cannot be determined. It " \
"should be set via the name property on the " \
"Deployment resources in the templates."
raise ValueError(message)
config_dict['deployment_name'] = deployment_name
# reset deploy_server_id to the actual server_id since we have to
# use a dummy server resource to create the deployment in the
# templates
deploy_server_id_input = \
[i for i in config_dict['inputs']
if i['name'] == 'deploy_server_id'].pop()
deploy_server_id_input['value'] = server_id
server_deployments.setdefault(
server_names[server_id],
[]).append(config_dict)
role = self.get_role_from_server_id(stack, server_id)
role_deployments = role_deployment_names.setdefault(role, {})
role_pre_deployments = role_deployments.setdefault(
'pre_deployments', [])
role_post_deployments = role_deployments.setdefault(
'post_deployments', [])
server_roles[server_names[server_id]] = role
# special handling of deployments that are run post the deploy
# steps. We have to look these up based on the
# physical_resource_id, but these names should be consistent since
# they are consistent interfaces in our templates.
if 'ExtraConfigPost' in deployment.physical_resource_id or \
'PostConfig' in deployment.physical_resource_id:
if deployment_name not in role_post_deployments:
role_post_deployments.append(deployment_name)
else:
if deployment_name not in role_pre_deployments:
role_pre_deployments.append(deployment_name)
env, templates_path = self.get_jinja_env(tmp_path)
templates_dest = os.path.join(tmp_path, 'templates')
self._mkdir(templates_dest)
shutil.copyfile(os.path.join(templates_path, 'heat-config.j2'),
os.path.join(templates_dest, 'heat-config.j2'))
group_vars_dir = os.path.join(tmp_path, 'group_vars')
self._mkdir(group_vars_dir)
for server, deployments in server_deployments.items():
deployment_template = env.get_template('deployment.j2')
for d in deployments:
server_deployment_dir = os.path.join(
tmp_path, server_roles[server], server)
self._mkdir(server_deployment_dir)
deployment_path = os.path.join(
server_deployment_dir, d['deployment_name'])
# See if the config can be loaded as a JSON data structure
# In some cases, it may already be JSON (hiera), or it may just
# be a string (script). In those cases, just use the value
# as-is.
try:
data = json.loads(d['config'])
except Exception:
data = d['config']
# If the value is not a string already, pretty print it as a
# string so it's rendered in a readable format.
if not (isinstance(data, six.text_type) or
isinstance(data, six.string_types)):
data = json.dumps(data, indent=2)
d['config'] = data
# The hiera Heat hook expects an actual dict for the config
# value, not a scalar. All other hooks expect a scalar.
if d['group'] == 'hiera':
d['scalar'] = False
else:
d['scalar'] = True
if d['group'] == 'os-apply-config':
message = ("group:os-apply-config is deprecated. "
"Deployment %s will not be applied by "
"config-download." % d['deployment_name'])
warnings.warn(message, DeprecationWarning)
with open(deployment_path, 'w') as f:
f.write(deployment_template.render(
deployment=d,
server_id=server_ids[server]))
for role, deployments in role_deployment_names.items():
group_var_role_path = os.path.join(group_vars_dir, role)
group_var_role_template = env.get_template('group_var_role.j2')
with open(group_var_role_path, 'w') as f:
f.write(group_var_role_template.render(
role=role,
pre_deployments=deployments['pre_deployments'],
post_deployments=deployments['post_deployments']))
for role_name, role in six.iteritems(role_data):
role_path = os.path.join(tmp_path, role_name)
shutil.copyfile(
os.path.join(templates_path, 'deployments.yaml'),
os.path.join(role_path, 'deployments.yaml'))
self.log.info("The TripleO configuration has been successfully "
"generated into: %s" % tmp_path)
return tmp_path