]> git.proxmox.com Git - ceph.git/blob - ceph/src/common/options/y2c.py
import quincy beta 17.1.0
[ceph.git] / ceph / src / common / options / y2c.py
1 #!/usr/bin/env python3
2
3 import yaml
4 import argparse
5 import math
6 import os
7 import sys
8
9 # flake8: noqa: E127
10
11 def type_to_cxx(t):
12 return f'Option::TYPE_{t.upper()}'
13
14
15 def level_to_cxx(lv):
16 return f'Option::LEVEL_{lv.upper()}'
17
18
19 def eval_str(v):
20 if v == "":
21 return v
22 v = v.strip('"').replace('"', '\\"')
23 return f'"{v}"'
24
25
26 def eval_value(v, typ):
27 try:
28 if typ == 'str':
29 return eval_str(v)
30 if typ == 'float':
31 return float(v)
32 if typ in ('uint', 'int', 'size', 'secs', 'millisecs'):
33 return int(v)
34 if typ == 'bool':
35 return 'true' if v else 'false'
36 else:
37 return f'"{v}"'
38 except ValueError:
39 times = dict(_min=60,
40 _hr=60*60,
41 _day=24*60*60,
42 _K=1 << 10,
43 _M=1 << 20,
44 _G=1 << 30,
45 _T=1 << 40)
46 for unit, m in times.items():
47 if v.endswith(unit):
48 int(v[:-len(unit)])
49 # user defined literals
50 return v
51 raise ValueError(f'unknown value: {v}')
52
53
54 def set_default(default, typ):
55 if default is None:
56 return ''
57 v = eval_value(default, typ)
58 return f'.set_default({v})\n'
59
60
61 def set_daemon_default(default, typ):
62 if default is None:
63 return ''
64 v = eval_value(default, typ)
65 return f'.set_daemon_default({v})\n'
66
67
68 def add_tags(tags):
69 if tags is None:
70 return ''
71 cxx = ''
72 for tag in tags:
73 v = eval_str(tag)
74 cxx += f'.add_tag({v})\n'
75 return cxx
76
77
78 def add_services(services):
79 if services is None:
80 return ''
81 if len(services) == 1:
82 return f'.add_service("{services[0]}")\n'
83 else:
84 param = ', '.join(f'"{s}"' for s in services)
85 return f'.add_service({{{param}}})\n'
86
87
88 def add_see_also(see_also):
89 if see_also is None:
90 return ''
91 param = ', '.join(f'"{v}"' for v in see_also)
92 return f'.add_see_also({{{param}}})\n'
93
94
95 def set_desc(desc):
96 if desc is None:
97 return ''
98 v = eval_str(desc)
99 return f'.set_description({v})\n'
100
101
102 def set_long_desc(desc):
103 if desc is None:
104 return ''
105 v = eval_str(desc)
106 return f'.set_long_description({v})\n'
107
108
109 def set_min_max(mi, ma, typ):
110 if mi is None and ma is None:
111 return ''
112 if mi is not None and ma is not None:
113 min_v = eval_value(mi, typ)
114 max_v = eval_value(ma, typ)
115 if isinstance(min_v, str) and isinstance(max_v, int):
116 return f'.set_min_max({min_v}, {max_v}ULL)\n'
117 elif isinstance(min_v, int) and isinstance(max_v, str):
118 return f'.set_min_max({min_v}ULL, {max_v})\n'
119 else:
120 return f'.set_min_max({min_v}, {max_v})\n'
121 if mi is not None:
122 min_v = eval_value(mi, typ)
123 return f'.set_min({min_v})\n'
124 raise ValueError('set_max() is not implemented')
125
126
127 def set_enum_allowed(values):
128 if values is None:
129 return ''
130 param = ', '.join(f'"{v}"' for v in values)
131 return f'.set_enum_allowed({{{param}}})\n'
132
133
134 def add_flags(flags):
135 if flags is None:
136 return ''
137 cxx = ''
138 for flag in flags:
139 cxx += f'.set_flag(Option::FLAG_{flag.upper()})\n'
140 return cxx
141
142
143 def set_validator(validator):
144 if validator is None:
145 return ''
146 validator = validator.rstrip()
147 return f'.set_validator({validator})\n'
148
149
150 def add_verbatim(verbatim):
151 if verbatim is None:
152 return ''
153 return verbatim + '\n'
154
155
156 def yaml_to_cxx(opt, indent):
157 name = opt['name']
158 typ = opt['type']
159 ctyp = type_to_cxx(typ)
160 level = level_to_cxx(opt['level'])
161 cxx = f'Option("{name}", {ctyp}, {level})\n'
162 cxx += set_desc(opt.get('desc'))
163 cxx += set_long_desc(opt.get('long_desc'))
164 cxx += set_default(opt.get('default'), typ)
165 cxx += set_daemon_default(opt.get('daemon_default'), typ)
166 cxx += set_min_max(opt.get('min'), opt.get('max'), typ)
167 cxx += set_enum_allowed(opt.get('enum_values'))
168 cxx += set_validator(opt.get('validator'))
169 cxx += add_flags(opt.get('flags'))
170 cxx += add_services(opt.get('services'))
171 cxx += add_tags(opt.get('tags'))
172 cxx += add_see_also(opt.get('see_also'))
173 verbatim = add_verbatim(opt.get('verbatim'))
174 cxx += verbatim
175 if verbatim:
176 cxx += '\n'
177 else:
178 cxx = cxx.rstrip()
179 cxx += ',\n'
180 if indent > 0:
181 indented = []
182 for line in cxx.split('\n'):
183 if line:
184 indented.append(' ' * indent + line + '\n')
185 cxx = ''.join(indented)
186 return cxx
187
188
189 def type_to_h(t):
190 if t == 'uint':
191 return 'OPT_U32'
192 return f'OPT_{t.upper()}'
193
194
195 def yaml_to_h(opt):
196 if opt.get('with_legacy', False):
197 name = opt['name']
198 typ = opt['type']
199 htyp = type_to_h(typ)
200 return f'OPTION({name}, {htyp})'
201 else:
202 return ''
203
204
205 TEMPLATE_CC = '''#include "common/options.h"
206 {headers}
207
208 std::vector<Option> get_{name}_options() {{
209 return std::vector<Option>({{
210 @body@
211 }});
212 }}
213 '''
214
215
216 # PyYAML doesn't check for duplicates even though the YAML spec says
217 # that mapping keys must be unique and that duplicates must be treated
218 # as an error. See https://github.com/yaml/pyyaml/issues/165.
219 #
220 # This workaround breaks merge keys -- in "<<: *xyz", duplicate keys
221 # from xyz mapping raise an error instead of being discarded.
222 class UniqueKeySafeLoader(yaml.SafeLoader):
223 def construct_mapping(self, node, deep=False):
224 mapping = super().construct_mapping(node, deep)
225 keys = set()
226 for key_node, _ in node.value:
227 key = self.construct_object(key_node, deep=deep)
228 if key in keys:
229 raise yaml.constructor.ConstructorError(None, None,
230 "found duplicate key",
231 key_node.start_mark)
232 keys.add(key)
233 return mapping
234
235
236 def translate(opts):
237 if opts.raw:
238 prelude, epilogue = '', ''
239 else:
240 prelude, epilogue = TEMPLATE_CC.split('@body@')
241
242 if opts.name:
243 name = opts.name
244 else:
245 name = os.path.split(opts.input)[-1]
246 name = name.rsplit('.', 1)[0]
247 name = name.replace('-', '_')
248 # noqa: E127
249 with open(opts.input) as infile, \
250 open(opts.output, 'w') as cc_file, \
251 open(opts.legacy, 'w') as h_file:
252 yml = yaml.load(infile, Loader=UniqueKeySafeLoader)
253 headers = yml.get('headers', '')
254 cc_file.write(prelude.format(name=name, headers=headers))
255 options = yml['options']
256 for option in options:
257 try:
258 cc_file.write(yaml_to_cxx(option, opts.indent) + '\n')
259 if option.get('with_legacy', False):
260 h_file.write(yaml_to_h(option) + '\n')
261 except ValueError as e:
262 print(f'failed to translate option "{name}": {e}',
263 file=sys.stderr)
264 return 1
265 cc_file.write(epilogue.replace("}}", "}"))
266
267
268 def readable_size(value, typ):
269 times = dict(T=1 << 40,
270 G=1 << 30,
271 M=1 << 20,
272 K=1 << 10)
273 if isinstance(value, str):
274 value = value.strip('"')
275 try:
276 v = int(value)
277 if v == 0:
278 return 0
279 for unit, m in times.items():
280 if v % m == 0:
281 v = int(v / m)
282 return f'{v}_{unit}'
283 return v
284 except ValueError:
285 return value
286
287
288 def readable_duration(value, typ):
289 times = dict(day=24*60*60,
290 hr=60*60,
291 min=60)
292 if isinstance(value, str):
293 value = value.strip('"')
294 try:
295 v = float(value)
296 if math.floor(v) != v:
297 return v
298 v = int(v)
299 if v == 0:
300 return 0
301 for unit, m in times.items():
302 if v % m == 0:
303 v = int(v / m)
304 return f'{v}_{unit}'
305 return v
306 except ValueError:
307 return value
308
309
310 def readable_millisecs(value, typ):
311 return int(value)
312
313
314 def readable(opts):
315 with open(opts.input) as infile, open(opts.output, 'w') as outfile:
316 yml = yaml.load(infile, Loader=UniqueKeySafeLoader)
317 options = yml['options']
318 for option in options:
319 typ = option['type']
320 if typ in ('size', 'uint'):
321 do_readable = readable_size
322 elif typ in ('float', 'int', 'secs'):
323 do_readable = readable_duration
324 elif typ == 'millisecs':
325 do_readable = readable_millisecs
326 else:
327 continue
328 for field in ['default', 'min', 'max', 'daemon_default']:
329 v = option.get(field)
330 if v is not None:
331 option[field] = do_readable(v, typ)
332 yml['options'] = options
333 yaml.dump(yml, outfile, sort_keys=False, indent=2)
334
335
336 def main():
337 parser = argparse.ArgumentParser(
338 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
339 parser.add_argument('-i', '--input', dest='input',
340 default='options.yaml',
341 help='the YAML file to be processed')
342 parser.add_argument('-o', '--output', dest='output',
343 default='options',
344 help='the path to the generated .cc file')
345 parser.add_argument('--legacy', dest='legacy',
346 default='legacy_options',
347 help='the path to the generated legacy .h file')
348 parser.add_argument('--indent', type=int,
349 default=4,
350 help='the number of spaces added before each line')
351 parser.add_argument('--name',
352 help='the name of the option group')
353 parser.add_argument('--raw', action='store_true',
354 help='output the array without the full function')
355 parser.add_argument('--op', choices=('readable', 'translate'),
356 default='translate',
357 help='operation to perform.')
358 opts = parser.parse_args(sys.argv[1:])
359 if opts.op == 'translate':
360 translate(opts)
361 else:
362 readable(opts)
363
364
365 if __name__ == '__main__':
366 main()