990cbabc2afc1aaea6b92fd8dd8eb80ea57be31e
[apex-tripleo-heat-templates.git] / network / endpoints / build_endpoint_map.py
1 #!/usr/bin/env python
2
3 """
4 Generate the endpoint_map.yaml template from data in the endpoint_data.yaml
5 file.
6
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.
9
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
12 mismatch is detected.
13 """
14
15 from __future__ import print_function
16
17
18 __all__ = ['load_endpoint_data', 'generate_endpoint_map_template',
19            'write_template', 'build_endpoint_map', 'check_up_to_date']
20
21
22 import collections
23 import copy
24 import itertools
25 import os
26 import sys
27 import yaml
28
29
30 (IN_FILE, OUT_FILE) = ('endpoint_data.yaml', 'endpoint_map.yaml')
31
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')
37
38 ENDPOINT_TYPES = frozenset(['Internal', 'Public', 'Admin'])
39
40
41 def get_file(default_fn, override=None, writable=False):
42     if override == '-':
43         if writable:
44             return sys.stdout
45         else:
46             return sys.stdin
47
48     if override is not None:
49         filename = override
50     else:
51         filename = os.path.join(os.path.dirname(__file__), default_fn)
52
53     return open(filename, 'w' if writable else 'r')
54
55
56 def load_endpoint_data(infile=None):
57     with get_file(IN_FILE, infile) as f:
58         return yaml.safe_load(f)
59
60
61 def net_param_name(endpoint_type_defn):
62     return endpoint_type_defn['net_param'] + 'Network'
63
64
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),
71         ])
72         return ep_name + ep_type, values
73
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) &
77                                                          ENDPOINT_TYPES))
78
79
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
84     return param
85
86
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 '
96                                                'resource registry.')
97
98     params[PARAM_CLOUD_ENDPOINTS] = make_parameter(
99         'json',
100         {},
101         ('A map containing the DNS names for the different endpoints '
102          '(external, internal_api, etc.)'))
103     return params
104
105
106 def template_output_definition(endpoint_name,
107                                endpoint_variant,
108                                endpoint_type,
109                                net_param,
110                                uri_suffix=None,
111                                name_override=None):
112     def extract_field(field):
113         assert field in FIELDS
114         return {'get_param': ['EndpointMap',
115                               endpoint_name + endpoint_type,
116                               copy.copy(field)]}
117
118     port = extract_field(F_PORT)
119     protocol = extract_field(F_PROTOCOL)
120     host_nobrackets = {
121         'str_replace': collections.OrderedDict([
122             ('template', extract_field(F_HOST)),
123             ('params', {
124                 SUBST_IP_ADDRESS: {'get_param':
125                                    ['NetIpMap',
126                                     {'get_param': ['ServiceNetMap',
127                                      net_param]}]},
128                 SUBST_CLOUDNAME: {'get_param':
129                                   [PARAM_CLOUD_ENDPOINTS,
130                                    {'get_param': ['ServiceNetMap',
131                                      net_param]}]},
132             })
133         ])
134     }
135     host = {
136         'str_replace': collections.OrderedDict([
137             ('template', extract_field(F_HOST)),
138             ('params', {
139                 SUBST_IP_ADDRESS: {'get_param':
140                                    ['NetIpMap',
141                                     {'str_replace':
142                                     {'template': 'NETWORK_uri',
143                                      'params': {'NETWORK':
144                                      {'get_param': ['ServiceNetMap',
145                                                     net_param]}}}}]},
146                 SUBST_CLOUDNAME: {'get_param':
147                                   [PARAM_CLOUD_ENDPOINTS,
148                                    {'get_param': ['ServiceNetMap',
149                                      net_param]}]},
150             })
151         ])
152     }
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 []))
156
157     name = name_override if name_override is not None else (endpoint_name +
158                                                             endpoint_variant +
159                                                             endpoint_type)
160
161     return name, {
162         'host_nobrackets': host_nobrackets,
163         'host': host,
164         'port': extract_field('port'),
165         'protocol': extract_field('protocol'),
166         'uri': {
167             'list_join': ['', uri_fields_suffix]
168         },
169         'uri_no_suffix': {
170             'list_join': ['', uri_fields]
171         },
172     }
173
174
175 def template_endpoint_items(config):
176     def get_svc_endpoints(ep_name, svc):
177         for ep_type in set(svc) & ENDPOINT_TYPES:
178             defn = svc[ep_type]
179             for variant, suffix in defn.get('uri_suffixes',
180                                             {'': None}).items():
181                 name_override = defn.get('names', {}).get(variant)
182                 yield template_output_definition(ep_name, variant, ep_type,
183                                                  net_param_name(defn),
184                                                  suffix,
185                                                  name_override)
186     return itertools.chain.from_iterable(sorted(get_svc_endpoints(ep_name,
187                                                                   svc))
188                                          for (ep_name,
189                                               svc) in sorted(config.items()))
190
191
192 def generate_endpoint_map_template(config):
193     return collections.OrderedDict([
194         ('heat_template_version', 'ocata'),
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)),
200         ('outputs', {
201             'endpoint_map': {
202                 'value':
203                     collections.OrderedDict(template_endpoint_items(config))
204             }
205         }),
206     ])
207
208
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
212
213 """
214
215
216 class TemplateDumper(yaml.SafeDumper):
217     def represent_ordered_dict(self, data):
218         return self.represent_dict(data.items())
219
220
221 TemplateDumper.add_representer(collections.OrderedDict,
222                                TemplateDumper.represent_ordered_dict)
223
224
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)
229
230
231 def read_template(template, filename=None):
232     with get_file(OUT_FILE, filename) as f:
233         return yaml.safe_load(f)
234
235
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)
242
243
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
251
252
253 def get_options():
254     from optparse import OptionParser
255
256     parser = OptionParser('usage: %prog'
257                           ' [-i INPUT_FILE] [-o OUTPUT_FILE] [--check]',
258                           description=__doc__)
259     parser.add_option('-i', '--input', dest='input_file', action='store',
260                       default=None,
261                       help='Specify a different endpoint data file')
262     parser.add_option('-o', '--output', dest='output_file', action='store',
263                       default=None,
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')
270
271     return parser.parse_args()
272
273
274 def main():
275     options, args = get_options()
276     if args:
277         print('Warning: ignoring positional args: %s' % ' '.join(args),
278               file=sys.stderr)
279
280     try:
281         if options.check:
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)
286                 sys.exit(2)
287         else:
288             build_endpoint_map(options.output_file, options.input_file)
289     except Exception as exc:
290         if options.debug:
291             raise
292         print('%s: %s' % (type(exc).__name__, str(exc)), file=sys.stderr)
293         sys.exit(1)
294
295
296 if __name__ == '__main__':
297     main()