Merge "Nova RPC unpinning"
[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_CLOUDNAME, PARAM_ENDPOINTMAP) = ('CloudName', 'EndpointMap')
34 FIELDS = (F_PORT, F_PROTOCOL, F_HOST) = ('port', 'protocol', 'host')
35
36 ENDPOINT_TYPES = frozenset(['Internal', 'Public', 'Admin'])
37
38
39 def get_file(default_fn, override=None, writable=False):
40     if override == '-':
41         if writable:
42             return sys.stdout
43         else:
44             return sys.stdin
45
46     if override is not None:
47         filename = override
48     else:
49         filename = os.path.join(os.path.dirname(__file__), default_fn)
50
51     return open(filename, 'w' if writable else 'r')
52
53
54 def load_endpoint_data(infile=None):
55     with get_file(IN_FILE, infile) as f:
56         return yaml.safe_load(f)
57
58
59 def vip_param_name(endpoint_type_defn):
60     return endpoint_type_defn['vip_param'] + 'VirtualIP'
61
62
63 def vip_param_names(config):
64     def ep_types(svc):
65         return (v for k, v in svc.items() if k in ENDPOINT_TYPES or not k)
66
67     return set(vip_param_name(defn)
68                for svc in config.values() for defn in ep_types(svc))
69
70
71 def endpoint_map_default(config):
72     def map_item(ep_name, ep_type, svc):
73         values = collections.OrderedDict([
74             (F_PROTOCOL, svc.get(F_PROTOCOL, 'http')),
75             (F_PORT, str(svc[ep_type].get(F_PORT, svc[F_PORT]))),
76             (F_HOST, SUBST_IP_ADDRESS),
77         ])
78         return ep_name + ep_type, values
79
80     return collections.OrderedDict(map_item(ep_name, ep_type, svc)
81                                    for ep_name, svc in sorted(config.items())
82                                    for ep_type in sorted(set(svc) &
83                                                          ENDPOINT_TYPES))
84
85
86 def make_parameter(ptype, default, description=None):
87     param = collections.OrderedDict([('type', ptype), ('default', default)])
88     if description is not None:
89         param['description'] = description
90     return param
91
92
93 def template_parameters(config):
94     params = collections.OrderedDict((n, make_parameter('string', ''))
95                                      for n in sorted(vip_param_names(config)))
96
97     params[PARAM_ENDPOINTMAP] = make_parameter('json',
98                                                endpoint_map_default(config),
99                                                'Mapping of service endpoint '
100                                                '-> protocol. Typically set '
101                                                'via parameter_defaults in the '
102                                                'resource registry.')
103
104     params[PARAM_CLOUDNAME] = make_parameter('string',
105                                              'overcloud',
106                                              'The DNS name of this cloud. '
107                                              'e.g. ci-overcloud.tripleo.org')
108     return params
109
110
111 def template_output_definition(endpoint_name,
112                                endpoint_variant,
113                                endpoint_type,
114                                vip_param,
115                                uri_suffix=None,
116                                name_override=None):
117     def extract_field(field):
118         assert field in FIELDS
119         return {'get_param': ['EndpointMap',
120                               endpoint_name + endpoint_type,
121                               copy.copy(field)]}
122
123     port = extract_field(F_PORT)
124     protocol = extract_field(F_PROTOCOL)
125     host = {
126         'str_replace': collections.OrderedDict([
127             ('template', extract_field(F_HOST)),
128             ('params', {
129                 SUBST_IP_ADDRESS: {'get_param': vip_param},
130                 SUBST_CLOUDNAME: {'get_param': PARAM_CLOUDNAME},
131             })
132         ])
133     }
134     uri_fields = [protocol, '://', copy.deepcopy(host), ':', port]
135     uri_fields_suffix = (copy.deepcopy(uri_fields) +
136                          ([uri_suffix] if uri_suffix is not None else []))
137
138     name = name_override if name_override is not None else (endpoint_name +
139                                                             endpoint_variant +
140                                                             endpoint_type)
141
142     return name, {
143         'host': host,
144         'port': extract_field('port'),
145         'protocol': extract_field('protocol'),
146         'uri': {
147             'list_join': ['', uri_fields_suffix]
148         },
149         'uri_no_suffix': {
150             'list_join': ['', uri_fields]
151         },
152     }
153
154
155 def template_endpoint_items(config):
156     def get_svc_endpoints(ep_name, svc):
157         for ep_type in set(svc) & ENDPOINT_TYPES:
158             defn = svc[ep_type]
159             for variant, suffix in defn.get('uri_suffixes',
160                                             {'': None}).items():
161                 name_override = defn.get('names', {}).get(variant)
162                 yield template_output_definition(ep_name, variant, ep_type,
163                                                  vip_param_name(defn),
164                                                  suffix,
165                                                  name_override)
166
167     return itertools.chain.from_iterable(sorted(get_svc_endpoints(ep_name,
168                                                                   svc))
169                                          for (ep_name,
170                                               svc) in sorted(config.items()))
171
172
173 def generate_endpoint_map_template(config):
174     return collections.OrderedDict([
175         ('heat_template_version', '2015-04-30'),
176         ('description', 'A map of OpenStack endpoints.'),
177         ('parameters', template_parameters(config)),
178         ('outputs', {
179             'endpoint_map': {
180                 'value':
181                     collections.OrderedDict(template_endpoint_items(config))
182             }
183         }),
184     ])
185
186
187 autogen_warning = """### DO NOT MODIFY THIS FILE
188 ### This file is automatically generated from endpoint_data.yaml
189 ### by the script build_endpoint_map.py
190
191 """
192
193
194 class TemplateDumper(yaml.SafeDumper):
195     def represent_ordered_dict(self, data):
196         return self.represent_dict(data.items())
197
198
199 TemplateDumper.add_representer(collections.OrderedDict,
200                                TemplateDumper.represent_ordered_dict)
201
202
203 def write_template(template, filename=None):
204     with get_file(OUT_FILE, filename, writable=True) as f:
205         f.write(autogen_warning)
206         yaml.dump(template, f, TemplateDumper, width=68)
207
208
209 def read_template(template, filename=None):
210     with get_file(OUT_FILE, filename) as f:
211         return yaml.safe_load(f)
212
213
214 def build_endpoint_map(output_filename=None, input_filename=None):
215     if output_filename is not None and output_filename == input_filename:
216         raise Exception('Cannot read from and write to the same file')
217     config = load_endpoint_data(input_filename)
218     template = generate_endpoint_map_template(config)
219     write_template(template, output_filename)
220
221
222 def check_up_to_date(output_filename=None, input_filename=None):
223     if output_filename is not None and output_filename == input_filename:
224         raise Exception('Input and output filenames must be different')
225     config = load_endpoint_data(input_filename)
226     template = generate_endpoint_map_template(config)
227     existing_template = read_template(output_filename)
228     return existing_template == template
229
230
231 def get_options():
232     from optparse import OptionParser
233
234     parser = OptionParser('usage: %prog'
235                           ' [-i INPUT_FILE] [-o OUTPUT_FILE] [--check]',
236                           description=__doc__)
237     parser.add_option('-i', '--input', dest='input_file', action='store',
238                       default=None,
239                       help='Specify a different endpoint data file')
240     parser.add_option('-o', '--output', dest='output_file', action='store',
241                       default=None,
242                       help='Specify a different endpoint map template file')
243     parser.add_option('-c', '--check', dest='check', action='store_true',
244                       default=False, help='Check that the output file is '
245                                           'up to date with the data')
246     parser.add_option('-d', '--debug', dest='debug', action='store_true',
247                       default=False, help='Print stack traces on error')
248
249     return parser.parse_args()
250
251
252 def main():
253     options, args = get_options()
254     if args:
255         print('Warning: ignoring positional args: %s' % ' '.join(args),
256               file=sys.stderr)
257
258     try:
259         if options.check:
260             if not check_up_to_date(options.output_file, options.input_file):
261                 print('EndpointMap template does not match input data',
262                       file=sys.stderr)
263                 sys.exit(2)
264         else:
265             build_endpoint_map(options.output_file, options.input_file)
266     except Exception as exc:
267         if options.debug:
268             raise
269         print('%s: %s' % (type(exc).__name__, str(exc)), file=sys.stderr)
270         sys.exit(1)
271
272
273 if __name__ == '__main__':
274     main()