diff options
Diffstat (limited to 'tools')
-rwxr-xr-x | tools/process-templates.py | 126 | ||||
-rwxr-xr-x | tools/releasenotes_tox.sh | 28 | ||||
-rwxr-xr-x | tools/tox_install.sh | 30 | ||||
-rwxr-xr-x | tools/yaml-nic-config-2-script.py | 219 | ||||
-rwxr-xr-x | tools/yaml-validate.py | 111 |
5 files changed, 514 insertions, 0 deletions
diff --git a/tools/process-templates.py b/tools/process-templates.py new file mode 100755 index 00000000..9a06812b --- /dev/null +++ b/tools/process-templates.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# 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 argparse +import jinja2 +import os +import six +import sys +import yaml + + +def parse_opts(argv): + parser = argparse.ArgumentParser( + description='Configure host network interfaces using a JSON' + ' config file format.') + parser.add_argument('-p', '--base_path', metavar='BASE_PATH', + help="""base path of templates to process.""", + default='.') + parser.add_argument('-r', '--roles-data', metavar='ROLES_DATA', + help="""relative path to the roles_data.yaml file.""", + default='roles_data.yaml') + parser.add_argument('--safe', + action='store_true', + help="""Enable safe mode (do not overwrite files).""", + default=False) + opts = parser.parse_args(argv[1:]) + + return opts + + +def _j2_render_to_file(j2_template, j2_data, outfile_name=None, + overwrite=True): + yaml_f = outfile_name or j2_template.replace('.j2.yaml', '.yaml') + print('rendering j2 template to file: %s' % outfile_name) + + if not overwrite and os.path.exists(outfile_name): + print('ERROR: path already exists for file: %s' % outfile_name) + sys.exit(1) + + try: + # Render the j2 template + template = jinja2.Environment().from_string(j2_template) + r_template = template.render(**j2_data) + except jinja2.exceptions.TemplateError as ex: + error_msg = ("Error rendering template %s : %s" + % (yaml_f, six.text_type(ex))) + print(error_msg) + raise Exception(error_msg) + with open(outfile_name, 'w') as out_f: + out_f.write(r_template) + + +def process_templates(template_path, role_data_path, overwrite): + + with open(role_data_path) as role_data_file: + role_data = yaml.safe_load(role_data_file) + + j2_excludes_path = os.path.join(template_path, 'j2_excludes.yaml') + with open(j2_excludes_path) as role_data_file: + j2_excludes = yaml.safe_load(role_data_file) + + role_names = [r.get('name') for r in role_data] + r_map = {} + for r in role_data: + r_map[r.get('name')] = r + excl_templates = ['%s/%s' % (template_path, e) + for e in j2_excludes.get('name')] + + if os.path.isdir(template_path): + for subdir, dirs, files in os.walk(template_path): + for f in files: + file_path = os.path.join(subdir, f) + # We do two templating passes here: + # 1. *.role.j2.yaml - we template just the role name + # and create multiple files (one per role) + # 2. *.j2.yaml - we template with all roles_data, + # and create one file common to all roles + if f.endswith('.role.j2.yaml'): + print("jinja2 rendering role template %s" % f) + with open(file_path) as j2_template: + template_data = j2_template.read() + print("jinja2 rendering roles %s" % "," + .join(role_names)) + for role in role_names: + j2_data = {'role': role} + # (dprince) For the undercloud installer we don't + # want to have heat check nova/glance API's + if r_map[role].get('disable_constraints', False): + j2_data['disable_constraints'] = True + out_f = "-".join( + [role.lower(), + os.path.basename(f).replace('.role.j2.yaml', + '.yaml')]) + out_f_path = os.path.join(subdir, out_f) + if not (out_f_path in excl_templates): + _j2_render_to_file(template_data, j2_data, + out_f_path, overwrite) + else: + print('skipping rendering of %s' % out_f_path) + elif f.endswith('.j2.yaml'): + print("jinja2 rendering normal template %s" % f) + with open(file_path) as j2_template: + template_data = j2_template.read() + j2_data = {'roles': role_data} + out_f = file_path.replace('.j2.yaml', '.yaml') + _j2_render_to_file(template_data, j2_data, out_f, + overwrite) + + else: + print('Unexpected argument %s' % template_path) + +opts = parse_opts(sys.argv) + +role_data_path = os.path.join(opts.base_path, opts.roles_data) + +process_templates(opts.base_path, role_data_path, (not opts.safe)) diff --git a/tools/releasenotes_tox.sh b/tools/releasenotes_tox.sh new file mode 100755 index 00000000..4fecfd92 --- /dev/null +++ b/tools/releasenotes_tox.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +rm -rf releasenotes/build + +sphinx-build -a -E -W \ + -d releasenotes/build/doctrees \ + -b html \ + releasenotes/source releasenotes/build/html +BUILD_RESULT=$? + +UNCOMMITTED_NOTES=$(git status --porcelain | \ + awk '$1 == "M" && $2 ~ /releasenotes\/notes/ {print $2}') + +if [ "${UNCOMMITTED_NOTES}" ] +then + cat <<EOF + +REMINDER: The following changes to release notes have not been committed: + +${UNCOMMITTED_NOTES} + +While that may be intentional, keep in mind that release notes are built from +committed changes, not the working directory. + +EOF +fi + +exit ${BUILD_RESULT} diff --git a/tools/tox_install.sh b/tools/tox_install.sh new file mode 100755 index 00000000..e61b63a8 --- /dev/null +++ b/tools/tox_install.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# Client constraint file contains this client version pin that is in conflict +# with installing the client from source. We should remove the version pin in +# the constraints file before applying it for from-source installation. + +CONSTRAINTS_FILE="$1" +shift 1 + +set -e + +# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get +# published to logs.openstack.org for easy debugging. +localfile="$VIRTUAL_ENV/log/upper-constraints.txt" + +if [[ "$CONSTRAINTS_FILE" != http* ]]; then + CONSTRAINTS_FILE="file://$CONSTRAINTS_FILE" +fi +# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep +curl "$CONSTRAINTS_FILE" --insecure --progress-bar --output "$localfile" + +pip install -c"$localfile" openstack-requirements + +# This is the main purpose of the script: Allow local installation of +# the current repo. It is listed in constraints file and thus any +# install will be constrained and we need to unconstrain it. +edit-constraints "$localfile" -- "$CLIENT_NAME" + +pip install -c"$localfile" -U "$@" +exit $? diff --git a/tools/yaml-nic-config-2-script.py b/tools/yaml-nic-config-2-script.py new file mode 100755 index 00000000..b8f07e4f --- /dev/null +++ b/tools/yaml-nic-config-2-script.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python +# 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 collections +import copy +import os +import sys +import traceback +import yaml +import six +import re + + +#convert comments into 'comments<num>: ...' YAML +def to_commented_yaml(filename): + out_str = '' + last_non_comment_spaces = '' + with open(filename, 'r') as f: + comment_count = 0 + for line in f: + char_count = 0 + spaces = '' + for char in line: + char_count += 1 + if char == ' ': + spaces+=' ' + next; + elif char == '#': + comment_count += 1 + comment = line[char_count:-1] + out_str += "%scomment%i_%i: '%s'\n" % (last_non_comment_spaces, comment_count, len(spaces), comment) + break; + else: + last_non_comment_spaces = spaces + out_str += line + + #inline comments check + m = re.match(".*:.*#(.*)", line) + if m: + comment_count += 1 + out_str += "%s inline_comment%i: '%s'\n" % (last_non_comment_spaces, comment_count, m.group(1)) + break; + + with open(filename, 'w') as f: + f.write(out_str) + + return out_str + +#convert back to normal #commented YAML +def to_normal_yaml(filename): + + with open(filename, 'r') as f: + data = f.read() + + out_str = '' + next_line_break = False + for line in data.split('\n'): + m = re.match(" +comment[0-9]+_([0-9]+): '(.*)'.*", line) #normal comments + i = re.match(" +inline_comment[0-9]+: '(.*)'.*", line) #inline comments + if m: + if next_line_break: + out_str += '\n' + next_line_break = False + for x in range(0, int(m.group(1))): + out_str += " " + out_str += "#%s\n" % m.group(2) + elif i: + out_str += " #%s\n" % i.group(1) + next_line_break = False + else: + if next_line_break: + out_str += '\n' + out_str += line + next_line_break = True + + if next_line_break: + out_str += '\n' + + with open(filename, 'w') as f: + f.write(out_str) + + return out_str + + +class description(six.text_type): + pass + +# FIXME: Some of this duplicates code from build_endpoint_map.py, we should +# refactor to share the common code +class TemplateDumper(yaml.SafeDumper): + def represent_ordered_dict(self, data): + return self.represent_dict(data.items()) + + def description_presenter(self, data): + if '\n' in data: + style = '>' + else: + style = '' + return self.represent_scalar( + yaml.resolver.BaseResolver.DEFAULT_SCALAR_TAG, data, style=style) + + +# We load mappings into OrderedDict to preserve their order +class TemplateLoader(yaml.SafeLoader): + def construct_mapping(self, node): + self.flatten_mapping(node) + return collections.OrderedDict(self.construct_pairs(node)) + + +TemplateDumper.add_representer(description, + TemplateDumper.description_presenter) + +TemplateDumper.add_representer(collections.OrderedDict, + TemplateDumper.represent_ordered_dict) + + +TemplateLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + TemplateLoader.construct_mapping) + +def write_template(template, filename=None): + with open(filename, 'w') as f: + yaml.dump(template, f, TemplateDumper, width=120, default_flow_style=False) + +def exit_usage(): + print('Usage %s <yaml file>' % sys.argv[0]) + sys.exit(1) + +def convert(filename): + print('Converting %s' % filename) + try: + tpl = yaml.load(open(filename).read(), Loader=TemplateLoader) + except Exception: + print(traceback.format_exc()) + return 0 + + # Check which path we need for run-os-net-config.sh because we have + # nic config templates in the top-level and network/config + script_paths = ['network/scripts/run-os-net-config.sh', + '../../scripts/run-os-net-config.sh'] + script_path = None + for p in script_paths: + check_path = os.path.join(os.path.dirname(filename), p) + if os.path.isfile(check_path): + print("Found %s, using %s" % (check_path, p)) + script_path = p + if script_path is None: + print("Error couldn't find run-os-net-config.sh relative to filename") + exit_usage() + + for r in six.iteritems(tpl.get('resources', {})): + if (r[1].get('type') == 'OS::Heat::StructuredConfig' and + r[1].get('properties', {}).get('group') == 'os-apply-config' and + r[1].get('properties', {}).get('config', {}).get('os_net_config')): + #print("match %s" % r[0]) + new_r = collections.OrderedDict() + new_r['type'] = 'OS::Heat::SoftwareConfig' + new_r['properties'] = collections.OrderedDict() + new_r['properties']['group'] = 'script' + old_net_config = r[1].get( + 'properties', {}).get('config', {}).get('os_net_config') + new_config = {'str_replace': collections.OrderedDict()} + new_config['str_replace']['template'] = {'get_file': script_path} + new_config['str_replace']['params'] = {'$network_config': old_net_config} + new_r['properties']['config'] = new_config + tpl['resources'][r[0]] = new_r + else: + print("No match %s" % r[0]) + return 0 + + # Preserve typical HOT template key ordering + od_result = collections.OrderedDict() + # Need to bump the HOT version so str_replace supports serializing to json + od_result['heat_template_version'] = "2016-10-14" + if tpl.get('description'): + od_result['description'] = description(tpl['description']) + od_result['parameters'] = tpl['parameters'] + od_result['resources'] = tpl['resources'] + od_result['outputs'] = tpl['outputs'] + #print('Result:') + #print('%s' % yaml.dump(od_result, Dumper=TemplateDumper, width=120, default_flow_style=False)) + #print('---') + #replace = raw_input( + #"Replace file %s? Answer y/n" % filename).lower() == 'y' + #if replace: + #print("Replace %s" % filename) + write_template(od_result, filename) + #else: + # print("NOT replacing %s" % filename) + # return 0 + return 1 + +if len(sys.argv) < 2: + exit_usage() + +path_args = sys.argv[1:] +exit_val = 0 +num_converted = 0 + +for base_path in path_args: + if os.path.isfile(base_path) and base_path.endswith('.yaml'): + to_commented_yaml(base_path) + num_converted += convert(base_path) + to_normal_yaml(base_path) + else: + print('Unexpected argument %s' % base_path) + exit_usage() +if num_converted == 0: + exit_val = 1 +sys.exit(exit_val) diff --git a/tools/yaml-validate.py b/tools/yaml-validate.py index 95c7d025..63e3ce51 100755 --- a/tools/yaml-validate.py +++ b/tools/yaml-validate.py @@ -19,12 +19,85 @@ import yaml required_params = ['EndpointMap', 'ServiceNetMap', 'DefaultPasswords'] +envs_containing_endpoint_map = ['tls-endpoints-public-dns.yaml', + 'tls-endpoints-public-ip.yaml', + 'tls-everywhere-endpoints-dns.yaml'] +ENDPOINT_MAP_FILE = 'endpoint_map.yaml' + def exit_usage(): print('Usage %s <yaml file or directory>' % sys.argv[0]) sys.exit(1) +def get_base_endpoint_map(filename): + try: + tpl = yaml.load(open(filename).read()) + return tpl['parameters']['EndpointMap']['default'] + except Exception: + print(traceback.format_exc()) + return None + + +def get_endpoint_map_from_env(filename): + try: + tpl = yaml.load(open(filename).read()) + return { + 'file': filename, + 'map': tpl['parameter_defaults']['EndpointMap'] + } + except Exception: + print(traceback.format_exc()) + return None + + +def validate_endpoint_map(base_map, env_map): + return sorted(base_map.keys()) == sorted(env_map.keys()) + + +def validate_mysql_connection(settings): + no_op = lambda *args: False + error_status = [0] + + def mysql_protocol(items): + return items == ['EndpointMap', 'MysqlInternal', 'protocol'] + + def client_bind_address(item): + return 'bind_address' in item + + def validate_mysql_uri(key, items): + # Only consider a connection if it targets mysql + if key.endswith('connection') and \ + search(items, mysql_protocol, no_op): + # Assume the "bind_address" option is one of + # the token that made up the uri + if not search(items, client_bind_address, no_op): + error_status[0] = 1 + return False + + def search(item, check_item, check_key): + if check_item(item): + return True + elif isinstance(item, list): + for i in item: + if search(i, check_item, check_key): + return True + elif isinstance(item, dict): + for k in item.keys(): + if check_key(k, item[k]): + return True + elif search(item[k], check_item, check_key): + return True + return False + + search(settings, no_op, validate_mysql_uri) + return error_status[0] + + def validate_service(filename, tpl): + if 'heat_template_version' in tpl and not str(tpl['heat_template_version']).isalpha(): + print('ERROR: heat_template_version needs to be the release alias not a date: %s' + % filename) + return 1 if 'outputs' in tpl and 'role_data' in tpl['outputs']: if 'value' not in tpl['outputs']['role_data']: print('ERROR: invalid role_data for filename: %s' @@ -41,6 +114,12 @@ def validate_service(filename, tpl): print('ERROR: service_name should match file name for service: %s.' % filename) return 1 + # if service connects to mysql, the uri should use option + # bind_address to avoid issues with VIP failover + if 'config_settings' in role_data and \ + validate_mysql_connection(role_data['config_settings']): + print('ERROR: mysql connection uri should use option bind_address') + return 1 if 'parameters' in tpl: for param in required_params: if param not in tpl['parameters']: @@ -83,6 +162,8 @@ if len(sys.argv) < 2: path_args = sys.argv[1:] exit_val = 0 failed_files = [] +base_endpoint_map = None +env_endpoint_maps = list() for base_path in path_args: if os.path.isdir(base_path): @@ -94,6 +175,12 @@ for base_path in path_args: if failed: failed_files.append(file_path) exit_val |= failed + if f == ENDPOINT_MAP_FILE: + base_endpoint_map = get_base_endpoint_map(file_path) + if f in envs_containing_endpoint_map: + env_endpoint_map = get_endpoint_map_from_env(file_path) + if env_endpoint_map: + env_endpoint_maps.append(env_endpoint_map) elif os.path.isfile(base_path) and base_path.endswith('.yaml'): failed = validate(base_path) if failed: @@ -103,6 +190,30 @@ for base_path in path_args: print('Unexpected argument %s' % base_path) exit_usage() +if base_endpoint_map and \ + len(env_endpoint_maps) == len(envs_containing_endpoint_map): + for env_endpoint_map in env_endpoint_maps: + matches = validate_endpoint_map(base_endpoint_map, + env_endpoint_map['map']) + if not matches: + print("ERROR: %s doesn't match base endpoint map" % + env_endpoint_map['file']) + failed_files.append(env_endpoint_map['file']) + exit_val |= 1 + else: + print("%s matches base endpoint map" % env_endpoint_map['file']) +else: + print("ERROR: Can't validate endpoint maps since a file is missing. " + "If you meant to delete one of these files you should update this " + "tool as well.") + if not base_endpoint_map: + failed_files.append(ENDPOINT_MAP_FILE) + if len(env_endpoint_maps) != len(envs_containing_endpoint_map): + matched_files = set(os.path.basename(matched_env_file['file']) + for matched_env_file in env_endpoint_maps) + failed_files.extend(set(envs_containing_endpoint_map) - matched_files) + exit_val |= 1 + if failed_files: print('Validation failed on:') for f in failed_files: |