blob: 6a2a3e76f14af5027c3e44d59e4c783c650166de [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
211def 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
269 fields.append(
270 {
271 # 'name': prefix + '-' + field.get('name', ''),
272 'name': field.get('name', ''),
273 'label': field.get('label', ''),
274 'repeated': field['label'] == FieldDescriptor.LABEL_REPEATED,
275 'number': field.get('number', ''),
276 'options': field.get('options', ''),
277 'type_name': field.get('type_name', ''),
278 'type': _type,
279 'type_ref': not yang_base_type,
280 'description': remove_unsupported_characters(field.get(
281 '_description', ''))
282 }
283 )
284 return fields
285
286
287def traverse_enums(enums_desc, prefix):
288 enums = []
289 for enum in enums_desc:
290 assert enum['_type'] == 'google.protobuf.EnumDescriptorProto'
291 # full_name = prefix + '-' + enum.get('name', '')
292 full_name = enum.get('name', '')
293 enums.append(
294 {
295 'name': full_name,
296 'value': enum.get('value', ''),
297 'description': remove_unsupported_characters(enum.get(
298 '_description', ''))
299 }
300 )
301 return enums
302
303
304def traverse_services(service_desc, referenced_messages):
305 services = []
306 for service in service_desc:
307 methods = []
308 for method in service.get('method', []):
309 assert method['_type'] == 'google.protobuf.MethodDescriptorProto'
310
311 input_name = method.get('input_type')
312 input_ref = False
313 if not is_base_type(input_name):
314 input_name = remove_first_character_if_match(input_name, '.')
315 # input_name = input_name.replace(".", "-")
316 input_name = input_name.split('.')[-1]
317 referenced_messages.append(input_name)
318 input_ref = True
319
320 output_name = method.get('output_type')
321 output_ref = False
322 if not is_base_type(output_name):
323 output_name = remove_first_character_if_match(output_name, '.')
324 # output_name = output_name.replace(".", "-")
325 output_name = output_name.split('.')[-1]
326 referenced_messages.append(output_name)
327 output_ref = True
328
329 methods.append(
330 {
331 'method': method.get('name', ''),
332 'input': input_name,
333 'input_ref': input_ref,
334 'output': output_name,
335 'output_ref': output_ref,
336 'description': remove_unsupported_characters(method.get(
337 '_description', '')),
338 'server_streaming': method.get('server_streaming',
339 False) == True
340 }
341 )
342 services.append(
343 {
344 'service': service.get('name', ''),
345 'methods': methods,
346 'description': remove_unsupported_characters(service.get(
347 '_description', '')),
348 }
349 )
350 return services
351
352
353def rchop(thestring, ending):
354 if thestring.endswith(ending):
355 return thestring[:-len(ending)]
356 return thestring
357
358
359def traverse_desc(descriptor):
360 referenced_messages = []
361 name = rchop(descriptor.get('name', ''), '.proto')
362 package = descriptor.get('package', '')
363 description = descriptor.get('_description', '')
364 # imports=traverse_dependencies(descriptor)
365 messages = traverse_messages(descriptor.get('message_type', []),
366 package, referenced_messages)
367 enums = traverse_enums(descriptor.get('enum_type', []), package)
368 services = traverse_services(descriptor.get('service', []),
369 referenced_messages)
370 # extensions = _traverse_extensions(descriptors)
371 # options = _traverse_options(descriptors)
372 # set_messages_keys(messages)
373 # unique_referred_messages_with_keys = []
374 # for message_name in list(set(referenced_messages)):
375 # unique_referred_messages_with_keys.append(
376 # {
377 # 'name': message_name,
378 # 'key': get_message_key(message_name, messages)
379 # }
380 # )
381
382 # Get a list of type definitions (messages, enums) defined in this
383 # descriptor
384 defined_types = [m['name'].split('/')[-1] for m in messages] + \
385 [e['name'].split('/')[-1] for e in enums]
386
387 data = {
388 'name': name.split('/')[-1],
389 'package': package,
390 'description': description,
391 # 'imports' : imports,
392 'messages': messages,
393 'enums': enums,
394 'services': services,
395 'defined_types' : defined_types,
396 'referenced_messages': list(set(referenced_messages)),
397 # TODO: simplify for easier jinja2 template use
398 # 'referred_messages_with_keys': unique_referred_messages_with_keys,
399 # 'extensions': extensions,
400 # 'options': options
401 }
402 return data
403
404
405def set_messages_keys(messages):
406 for message in messages:
407 message['key'] = _get_message_key(message, messages)
408 if message['messages']:
409 set_messages_keys(message['messages'])
410
411def _get_message_key(message, messages):
412 # assume key is first yang base type field
413 for field in message['fields']:
414 if not field['type_ref']:
415 return field['name']
416 else:
417 # if the field name is a message then loop for the key in that
418 # message
419 ref_message = _get_message(field['type'], messages)
420 if ref_message:
421 return _get_message_key(ref_message, messages)
422
423 # no key yet - search nested messaged
424 for m in message['messages']:
425 key = _get_message_key(m, messages)
426 if key is not None:
427 return key
428 else:
429 return None
430
431def _get_message(name, messages):
432 for m in messages:
433 if m['name'] == name:
434 return m
435 return None
436
437def get_message_key(message_name, messages):
438 for message in messages:
439 if message_name == message['name']:
440 return message['key']
441 if message['messages']:
442 return get_message_key(message_name, message['messages'])
443 return None
444
445
446def update_module_imports(module):
447 used_imports = []
448 for ref_msg in module['referenced_messages']:
449 for type_dict in module['data_types']:
450 if ref_msg == type_dict['type']:
451 if module['name'] != type_dict['module']:
452 print '{}++++{}'.format(module['name'], type_dict[
453 'module'])
454 used_imports.append(
455 {
456 'name' : type_dict['module']
457 }
458 )
459 break
460 module['imports'] = used_imports
461
462def generate_code(request, response):
463 assert isinstance(request, plugin.CodeGeneratorRequest)
464
465 parser = DescriptorParser()
466
467 # First process the proto file with the imports
468 all_defined_types = []
469 all_proto_data = []
470 all_referred_messages = []
471 all_messages = []
472 for proto_file in request.proto_file:
473 native_data = parser.parse_file_descriptor(proto_file,
474 type_tag_name='_type',
475 fold_comments=True)
476
477 # Consolidate the defined types across imports
478 yang_data = traverse_desc(native_data)
479 for type in yang_data['defined_types']:
480 all_defined_types.append(
481 {
482 'type' : type,
483 'module' : yang_data['name']
484 }
485 )
486
487 all_proto_data.append(
488 {
489 'file_name': '{}-{}'.format('ietf', proto_file.name.split(
490 '/')[-1].replace('.proto','.yang')),
491 'module': yang_data
492 }
493 )
494
495 # Consolidate referred messages across imports
496 all_referred_messages = all_referred_messages + yang_data['referenced_messages']
497
498 # consolidate all messages
499 all_messages = all_messages + yang_data['messages']
500
501 # Set the message keys - required for List definitions (repeated label)
502 set_messages_keys(all_messages)
503 unique_referred_messages_with_keys = []
504 for m in all_messages:
505 unique_referred_messages_with_keys.append(
506 {
507 'name': m['name'],
508 'key': m['key']
509 }
510 )
511
512 # Create the files
513 for proto_data in all_proto_data:
514 f = response.file.add()
515 f.name = proto_data['file_name']
516 proto_data['module']['data_types'] = all_defined_types
517 proto_data['module']['referred_messages'] = all_referred_messages
518 proto_data['module']['referred_messages_with_keys'] = unique_referred_messages_with_keys
519 update_module_imports(proto_data['module'])
520 f.content = template_yang.render(module=proto_data['module'])
521
522
523def get_yang_type(field):
524 type = field['type']
525 if type in YANG_TYPE_MAP.keys():
526 _type, _ = YANG_TYPE_MAP[type]
527 if _type in ['enumeration', 'message', 'group']:
528 return field['type_name'].split('.')[-1]
529 # return remove_first_character_if_match(field['type_name'],
530 # '.').replace('.', '-')
531 else:
532 return _type
533 else:
534 return type
535
536
537def is_base_type(type):
538 # check numeric value of the type first
539 if type in YANG_TYPE_MAP.keys():
540 _type, _ = YANG_TYPE_MAP[type]
541 return _type not in ['message', 'group']
542 else:
543 # proto name of the type
544 result = [_format for (_, _format) in YANG_TYPE_MAP.values() if
545 _format == type and _format not in ['message', 'group']]
546 return len(result) > 0
547
548
549def remove_unsupported_characters(text):
550 unsupported_characters = ["{", "}", "[", "]", "\"", "\\", "*", "/"]
551 return ''.join([i if i not in unsupported_characters else ' ' for i in
552 text])
553
554
555def remove_first_character_if_match(str, char):
556 if str.startswith(char):
557 return str[1:]
558 return str
559
560
561YANG_TYPE_MAP = {
562 FieldDescriptor.TYPE_BOOL: ('boolean', 'boolean'),
563 FieldDescriptor.TYPE_BYTES: ('binary', 'byte'),
564 FieldDescriptor.TYPE_DOUBLE: ('decimal64', 'double'),
565 FieldDescriptor.TYPE_ENUM: ('enumeration', 'enum'),
566 FieldDescriptor.TYPE_FIXED32: ('int32', 'int64'),
567 FieldDescriptor.TYPE_FIXED64: ('int64', 'uint64'),
568 FieldDescriptor.TYPE_FLOAT: ('decimal64', 'float'),
569 FieldDescriptor.TYPE_INT32: ('int32', 'int32'),
570 FieldDescriptor.TYPE_INT64: ('int64', 'int64'),
571 FieldDescriptor.TYPE_SFIXED32: ('int32', 'int32'),
572 FieldDescriptor.TYPE_SFIXED64: ('int64', 'int64'),
573 FieldDescriptor.TYPE_STRING: ('string', 'string'),
574 FieldDescriptor.TYPE_SINT32: ('int32', 'int32'),
575 FieldDescriptor.TYPE_SINT64: ('int64', 'int64'),
576 FieldDescriptor.TYPE_UINT32: ('uint32', 'int64'),
577 FieldDescriptor.TYPE_UINT64: ('uint64', 'uint64'),
578 FieldDescriptor.TYPE_MESSAGE: ('message', 'message'),
579 FieldDescriptor.TYPE_GROUP: ('group', 'group')
580}
581
582if __name__ == '__main__':
583 # Read request message from stdin
584 data = sys.stdin.read()
585
586 # Parse request
587 request = plugin.CodeGeneratorRequest()
588 request.ParseFromString(data)
589
590 # Create response
591 response = plugin.CodeGeneratorResponse()
592
593 # Generate code
594 generate_code(request, response)
595
596 # Serialise response message
597 output = response.SerializeToString()
598
599 # Write to stdout
600 sys.stdout.write(output)
601 # print is_base_type(9)