blob: ae1999efd8854468470604393985a7b8c5105222 [file] [log] [blame]
Khen Nursimuluaaac7ee2016-12-11 22:03:52 -05001#!/usr/bin/env python
2#
3# Copyright 2016 the original author or authors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18"""protoc plugin to convert a protobuf schema to a yang schema
19
20 - basic support for message, fields. enumeration, service, method
21
22 - yang semantic rules needs to be implemented
23
24 - to run this plugin :
25
26 $ python -m grpc.tools.protoc -I.
27 --plugin=protoc-gen-custom=./proto2yang.py --custom_out=. <proto file>.proto
28
29 - the above will produce a ietf-<proto file>.yang file formatted for yang
30
31 - two examples of proto that can be used in the same directory are
32 yang.proto and addressbook.proto
33
34"""
35
36import sys
37
38from jinja2 import Template
39from google.protobuf.compiler import plugin_pb2 as plugin
40from descriptor_parser import DescriptorParser
41
42from google.protobuf.descriptor import FieldDescriptor
43
44template_yang = Template("""
45module ietf-{{ module.name }} {
46
47 {% macro set_module_prefix(type) %}
48 {% for t in module.data_types %}
49 {% if t.type == type %}
50 {% if t.module != module.name %} {{ t.module }}:{{ type }};
51 {% else %} {{ type }};
52 {% endif %}
53 {% set found=True %}
54 {% endif %}
55 {% if loop.last %}
56 {% if not found %} {{ type }}; {% endif %}
57 {% endif %}
58 {% endfor %}
59 {% endmacro %}
60
61 namespace "urn:opencord:params:xml:ns:voltha:ietf-{{ module.name }}";
62 prefix {{ module.name }};
63
64 {% for imp in module.imports %}
65 import ietf-{{ imp.name }} { prefix {{ imp.name }} ; }
66 {% endfor %}
67
68 organization "CORD";
69 contact
70 " Any name";
71
72 description
73 "{{ module.description }}";
74
75 revision "2016-11-15" {
76 description "Initial revision.";
77 reference "reference";
78 }
79
80 {% for enum in module.enums %}
81 typedef {{ enum.name }} {
82 type enumeration {
83 {% for v in enum.value %}
84 enum {{ v.name }} {
85 description "{{ v.description }}";
86 }
87 {% endfor %}
88 }
89 description
90 "{{ enum.description }}";
91 }
92 {% endfor %}
93
94 {% for message in module.messages recursive %}
95 {% if message.name in module.referred_messages %}
96 grouping {{ message.name }} {
97 {% else %}
98 container {{ message.name }} {
99 {% endif %}
100 description
101 "{{ message.description }}";
102 {% for field in message.fields %}
103 {% if field.type_ref %}
104 {% for dict_item in module.referred_messages_with_keys %}
105 {% if dict_item.name == field.type %}
106 {% if not field.repeated %}
107 container {{ field.name }} {
108 {% else %}
109 list {{ field.name }} {
110 key "{{ dict_item.key }}";
111 {% if not field.repeated %}
112 max-elements 1;
113 {% endif %}
114 {% endif %}
115 uses {{ set_module_prefix(field.type) }}
116 description
117 "{{ field.description }}";
118 }
119 {% endif %}
120 {% endfor %}
121 {% elif field.repeated %}
122 list {{ field.name }} {
123 key "{{ field.name }}";
124 leaf {{ field.name }} {
125 {% if field.type == "decimal64" %}
126 type {{ field.type }} {
127 fraction-digits 5;
128 }
129 {% else %}
130 type {{ set_module_prefix(field.type) }}
131 {% endif %}
132 description
133 "{{ field.description }}";
134 }
135 description
136 "{{ field.description }}";
137 }
138 {% else %}
139 leaf {{ field.name }} {
140 {% if field.type == "decimal64" %}
141 type {{ field.type }} {
142 fraction-digits 5;
143 }
144 {% else %}
145 type {{ set_module_prefix(field.type) }}
146 {% endif %}
147 description
148 "{{ field.description }}";
149 }
150 {% endif %}
151
152 {% endfor %}
153 {% for enum_type in message.enums %}
154 typedef {{ enum_type.name }} {
155 type enumeration {
156 {% for v in enum_type.value %}
157 enum {{ v.name }} {
158 description "{{ v.description }}";
159 }
160 {% endfor %}
161 }
162 description
163 "{{ enum_type.description }}";
164 }
165
166 {% endfor %}
167 {% if message.messages %}
168 {{ loop (message.messages)|indent(4, false) }}
169 {% endif %}
170 }
171
172 {% endfor %}
173 {% for service in module.services %}
174 {% if service.description %}
175 /* {{ service.description }}" */
176 {% endif %}
177 {% for method in service.methods %}
178 rpc {{ service.service }}-{{ method.method }} {
179 description
180 "{{ method.description }}";
181 {% if method.input %}
182 input {
183 {% if method.input_ref %}
184 uses {{ set_module_prefix(method.input) }}
185 {% else %}
186 leaf {{ method.input }} {
187 type {{ set_module_prefix(method.input) }}
188 }
189 {% endif %}
190 }
191 {% endif %}
192 {% if method.output %}
193 output {
194 {% if method.output_ref %}
195 uses {{ set_module_prefix(method.output) }}
196 {% else %}
197 leaf {{ method.output }} {
198 type {{ set_module_prefix(method.output) }}
199 }
200 {% endif %}
201 }
202 {% endif %}
203 }
204
205 {% endfor %}
206
207 {% endfor %}
208}
209""", trim_blocks=True, lstrip_blocks=True)
210
211# def traverse_dependencies(descriptor):
212# dependencies = []
213# proto_imports = descriptor.get('dependency', [])
214# for proto_import in proto_imports:
215# # If the import file has a directory path to it remove it as it is not
216# # allowed in Yang. The proto extension should be removed as well
217# dependencies.append (
218# {
219# 'name' : proto_import.split('/')[-1][:-len('.proto')]
220# }
221# )
222# return dependencies
223
224
225def traverse_messages(message_types, prefix, referenced_messages):
226 messages = []
227 for message_type in message_types:
228 assert message_type['_type'] == 'google.protobuf.DescriptorProto'
229
230 # full_name = prefix + '-' + message_type['name']
231 full_name = message_type['name']
232
233 # parse the fields
234 fields = traverse_fields(message_type.get('field', []), full_name,
235 referenced_messages)
236
237 # parse the enums
238 enums = traverse_enums(message_type.get('enum_type', []), full_name)
239
240 # parse nested messages
241 nested = message_type.get('nested_type', [])
242 nested_messages = traverse_messages(nested, full_name,
243 referenced_messages)
244 messages.append(
245 {
246 'name': full_name,
247 'fields': fields,
248 'enums': enums,
249 # 'extensions': extensions,
250 'messages': nested_messages,
251 'description': remove_unsupported_characters(
252 message_type.get('_description', '')),
253 # 'extension_ranges': extension_ranges,
254 # 'oneof': oneof
255 }
256 )
257 return messages
258
259
260def traverse_fields(fields_desc, prefix, referenced_messages):
261 fields = []
262 for field in fields_desc:
263 assert field['_type'] == 'google.protobuf.FieldDescriptorProto'
264 yang_base_type = is_base_type(field['type'])
265 _type = get_yang_type(field)
266 if not yang_base_type:
267 referenced_messages.append(_type)
268 # add to referred messages also if it is an enumeration type
269 if is_enumeration(field['type']):
270 referenced_messages.append(_type)
271
272 fields.append(
273 {
274 # 'name': prefix + '-' + field.get('name', ''),
275 'name': field.get('name', ''),
276 'label': field.get('label', ''),
277 'repeated': field['label'] == FieldDescriptor.LABEL_REPEATED,
278 'number': field.get('number', ''),
279 'options': field.get('options', ''),
280 'type_name': field.get('type_name', ''),
281 'type': _type,
282 'type_ref': not yang_base_type,
283 'description': remove_unsupported_characters(field.get(
284 '_description', ''))
285 }
286 )
287 return fields
288
289
290def traverse_enums(enums_desc, prefix):
291 enums = []
292 for enum in enums_desc:
293 assert enum['_type'] == 'google.protobuf.EnumDescriptorProto'
294 # full_name = prefix + '-' + enum.get('name', '')
295 full_name = enum.get('name', '')
296 enums.append(
297 {
298 'name': full_name,
299 'value': enum.get('value', ''),
300 'description': remove_unsupported_characters(enum.get(
301 '_description', ''))
302 }
303 )
304 return enums
305
306
307def traverse_services(service_desc, referenced_messages):
308 services = []
309 for service in service_desc:
310 methods = []
311 for method in service.get('method', []):
312 assert method['_type'] == 'google.protobuf.MethodDescriptorProto'
313
314 input_name = method.get('input_type')
315 input_ref = False
316 if not is_base_type(input_name):
317 input_name = remove_first_character_if_match(input_name, '.')
318 # input_name = input_name.replace(".", "-")
319 input_name = input_name.split('.')[-1]
320 referenced_messages.append(input_name)
321 input_ref = True
322
323 output_name = method.get('output_type')
324 output_ref = False
325 if not is_base_type(output_name):
326 output_name = remove_first_character_if_match(output_name, '.')
327 # output_name = output_name.replace(".", "-")
328 output_name = output_name.split('.')[-1]
329 referenced_messages.append(output_name)
330 output_ref = True
331
332 methods.append(
333 {
334 'method': method.get('name', ''),
335 'input': input_name,
336 'input_ref': input_ref,
337 'output': output_name,
338 'output_ref': output_ref,
339 'description': remove_unsupported_characters(method.get(
340 '_description', '')),
341 'server_streaming': method.get('server_streaming',
342 False) == True
343 }
344 )
345 services.append(
346 {
347 'service': service.get('name', ''),
348 'methods': methods,
349 'description': remove_unsupported_characters(service.get(
350 '_description', '')),
351 }
352 )
353 return services
354
355
356def rchop(thestring, ending):
357 if thestring.endswith(ending):
358 return thestring[:-len(ending)]
359 return thestring
360
361
362def traverse_desc(descriptor):
363 referenced_messages = []
364 name = rchop(descriptor.get('name', ''), '.proto')
365 package = descriptor.get('package', '')
366 description = descriptor.get('_description', '')
367 # imports=traverse_dependencies(descriptor)
368 messages = traverse_messages(descriptor.get('message_type', []),
369 package, referenced_messages)
370 enums = traverse_enums(descriptor.get('enum_type', []), package)
371 services = traverse_services(descriptor.get('service', []),
372 referenced_messages)
373 # extensions = _traverse_extensions(descriptors)
374 # options = _traverse_options(descriptors)
375 # set_messages_keys(messages)
376 # unique_referred_messages_with_keys = []
377 # for message_name in list(set(referenced_messages)):
378 # unique_referred_messages_with_keys.append(
379 # {
380 # 'name': message_name,
381 # 'key': get_message_key(message_name, messages)
382 # }
383 # )
384
385 # Get a list of type definitions (messages, enums) defined in this
386 # descriptor
387 defined_types = [m['name'].split('/')[-1] for m in messages] + \
388 [e['name'].split('/')[-1] for e in enums]
389
390 data = {
391 'name': name.split('/')[-1],
392 'package': package,
393 'description': description,
394 # 'imports' : imports,
395 'messages': messages,
396 'enums': enums,
397 'services': services,
398 'defined_types' : defined_types,
399 'referenced_messages': list(set(referenced_messages)),
400 # TODO: simplify for easier jinja2 template use
401 # 'referred_messages_with_keys': unique_referred_messages_with_keys,
402 # 'extensions': extensions,
403 # 'options': options
404 }
405 return data
406
407
408def set_messages_keys(messages):
409 for message in messages:
410 message['key'] = _get_message_key(message, messages)
411 if message['messages']:
412 set_messages_keys(message['messages'])
413
414def _get_message_key(message, messages):
415 # assume key is first yang base type field
416 for field in message['fields']:
417 if not field['type_ref']:
418 return field['name']
419 else:
420 # if the field name is a message then loop for the key in that
421 # message
422 ref_message = _get_message(field['type'], messages)
423 if ref_message:
424 return _get_message_key(ref_message, messages)
425
426 # no key yet - search nested messaged
427 for m in message['messages']:
428 key = _get_message_key(m, messages)
429 if key is not None:
430 return key
431 else:
432 return None
433
434def _get_message(name, messages):
435 for m in messages:
436 if m['name'] == name:
437 return m
438 return None
439
440def get_message_key(message_name, messages):
441 for message in messages:
442 if message_name == message['name']:
443 return message['key']
444 if message['messages']:
445 return get_message_key(message_name, message['messages'])
446 return None
447
448
449def update_module_imports(module):
450 used_imports = set()
451 for ref_msg in module['referenced_messages']:
452 for type_dict in module['data_types']:
453 if ref_msg == type_dict['type']:
454 if module['name'] != type_dict['module']:
455 used_imports.add(type_dict['module'])
456 break
457 module['imports'] = [{'name' : i} for i in used_imports]
458
459
460def generate_code(request, response):
461 assert isinstance(request, plugin.CodeGeneratorRequest)
462
463 parser = DescriptorParser()
464
465 # First process the proto file with the imports
466 all_defined_types = []
467 all_proto_data = []
468 all_referred_messages = []
469 all_messages = []
470 for proto_file in request.proto_file:
471 native_data = parser.parse_file_descriptor(proto_file,
472 type_tag_name='_type',
473 fold_comments=True)
474
475 # Consolidate the defined types across imports
476 yang_data = traverse_desc(native_data)
477 for type in yang_data['defined_types']:
478 all_defined_types.append(
479 {
480 'type' : type,
481 'module' : yang_data['name']
482 }
483 )
484
485 all_proto_data.append(
486 {
487 'file_name': '{}-{}'.format('ietf', proto_file.name.split(
488 '/')[-1].replace('.proto','.yang')),
489 'module': yang_data
490 }
491 )
492
493 # Consolidate referred messages across imports
494 all_referred_messages = all_referred_messages + yang_data['referenced_messages']
495
496 # consolidate all messages
497 all_messages = all_messages + yang_data['messages']
498
499 # Set the message keys - required for List definitions (repeated label)
500 set_messages_keys(all_messages)
501 unique_referred_messages_with_keys = []
502 for m in all_messages:
503 unique_referred_messages_with_keys.append(
504 {
505 'name': m['name'],
506 'key': m['key']
507 }
508 )
509
510 # Create the files
511 for proto_data in all_proto_data:
512 f = response.file.add()
513 f.name = proto_data['file_name']
514 proto_data['module']['data_types'] = all_defined_types
515 proto_data['module']['referred_messages'] = all_referred_messages
516 proto_data['module']['referred_messages_with_keys'] = unique_referred_messages_with_keys
517 update_module_imports(proto_data['module'])
518 f.content = template_yang.render(module=proto_data['module'])
519
520
521def get_yang_type(field):
522 type = field['type']
523 if type in YANG_TYPE_MAP.keys():
524 _type, _ = YANG_TYPE_MAP[type]
525 if _type in ['enumeration', 'message', 'group']:
526 return field['type_name'].split('.')[-1]
527 # return remove_first_character_if_match(field['type_name'],
528 # '.').replace('.', '-')
529 else:
530 return _type
531 else:
532 return type
533
534def is_enumeration(type):
535 if type in YANG_TYPE_MAP.keys():
536 _type, _ = YANG_TYPE_MAP[type]
537 return _type in ['enumeration']
538 return False
539
540def is_base_type(type):
541 # check numeric value of the type first
542 if type in YANG_TYPE_MAP.keys():
543 _type, _ = YANG_TYPE_MAP[type]
544 return _type not in ['message', 'group']
545 else:
546 # proto name of the type
547 result = [_format for (_, _format) in YANG_TYPE_MAP.values() if
548 _format == type and _format not in ['message',
549 'group']]
550 return len(result) > 0
551
552
553def remove_unsupported_characters(text):
554 unsupported_characters = ["{", "}", "[", "]", "\"", "\\", "*", "/"]
555 return ''.join([i if i not in unsupported_characters else ' ' for i in
556 text])
557
558
559def remove_first_character_if_match(str, char):
560 if str.startswith(char):
561 return str[1:]
562 return str
563
564
565YANG_TYPE_MAP = {
566 FieldDescriptor.TYPE_BOOL: ('boolean', 'boolean'),
567 FieldDescriptor.TYPE_BYTES: ('binary', 'byte'),
568 FieldDescriptor.TYPE_DOUBLE: ('decimal64', 'double'),
569 FieldDescriptor.TYPE_ENUM: ('enumeration', 'enum'),
570 FieldDescriptor.TYPE_FIXED32: ('int32', 'int64'),
571 FieldDescriptor.TYPE_FIXED64: ('int64', 'uint64'),
572 FieldDescriptor.TYPE_FLOAT: ('decimal64', 'float'),
573 FieldDescriptor.TYPE_INT32: ('int32', 'int32'),
574 FieldDescriptor.TYPE_INT64: ('int64', 'int64'),
575 FieldDescriptor.TYPE_SFIXED32: ('int32', 'int32'),
576 FieldDescriptor.TYPE_SFIXED64: ('int64', 'int64'),
577 FieldDescriptor.TYPE_STRING: ('string', 'string'),
578 FieldDescriptor.TYPE_SINT32: ('int32', 'int32'),
579 FieldDescriptor.TYPE_SINT64: ('int64', 'int64'),
580 FieldDescriptor.TYPE_UINT32: ('uint32', 'int64'),
581 FieldDescriptor.TYPE_UINT64: ('uint64', 'uint64'),
582 FieldDescriptor.TYPE_MESSAGE: ('message', 'message'),
583 FieldDescriptor.TYPE_GROUP: ('group', 'group')
584}
585
586if __name__ == '__main__':
587 # Read request message from stdin
588 data = sys.stdin.read()
589
590 # Parse request
591 request = plugin.CodeGeneratorRequest()
592 request.ParseFromString(data)
593
594 # Create response
595 response = plugin.CodeGeneratorResponse()
596
597 # Generate code
598 generate_code(request, response)
599
600 # Serialise response message
601 output = response.SerializeToString()
602
603 # Write to stdout
604 sys.stdout.write(output)
605 # print is_base_type(9)