blob: eba8924d7c2073fce4e780421216b69f2e3b4824 [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 yang-version 1.1;
62 namespace "urn:ietf:params:xml:ns:yang:ietf-{{ module.name }}";
63 prefix {{ module.name }};
64
65 {% for imp in module.imports %}
66 import ietf-{{ imp.name }} { prefix {{ imp.name }} ; }
67 {% endfor %}
68
69 organization "CORD";
70 contact
71 " Any name";
72
73 description
74 "{{ module.description }}";
75
76 revision "2016-11-15" {
77 description "Initial revision.";
78 reference "reference";
79 }
80
81 {% for enum in module.enums %}
82 typedef {{ enum.name }} {
83 type enumeration {
84 {% for v in enum.value %}
85 enum {{ v.name }} {
86 description "{{ v.description }}";
87 }
88 {% endfor %}
89 }
90 description
91 "{{ enum.description }}";
92 }
93 {% endfor %}
94
95 {% for message in module.messages recursive %}
96 {% if message.name in module.referred_messages %}
97 grouping {{ message.name }} {
98 {% else %}
99 container {{ message.name }} {
100 {% endif %}
101 description
102 "{{ message.description }}";
103 {% for field in message.fields %}
104 {% if field.type_ref %}
105 {% for dict_item in module.referred_messages_with_keys %}
106 {% if dict_item.name == field.type %}
107 list {{ field.name }} {
108 key "{{ dict_item.key }}";
109 {% if not field.repeated %}
110 max-elements 1;
111 {% endif %}
112 uses {{ set_module_prefix(field.type) }}
113 description
114 "{{ field.description }}";
115 }
116 {% endif %}
117 {% endfor %}
118 {% elif field.repeated %}
119 list {{ field.name }} {
120 key "{{ field.name }}";
121 leaf {{ field.name }} {
122 {% if field.type == "decimal64" %}
123 type {{ field.type }} {
124 fraction-digits 5;
125 }
126 {% else %}
127 type {{ set_module_prefix(field.type) }}
128 {% endif %}
129 description
130 "{{ field.description }}";
131 }
132 description
133 "{{ field.description }}";
134 }
135 {% else %}
136 leaf {{ field.name }} {
137 {% if field.type == "decimal64" %}
138 type {{ field.type }} {
139 fraction-digits 5;
140 }
141 {% else %}
142 type {{ set_module_prefix(field.type) }}
143 {% endif %}
144 description
145 "{{ field.description }}";
146 }
147 {% endif %}
148
149 {% endfor %}
150 {% for enum_type in message.enums %}
151 typedef {{ enum_type.name }} {
152 type enumeration {
153 {% for v in enum_type.value %}
154 enum {{ v.name }} {
155 description "{{ v.description }}";
156 }
157 {% endfor %}
158 }
159 description
160 "{{ enum_type.description }}";
161 }
162
163 {% endfor %}
164 {% if message.messages %}
165 {{ loop (message.messages)|indent(4, false) }}
166 {% endif %}
167 }
168
169 {% endfor %}
170 {% for service in module.services %}
171 {% if service.description %}
172 /* {{ service.description }}" */
173 {% endif %}
174 {% for method in service.methods %}
175 rpc {{ service.service }}-{{ method.method }} {
176 description
177 "{{ method.description }}";
178 {% if method.input %}
179 input {
180 {% if method.input_ref %}
181 uses {{ set_module_prefix(method.input) }}
182 {% else %}
183 leaf {{ method.input }} {
184 type {{ set_module_prefix(method.input) }}
185 }
186 {% endif %}
187 }
188 {% endif %}
189 {% if method.output %}
190 output {
191 {% if method.output_ref %}
192 uses {{ set_module_prefix(method.output) }}
193 {% else %}
194 leaf {{ method.output }} {
195 type {{ set_module_prefix(method.output) }}
196 }
197 {% endif %}
198 }
199 {% endif %}
200 }
201
202 {% endfor %}
203
204 {% endfor %}
205}
206""", trim_blocks=True, lstrip_blocks=True)
207
208def traverse_dependencies(descriptor):
209 dependencies = []
210 proto_imports = descriptor.get('dependency', [])
211 for proto_import in proto_imports:
212 # If the import file has a directory path to it remove it as it is not
213 # allowed in Yang. The proto extension should be removed as well
214 dependencies.append (
215 {
216 'name' : proto_import.split('/')[-1][:-len('.proto')]
217 }
218 )
219 return dependencies
220
221
222def traverse_messages(message_types, prefix, referenced_messages):
223 messages = []
224 for message_type in message_types:
225 assert message_type['_type'] == 'google.protobuf.DescriptorProto'
226
227 # full_name = prefix + '-' + message_type['name']
228 full_name = message_type['name']
229
230 # parse the fields
231 fields = traverse_fields(message_type.get('field', []), full_name,
232 referenced_messages)
233
234 # parse the enums
235 enums = traverse_enums(message_type.get('enum_type', []), full_name)
236
237 # parse nested messages
238 nested = message_type.get('nested_type', [])
239 nested_messages = traverse_messages(nested, full_name,
240 referenced_messages)
241 messages.append(
242 {
243 'name': full_name,
244 'fields': fields,
245 'enums': enums,
246 # 'extensions': extensions,
247 'messages': nested_messages,
248 'description': remove_unsupported_characters(
249 message_type.get('_description', '')),
250 # 'extension_ranges': extension_ranges,
251 # 'oneof': oneof
252 }
253 )
254 return messages
255
256
257def traverse_fields(fields_desc, prefix, referenced_messages):
258 fields = []
259 for field in fields_desc:
260 assert field['_type'] == 'google.protobuf.FieldDescriptorProto'
261 yang_base_type = is_base_type(field['type'])
262 _type = get_yang_type(field)
263 if not yang_base_type:
264 referenced_messages.append(_type)
265
266 fields.append(
267 {
268 # 'name': prefix + '-' + field.get('name', ''),
269 'name': field.get('name', ''),
270 'label': field.get('label', ''),
271 'repeated': field['label'] == FieldDescriptor.LABEL_REPEATED,
272 'number': field.get('number', ''),
273 'options': field.get('options', ''),
274 'type_name': field.get('type_name', ''),
275 'type': _type,
276 'type_ref': not yang_base_type,
277 'description': remove_unsupported_characters(field.get(
278 '_description', ''))
279 }
280 )
281 return fields
282
283
284def traverse_enums(enums_desc, prefix):
285 enums = []
286 for enum in enums_desc:
287 assert enum['_type'] == 'google.protobuf.EnumDescriptorProto'
288 # full_name = prefix + '-' + enum.get('name', '')
289 full_name = enum.get('name', '')
290 enums.append(
291 {
292 'name': full_name,
293 'value': enum.get('value', ''),
294 'description': remove_unsupported_characters(enum.get(
295 '_description', ''))
296 }
297 )
298 return enums
299
300
301def traverse_services(service_desc, referenced_messages):
302 services = []
303 for service in service_desc:
304 methods = []
305 for method in service.get('method', []):
306 assert method['_type'] == 'google.protobuf.MethodDescriptorProto'
307
308 input_name = method.get('input_type')
309 input_ref = False
310 if not is_base_type(input_name):
311 input_name = remove_first_character_if_match(input_name, '.')
312 # input_name = input_name.replace(".", "-")
313 input_name = input_name.split('.')[-1]
314 referenced_messages.append(input_name)
315 input_ref = True
316
317 output_name = method.get('output_type')
318 output_ref = False
319 if not is_base_type(output_name):
320 output_name = remove_first_character_if_match(output_name, '.')
321 # output_name = output_name.replace(".", "-")
322 output_name = output_name.split('.')[-1]
323 referenced_messages.append(output_name)
324 output_ref = True
325
326 methods.append(
327 {
328 'method': method.get('name', ''),
329 'input': input_name,
330 'input_ref': input_ref,
331 'output': output_name,
332 'output_ref': output_ref,
333 'description': remove_unsupported_characters(method.get(
334 '_description', '')),
335 'server_streaming': method.get('server_streaming',
336 False) == True
337 }
338 )
339 services.append(
340 {
341 'service': service.get('name', ''),
342 'methods': methods,
343 'description': remove_unsupported_characters(service.get(
344 '_description', '')),
345 }
346 )
347 return services
348
349
350def rchop(thestring, ending):
351 if thestring.endswith(ending):
352 return thestring[:-len(ending)]
353 return thestring
354
355
356def traverse_desc(descriptor):
357 referenced_messages = []
358 name = rchop(descriptor.get('name', ''), '.proto')
359 package = descriptor.get('package', '')
360 description = descriptor.get('_description', '')
361 imports=traverse_dependencies(descriptor)
362 messages = traverse_messages(descriptor.get('message_type', []),
363 package, referenced_messages)
364 enums = traverse_enums(descriptor.get('enum_type', []), package)
365 services = traverse_services(descriptor.get('service', []),
366 referenced_messages)
367 # extensions = _traverse_extensions(descriptors)
368 # options = _traverse_options(descriptors)
369 set_messages_keys(messages)
370 unique_referred_messages_with_keys = []
371 for message_name in list(set(referenced_messages)):
372 unique_referred_messages_with_keys.append(
373 {
374 'name': message_name,
375 'key': get_message_key(message_name, messages)
376 }
377 )
378
379 # Get a list of type definitions (messages, enums) defined in this
380 # descriptor
381 defined_types = [m['name'].split('/')[-1] for m in messages] + \
382 [e['name'].split('/')[-1] for e in enums]
383
384 data = {
385 'name': name.split('/')[-1],
386 'package': package,
387 'description': description,
388 'imports' : imports,
389 'messages': messages,
390 'enums': enums,
391 'services': services,
392 'defined_types' : defined_types,
393 'referenced_messages': list(set(referenced_messages)),
394 # TODO: simplify for easier jinja2 template use
395 'referred_messages_with_keys': unique_referred_messages_with_keys,
396 # 'extensions': extensions,
397 # 'options': options
398 }
399 return data
400
401
402def set_messages_keys(messages):
403 for message in messages:
404 message['key'] = _get_message_key(message)
405 if message['messages']:
406 set_messages_keys(message['messages'])
407
408
409def _get_message_key(message):
410 # assume key is first yang base type field
411 for field in message['fields']:
412 if not field['type_ref']:
413 return field['name']
414 # no key yet - search nested messaged
415 if message['messages']:
416 return get_message_key(message['name'], message['messages'])
417 else:
418 return None
419
420
421def get_message_key(message_name, messages):
422 for message in messages:
423 if message_name == message['name']:
424 return message['key']
425 if message['messages']:
426 return get_message_key(message_name, message['messages'])
427 return None
428
429
430def generate_code(request, response):
431 assert isinstance(request, plugin.CodeGeneratorRequest)
432
433 parser = DescriptorParser()
434
435 # First process the proto file with the imports
436 all_defined_types = []
437 all_proto_data = []
438 all_referred_messages = []
439 for proto_file in request.proto_file:
440 native_data = parser.parse_file_descriptor(proto_file,
441 type_tag_name='_type',
442 fold_comments=True)
443
444 # Consolidate the defined types across imports
445 yang_data = traverse_desc(native_data)
446 for type in yang_data['defined_types']:
447 all_defined_types.append(
448 {
449 'type' : type,
450 'module' : yang_data['name']
451 }
452 )
453
454 all_proto_data.append(
455 {
456 'file_name': '{}-{}'.format('ietf', proto_file.name.split(
457 '/')[-1].replace('.proto','.yang')),
458 'module': yang_data
459 }
460 )
461
462 # Consolidate referred messages across imports
463 all_referred_messages = all_referred_messages + yang_data['referenced_messages']
464
465 # Create the files
466 for proto_data in all_proto_data:
467 f = response.file.add()
468 f.name = proto_data['file_name']
469 proto_data['module']['data_types'] = all_defined_types
470 proto_data['module']['referred_messages'] = all_referred_messages
471 f.content = template_yang.render(module=proto_data['module'])
472
473
474def get_yang_type(field):
475 type = field['type']
476 if type in YANG_TYPE_MAP.keys():
477 _type, _ = YANG_TYPE_MAP[type]
478 if _type in ['enumeration', 'message', 'group']:
479 return field['type_name'].split('.')[-1]
480 # return remove_first_character_if_match(field['type_name'],
481 # '.').replace('.', '-')
482 else:
483 return _type
484 else:
485 return type
486
487
488def is_base_type(type):
489 # check numeric value of the type first
490 if type in YANG_TYPE_MAP.keys():
491 _type, _ = YANG_TYPE_MAP[type]
492 return _type not in ['message', 'group']
493 else:
494 # proto name of the type
495 result = [_format for (_, _format) in YANG_TYPE_MAP.values() if
496 _format == type and _format not in ['message', 'group']]
497 return len(result) > 0
498
499
500def remove_unsupported_characters(text):
501 unsupported_characters = ["{", "}", "[", "]", "\"", "\\", "*", "/"]
502 return ''.join([i if i not in unsupported_characters else ' ' for i in
503 text])
504
505
506def remove_first_character_if_match(str, char):
507 if str.startswith(char):
508 return str[1:]
509 return str
510
511
512YANG_TYPE_MAP = {
513 FieldDescriptor.TYPE_BOOL: ('boolean', 'boolean'),
514 FieldDescriptor.TYPE_BYTES: ('binary', 'byte'),
515 FieldDescriptor.TYPE_DOUBLE: ('decimal64', 'double'),
516 FieldDescriptor.TYPE_ENUM: ('enumeration', 'enum'),
517 FieldDescriptor.TYPE_FIXED32: ('int32', 'int64'),
518 FieldDescriptor.TYPE_FIXED64: ('int64', 'uint64'),
519 FieldDescriptor.TYPE_FLOAT: ('decimal64', 'float'),
520 FieldDescriptor.TYPE_INT32: ('int32', 'int32'),
521 FieldDescriptor.TYPE_INT64: ('int64', 'int64'),
522 FieldDescriptor.TYPE_SFIXED32: ('int32', 'int32'),
523 FieldDescriptor.TYPE_SFIXED64: ('int64', 'int64'),
524 FieldDescriptor.TYPE_STRING: ('string', 'string'),
525 FieldDescriptor.TYPE_SINT32: ('int32', 'int32'),
526 FieldDescriptor.TYPE_SINT64: ('int64', 'int64'),
527 FieldDescriptor.TYPE_UINT32: ('uint32', 'int64'),
528 FieldDescriptor.TYPE_UINT64: ('uint64', 'uint64'),
529 FieldDescriptor.TYPE_MESSAGE: ('message', 'message'),
530 FieldDescriptor.TYPE_GROUP: ('group', 'group')
531}
532
533if __name__ == '__main__':
534 # Read request message from stdin
535 data = sys.stdin.read()
536
537 # Parse request
538 request = plugin.CodeGeneratorRequest()
539 request.ParseFromString(data)
540
541 # Create response
542 response = plugin.CodeGeneratorResponse()
543
544 # Generate code
545 generate_code(request, response)
546
547 # Serialise response message
548 output = response.SerializeToString()
549
550 # Write to stdout
551 sys.stdout.write(output)
552 # print is_base_type(9)