blob: 3a3af0fe034d027f4f02511cc0a19f0c6eada237 [file] [log] [blame]
Khen Nursimulu8ffb8932017-01-26 13:40:49 -05001#!/usr/bin/env python
2#
3# Copyright 2017 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
18import sys
19
20from google.protobuf.compiler import plugin_pb2 as plugin
21from google.protobuf.descriptor_pb2 import ServiceDescriptorProto, \
22 MethodOptions
23from jinja2 import Template
24from simplejson import dumps
Khen Nursimulu3676b7c2017-01-31 13:48:38 -050025import yang_options_pb2
Khen Nursimulu8ffb8932017-01-26 13:40:49 -050026
27from netconf.protos.third_party.google.api import annotations_pb2, http_pb2
28
29_ = annotations_pb2, http_pb2 # to keep import line from being optimized out
30
31template = Template("""
32# Generated file; please do not edit
33
34from simplejson import dumps, load
35from structlog import get_logger
36from google.protobuf.json_format import MessageToDict, ParseDict
37from twisted.internet.defer import inlineCallbacks, returnValue
38
39{% set package = file_name.replace('.proto', '') %}
40
41{% for pypackage, module in includes %}
42{% if pypackage %}
43from {{ pypackage }} import {{ module }}
44{% else %}
45import {{ module }}
46{% endif %}
47{% endfor %}
48
49log = get_logger()
50
51{% for method in methods %}
52{% set method_name = method['service'].rpartition('.')[2] + '_' + method['method'] %}
53@inlineCallbacks
54def {{ method_name }}(grpc_client, params, metadata, **kw):
55 log.info('{{ method_name }}', params=params, metadata=metadata, **kw)
56 data = params
57 data.update(kw)
58 try:
59 req = ParseDict(data, {{ type_map[method['input_type']] }}())
60 except Exception, e:
61 log.error('cannot-convert-to-protobuf', e=e, data=data)
62 raise
63 res, _ = yield grpc_client.invoke(
64 {{ type_map[method['service']] }}Stub,
65 '{{ method['method'] }}', req, metadata)
66 try:
67 out_data = grpc_client.convertToDict(res)
68 except AttributeError, e:
69 filename = '/tmp/netconf_failed_to_convert_data.pbd'
70 with file(filename, 'w') as f:
71 f.write(res.SerializeToString())
72 log.error('cannot-convert-from-protobuf', outdata_saved=filename)
73 raise
74 log.info('{{ method_name }}', **out_data)
75 returnValue(out_data)
76
Khen Nursimulu3676b7c2017-01-31 13:48:38 -050077def get_xml_tag_{{ method_name }}():
78 return '{{ method['xml_tag'] }}'
79
80def get_list_items_name_{{ method_name }}():
81 return '{{ method['list_item_name'] }}'
82
83def get_return_type_{{ method_name }}():
84 return '{{ type_map[method['output_type']] }}'
85
Khen Nursimulu8ffb8932017-01-26 13:40:49 -050086{% endfor %}
87
88""", trim_blocks=True, lstrip_blocks=True)
89
90
91def traverse_methods(proto_file):
92 package = proto_file.name
93 for service in proto_file.service:
94 assert isinstance(service, ServiceDescriptorProto)
95
96 for method in service.method:
97 input_type = method.input_type
98 if input_type.startswith('.'):
99 input_type = input_type[1:]
100
101 output_type = method.output_type
102 if output_type.startswith('.'):
103 output_type = output_type[1:]
104
Khen Nursimulu3676b7c2017-01-31 13:48:38 -0500105 # Process any specific yang option
106 xml_tag = ''
107 list_item_name = ''
108 options = method.options
109 assert isinstance(options, MethodOptions)
110 for fd, yang_tag in options.ListFields():
111 if fd.full_name == 'voltha.yang_xml_tag':
112 if yang_tag.xml_tag:
113 xml_tag = yang_tag.xml_tag
114 if yang_tag.list_items_name:
115 list_item_name = yang_tag.list_items_name
116
Khen Nursimulu8ffb8932017-01-26 13:40:49 -0500117 data = {
118 'package': package,
119 'filename': proto_file.name,
120 'service': proto_file.package + '.' + service.name,
121 'method': method.name,
122 'input_type': input_type,
Khen Nursimulu3676b7c2017-01-31 13:48:38 -0500123 'output_type': output_type,
124 'xml_tag': xml_tag,
125 'list_item_name': list_item_name
Khen Nursimulu8ffb8932017-01-26 13:40:49 -0500126 }
127
128 yield data
129
130
131def generate_gw_code(file_name, methods, type_map, includes):
132 return template.render(file_name=file_name, methods=methods,
133 type_map=type_map, includes=includes)
134
135
136class IncludeManager(object):
137 # need to keep track of what files define what message types and
138 # under what package name. Later, when we analyze the methods, we
139 # need to be able to derive the list of files we need to load and we
140 # also need to replce the <proto-package-name>.<artifact-name> in the
141 # templates with <python-package-name>.<artifact-name> so Python can
142 # resolve these.
143 def __init__(self):
144 self.package_to_localname = {}
145 self.fullname_to_filename = {}
146 self.prefix_table = [] # sorted table of top-level symbols in protos
147 self.type_map = {} # full name as used in .proto -> python name
148 self.includes_needed = set() # names of files needed to be included
149 self.filename_to_module = {} # filename -> (package, module)
150
151 def extend_symbol_tables(self, proto_file):
152 # keep track of what file adds what top-level symbol to what abstract
153 # package name
154 package_name = proto_file.package
155 file_name = proto_file.name
156 self._add_filename(file_name)
157 all_defs = list(proto_file.message_type)
158 all_defs.extend(list(proto_file.enum_type))
159 all_defs.extend(list(proto_file.service))
160 for typedef in all_defs:
161 name = typedef.name
162 fullname = package_name + '.' + name
163 self.fullname_to_filename[fullname] = file_name
164 self.package_to_localname.setdefault(package_name, []).append(name)
165 self._update_prefix_table()
166
167 def _add_filename(self, filename):
168 if filename not in self.filename_to_module:
169 python_path = filename.replace('.proto', '_pb2').replace('/', '.')
170 package_name, _, module_name = python_path.rpartition('.')
171 self.filename_to_module[filename] = (package_name, module_name)
172
173 def _update_prefix_table(self):
174 # make a sorted list symbol prefixes needed to resolv for potential use
175 # of nested symbols
176 self.prefix_table = sorted(self.fullname_to_filename.iterkeys(),
177 reverse=True)
178
179 def _find_matching_prefix(self, fullname):
180 for prefix in self.prefix_table:
181 if fullname.startswith(prefix):
182 return prefix
183 # This should never happen
184 raise Exception('No match for type name "{}"'.format(fullname))
185
186 def add_needed_symbol(self, fullname):
187 if fullname in self.type_map:
188 return
189 top_level_symbol = self._find_matching_prefix(fullname)
190 name = top_level_symbol.rpartition('.')[2]
191 nested_name = fullname[len(top_level_symbol):] # may be empty
192 file_name = self.fullname_to_filename[top_level_symbol]
193 self.includes_needed.add(file_name)
194 module_name = self.filename_to_module[file_name][1]
195 python_name = module_name + '.' + name + nested_name
196 self.type_map[fullname] = python_name
197
198 def get_type_map(self):
199 return self.type_map
200
201 def get_includes(self):
202 return sorted(
203 self.filename_to_module[fn] for fn in self.includes_needed)
204
205
206def generate_code(request, response):
207 assert isinstance(request, plugin.CodeGeneratorRequest)
208
209 include_manager = IncludeManager()
210 for proto_file in request.proto_file:
211
212 include_manager.extend_symbol_tables(proto_file)
213
214 methods = []
215
216 for data in traverse_methods(proto_file):
217 methods.append(data)
218 include_manager.add_needed_symbol(data['input_type'])
219 include_manager.add_needed_symbol(data['output_type'])
220 include_manager.add_needed_symbol(data['service'])
221
222 type_map = include_manager.get_type_map()
223 includes = include_manager.get_includes()
224
225 # as a nice side-effect, generate a json file capturing the essence
226 # of the RPC method entries
227 f = response.file.add()
228 f.name = proto_file.name + '.json'
229 f.content = dumps(dict(
230 type_rename_map=type_map,
231 includes=includes,
232 methods=methods), indent=4)
233
234 # generate the real Python code file
235 f = response.file.add()
236 assert proto_file.name.endswith('.proto')
237 f.name = proto_file.name.replace('.proto', '_rpc_gw.py')
238 f.content = generate_gw_code(proto_file.name,
239 methods, type_map, includes)
240
241
242if __name__ == '__main__':
243
244 if len(sys.argv) >= 2:
245 # read input from file, to allow troubleshooting
246 with open(sys.argv[1], 'r') as f:
247 data = f.read()
248 else:
249 # read input from stdin
250 data = sys.stdin.read()
251
252 # parse request
253 request = plugin.CodeGeneratorRequest()
254 request.ParseFromString(data)
255
256 # create response object
257 response = plugin.CodeGeneratorResponse()
258
259 # generate the output and the response
260 generate_code(request, response)
261
262 # serialize the response
263 output = response.SerializeToString()
264
265 # write response to stdout
266 sys.stdout.write(output)