4 Generate the endpoint_map.yaml template from data in the endpoint_data.yaml
7 By default the files in the same directory as this script are operated on, but
8 different files can be optionally specified on the command line.
10 The --check option verifies that the current output file is up-to-date with the
11 latest data in the input file. The script exits with status code 2 if a
15 from __future__ import print_function
18 __all__ = ['load_endpoint_data', 'generate_endpoint_map_template',
19 'write_template', 'build_endpoint_map', 'check_up_to_date']
30 (IN_FILE, OUT_FILE) = ('endpoint_data.yaml', 'endpoint_map.yaml')
32 SUBST = (SUBST_IP_ADDRESS, SUBST_CLOUDNAME) = ('IP_ADDRESS', 'CLOUDNAME')
33 PARAMS = (PARAM_CLOUD_ENDPOINTS, PARAM_ENDPOINTMAP, PARAM_NETIPMAP,
34 PARAM_SERVICENETMAP) = (
35 'CloudEndpoints', 'EndpointMap', 'NetIpMap', 'ServiceNetMap')
36 FIELDS = (F_PORT, F_PROTOCOL, F_HOST) = ('port', 'protocol', 'host')
38 ENDPOINT_TYPES = frozenset(['Internal', 'Public', 'Admin'])
41 def get_file(default_fn, override=None, writable=False):
48 if override is not None:
51 filename = os.path.join(os.path.dirname(__file__), default_fn)
53 return open(filename, 'w' if writable else 'r')
56 def load_endpoint_data(infile=None):
57 with get_file(IN_FILE, infile) as f:
58 return yaml.safe_load(f)
61 def net_param_name(endpoint_type_defn):
62 return endpoint_type_defn['net_param'] + 'Network'
65 def endpoint_map_default(config):
66 def map_item(ep_name, ep_type, svc):
67 values = collections.OrderedDict([
68 (F_PROTOCOL, svc.get(F_PROTOCOL, 'http')),
69 (F_PORT, str(svc[ep_type].get(F_PORT, svc[F_PORT]))),
70 (F_HOST, SUBST_IP_ADDRESS),
72 return ep_name + ep_type, values
74 return collections.OrderedDict(map_item(ep_name, ep_type, svc)
75 for ep_name, svc in sorted(config.items())
76 for ep_type in sorted(set(svc) &
80 def make_parameter(ptype, default, description=None):
81 param = collections.OrderedDict([('type', ptype), ('default', default)])
82 if description is not None:
83 param['description'] = description
87 def template_parameters(config):
88 params = collections.OrderedDict()
89 params[PARAM_NETIPMAP] = make_parameter('json', {}, 'The Net IP map')
90 params[PARAM_SERVICENETMAP] = make_parameter('json', {}, 'The Service Net map')
91 params[PARAM_ENDPOINTMAP] = make_parameter('json',
92 endpoint_map_default(config),
93 'Mapping of service endpoint '
94 '-> protocol. Typically set '
95 'via parameter_defaults in the '
98 params[PARAM_CLOUD_ENDPOINTS] = make_parameter(
101 ('A map containing the DNS names for the different endpoints '
102 '(external, internal_api, etc.)'))
106 def template_output_definition(endpoint_name,
112 def extract_field(field):
113 assert field in FIELDS
114 return {'get_param': ['EndpointMap',
115 endpoint_name + endpoint_type,
118 port = extract_field(F_PORT)
119 protocol = extract_field(F_PROTOCOL)
121 'str_replace': collections.OrderedDict([
122 ('template', extract_field(F_HOST)),
124 SUBST_IP_ADDRESS: {'get_param':
126 {'get_param': ['ServiceNetMap',
128 SUBST_CLOUDNAME: {'get_param':
129 [PARAM_CLOUD_ENDPOINTS,
130 {'get_param': ['ServiceNetMap',
136 'str_replace': collections.OrderedDict([
137 ('template', extract_field(F_HOST)),
139 SUBST_IP_ADDRESS: {'get_param':
142 {'template': 'NETWORK_uri',
143 'params': {'NETWORK':
144 {'get_param': ['ServiceNetMap',
146 SUBST_CLOUDNAME: {'get_param':
147 [PARAM_CLOUD_ENDPOINTS,
148 {'get_param': ['ServiceNetMap',
153 uri_fields = [protocol, '://', copy.deepcopy(host), ':', port]
154 uri_fields_suffix = (copy.deepcopy(uri_fields) +
155 ([uri_suffix] if uri_suffix is not None else []))
157 name = name_override if name_override is not None else (endpoint_name +
162 'host_nobrackets': host_nobrackets,
164 'port': extract_field('port'),
165 'protocol': extract_field('protocol'),
167 'list_join': ['', uri_fields_suffix]
170 'list_join': ['', uri_fields]
175 def template_endpoint_items(config):
176 def get_svc_endpoints(ep_name, svc):
177 for ep_type in set(svc) & ENDPOINT_TYPES:
179 for variant, suffix in defn.get('uri_suffixes',
181 name_override = defn.get('names', {}).get(variant)
182 yield template_output_definition(ep_name, variant, ep_type,
183 net_param_name(defn),
186 return itertools.chain.from_iterable(sorted(get_svc_endpoints(ep_name,
189 svc) in sorted(config.items()))
192 def generate_endpoint_map_template(config):
193 return collections.OrderedDict([
194 ('heat_template_version', 'pike'),
195 ('description', 'A map of OpenStack endpoints. Since the endpoints '
196 'are URLs, we need to have brackets around IPv6 IP addresses. The '
197 'inputs to these parameters come from net_ip_uri_map, which will '
198 'include these brackets in IPv6 addresses.'),
199 ('parameters', template_parameters(config)),
203 collections.OrderedDict(template_endpoint_items(config))
209 autogen_warning = """### DO NOT MODIFY THIS FILE
210 ### This file is automatically generated from endpoint_data.yaml
211 ### by the script build_endpoint_map.py
216 class TemplateDumper(yaml.SafeDumper):
217 def represent_ordered_dict(self, data):
218 return self.represent_dict(data.items())
221 TemplateDumper.add_representer(collections.OrderedDict,
222 TemplateDumper.represent_ordered_dict)
225 def write_template(template, filename=None):
226 with get_file(OUT_FILE, filename, writable=True) as f:
227 f.write(autogen_warning)
228 yaml.dump(template, f, TemplateDumper, width=68)
231 def read_template(template, filename=None):
232 with get_file(OUT_FILE, filename) as f:
233 return yaml.safe_load(f)
236 def build_endpoint_map(output_filename=None, input_filename=None):
237 if output_filename is not None and output_filename == input_filename:
238 raise Exception('Cannot read from and write to the same file')
239 config = load_endpoint_data(input_filename)
240 template = generate_endpoint_map_template(config)
241 write_template(template, output_filename)
244 def check_up_to_date(output_filename=None, input_filename=None):
245 if output_filename is not None and output_filename == input_filename:
246 raise Exception('Input and output filenames must be different')
247 config = load_endpoint_data(input_filename)
248 template = generate_endpoint_map_template(config)
249 existing_template = read_template(output_filename)
250 return existing_template == template
254 from optparse import OptionParser
256 parser = OptionParser('usage: %prog'
257 ' [-i INPUT_FILE] [-o OUTPUT_FILE] [--check]',
259 parser.add_option('-i', '--input', dest='input_file', action='store',
261 help='Specify a different endpoint data file')
262 parser.add_option('-o', '--output', dest='output_file', action='store',
264 help='Specify a different endpoint map template file')
265 parser.add_option('-c', '--check', dest='check', action='store_true',
266 default=False, help='Check that the output file is '
267 'up to date with the data')
268 parser.add_option('-d', '--debug', dest='debug', action='store_true',
269 default=False, help='Print stack traces on error')
271 return parser.parse_args()
275 options, args = get_options()
277 print('Warning: ignoring positional args: %s' % ' '.join(args),
282 if not check_up_to_date(options.output_file, options.input_file):
283 print('EndpointMap template does not match input data. Please '
284 'run the build_endpoint_map.py tool to update the '
285 'template.', file=sys.stderr)
288 build_endpoint_map(options.output_file, options.input_file)
289 except Exception as exc:
292 print('%s: %s' % (type(exc).__name__, str(exc)), file=sys.stderr)
296 if __name__ == '__main__':