blob: 2de6192daf26d70781e02cff0a6c9236f7e268d7 [file] [log] [blame]
Martin Cosyns0efdc872021-09-27 16:24:30 +00001# Copyright 2020-present Open Networking Foundation
2# Original copyright 2020-present ADTRAN, Inc.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14import os
15import grpc
16import json
17import getpass
18import inspect
19import logging
20import logging.config
21import tempfile
22import textwrap
23import importlib
24import pkg_resources
25
26from .tools.protop import ProtoBufParser
27from distutils.version import StrictVersion
28from robot.api.deco import keyword
29from robot.libraries.BuiltIn import BuiltIn, RobotNotRunningError
30
31
32def _package_version_get(package_name, source=None):
33 """
34 Returns the installed version number for the given pip package with name _package_name_.
35 """
36
37 if source:
38 head, tail = os.path.split(os.path.dirname(os.path.abspath(source)))
39
40 while tail:
41 try:
42 with open(os.path.join(head, 'VERSION')) as version_file:
43 return version_file.read().strip()
44 except Exception:
45 head, tail = os.path.split(head)
46
47 try:
48 return pkg_resources.get_distribution(package_name).version
49 except pkg_resources.DistributionNotFound:
50 raise NameError("Package '%s' is not installed!" % package_name)
51
52
53class GrpcRobot(object):
54
55 device = None
56 package_name = ''
57 installed_package = None
58
59 try:
60 ROBOT_LIBRARY_VERSION = _package_version_get('grpc_robot')
61 except NameError:
62 ROBOT_LIBRARY_VERSION = 'unknown'
63
64 ROBOT_LIBRARY_SCOPE = 'TEST_SUITE'
65 global_init = 0
66 global_timeout = 120
67 min_robot_version = 30202
68
69 connection_type = 'grpc'
70
71 def __init__(self, **kwargs):
72 super().__init__()
73
74 self._host = None
75 self._port = None
76
77 self.grpc_channel = None
78 self.timeout = 30
79 self.protobuf = None
80
81 self.keywords = {}
82
83 self.enable_logging()
84 self.logger = logging.getLogger('grpc')
85
86 self.pb_version = self.get_installed_version() or self.get_latest_pb_version()
87 self.load_services(self.pb_version)
88
89 @staticmethod
90 def enable_logging():
91
92 try:
93 log_dir = BuiltIn().replace_variables('${OUTPUT_DIR}')
94 except RobotNotRunningError:
95 log_dir = tempfile.gettempdir()
96
97 try:
98 logfile_name = os.path.join(log_dir, 'grpc_robot_%s.log' % getpass.getuser())
99 except KeyError:
100 logfile_name = os.path.join(log_dir, 'grpc_robot.log')
101
102 logging.config.dictConfig({
103 'version': 1,
104 'disable_existing_loggers': False,
105
106 'formatters': {
107 'standard': {
108 'format': '%(asctime)s %(name)s [%(levelname)s] : %(message)s'
109 },
110 },
111 'handlers': {
112 'file': {
113 'level': 'DEBUG',
114 'class': 'logging.FileHandler',
115 'mode': 'a',
116 'filename': logfile_name,
117 'formatter': 'standard'
118 },
119 },
120 'loggers': {
121 'grpc': {
122 'handlers': ['file'],
123 'level': 'DEBUG',
124 'propagate': True
125 },
126 }
127 })
128
129 @staticmethod
130 def get_modules(*modules):
131 module_list = []
132 for module in modules:
133 for name, obj in inspect.getmembers(module, predicate=lambda o: inspect.isclass(o)):
134 module_list.append(obj)
135
136 return module_list
137
138 @staticmethod
139 def get_keywords_from_modules(*modules):
140 keywords = {}
141
142 for module in modules:
143 for name, obj in inspect.getmembers(module):
144 if hasattr(obj, 'robot_name'):
145 keywords[name] = module
146
147 return keywords
148
149 def get_installed_version(self):
150 dists = [str(d).split() for d in pkg_resources.working_set if str(d).split()[0] == self.package_name]
151
152 try:
153 pb_version = dists[0][-1]
154 self.logger.info('installed package %s==%s' % (self.package_name, pb_version))
155 except IndexError:
156 self.logger.error('package for %s not installed' % self.package_name)
157 return None
158
159 if pb_version not in self.get_supported_versions():
160 self.logger.warning('installed package %s==%s not supported by library, using version %s' % (
161 self.package_name, pb_version, self.get_latest_pb_version()))
162 pb_version = None
163
164 return pb_version
165
166 def get_supported_versions(self):
167
168 path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'services', self.device)
169
170 return sorted([
171 (name.split(self.device + '_')[1]).replace('_', '.')
172 for name in os.listdir(path)
173 if os.path.isdir(os.path.join(path, name)) and name.startswith(self.device)
174 ], key=StrictVersion)
175
176 def get_latest_pb_version(self):
177 return self.get_supported_versions()[-1]
178
179 def load_services(self, pb_version):
180 pb_version = pb_version.replace('.', '_')
181 path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'services', self.device, '%s_%s' % (self.device, pb_version))
182
183 modules = importlib.import_module('grpc_robot.services.%s.%s_%s' % (self.device, self.device, pb_version))
184
185 module_list = self.get_modules(modules)
186
187 self.keywords = self.get_keywords_from_modules(*module_list)
188
189 try:
190 self.protobuf = json.loads(open(os.path.join(path, '%s.json' % self.device)).read())
191 self.logger.debug('loaded services from %s' % os.path.join(path, '%s.json' % self.device))
192 except FileNotFoundError:
193 pip_dir = os.path.join(os.path.dirname(self.installed_package.__file__), 'protos')
194 self.protobuf = ProtoBufParser(self.device, self.pb_version, pip_dir).parse_files()
195
196 @keyword
197 def get_keyword_names(self):
198 """
199 Returns the list of keyword names
200 """
201 return sorted(list(self.keywords.keys()) + [name for name in dir(self) if hasattr(getattr(self, name), 'robot_name')])
202
203 def run_keyword(self, keyword_name, args, kwargs):
204 """
205 http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#running-keywords
206
207 :param keyword_name: name of method to run
208 :param args: arguments to this method
209 :param kwargs: kwargs
210 :return: whatever the method returns
211 """
212 if keyword_name in self.keywords:
213 c = self.keywords[keyword_name](self)
214 else:
215 c = self
216
217 return getattr(c, keyword_name)(*args, **kwargs)
218
219 def get_keyword_arguments(self, keyword_name):
220 """
221 http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#getting-keyword-arguments
222
223 :param keyword_name: name of method
224 :return: list of method arguments like in urls above
225 """
226
227 if keyword_name in self.keywords:
228 a = inspect.getargspec(getattr(self.keywords[keyword_name], keyword_name))
229 else:
230 a = inspect.getargspec(getattr(self, keyword_name))
231
232 # skip "self" as first parameter -> [1:]
233 args_without_defaults = a.args[1:-len(a.defaults)] if a.defaults is not None else a.args[1:]
234
235 args_with_defaults = []
236 if a.defaults is not None:
237 args_with_defaults = zip(a.args[-len(a.defaults):], a.defaults)
238 args_with_defaults = ['%s=%s' % (x, y) for x, y in args_with_defaults]
239
240 args = args_without_defaults + args_with_defaults
241
242 if a.varargs is not None:
243 args.append('*%s' % a.varargs)
244
245 if a.keywords is not None:
246 args.append('**%s' % a.keywords)
247
248 return args
249
250 def get_keyword_documentation(self, keyword_name):
251 """
252 http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#getting-keyword-documentation
253 http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#documentation-formatting
254
255 :param keyword_name: name of method to get documentation for
256 :return: string formatted according documentation
257 """
258
259 if keyword_name == '__intro__':
260 return self.__doc__
261
262 doc_string = ''
263
264 if keyword_name in self.keywords:
265 c = self.keywords[keyword_name]
266 doc_string += textwrap.dedent(getattr(c, keyword_name).__doc__ or '') + '\n'
267 doc_string += c(self).get_documentation(keyword_name) # instanciate class and call "get_documentation"
268 return doc_string
269
270 return textwrap.dedent(getattr(self, keyword_name).__doc__ or '')
271
272 @keyword
273 def connection_open(self, host, port, **kwargs):
274 """
275 Opens a connection to the gRPC host.
276
277 *Parameters*:
278 - host: <string>|<IP address>; Name or IP address of the gRPC host.
279 - port: <number>; TCP port of the gRPC host.
280
281 *Named Parameters*:
282 - timeout: <number>; Timeout in seconds for a gRPC response. Default: 30 s
283 """
284 self._host = host
285 self._port = port
286 self.timeout = int(kwargs.get('timeout', self.timeout))
287
288 channel_options = [
289 ('grpc.keepalive_time_ms', 10000),
290 ('grpc.keepalive_timeout_ms', 5000)
291 ]
292
293 if kwargs.get('insecure', True):
294 self.grpc_channel = grpc.insecure_channel('%s:%s' % (self._host, self._port), options=channel_options)
295 else:
296 raise NotImplementedError('other than "insecure channel" not implemented')
297
298 user_pb_version = kwargs.get('pb_version') or self.pb_version
299 pb_version = user_pb_version # ToDo: or device_pb_version # get the pb version from device when available
300
301 self.load_services(pb_version)
302
303 @keyword
304 def connection_close(self):
305 """
306 Closes the connection to the gRPC host.
307 """
308 del self.grpc_channel
309 self.grpc_channel = None
310
311 def _connection_parameters_get(self):
312 return {
313 'timeout': self.timeout
314 }
315
316 @keyword
317 def connection_parameters_set(self, **kwargs):
318 """
319 Sets the gRPC channel connection parameters.
320
321 *Named Parameters*:
322 - timeout: <number>; Timeout in seconds for a gRPC response.
323
324 *Return*: Same dictionary as the keyword _Connection Parameter Get_ with the values before they got changed.
325 """
326 connection_parameters = self._connection_parameters_get()
327
328 self.timeout = int(kwargs.get('timeout', self.timeout))
329
330 return connection_parameters
331
332 @keyword
333 def connection_parameters_get(self):
334 """
335 Retrieves the connection parameters for the gRPC channel.
336
337 *Return*: A dictionary with the keys:
338 - timeout
339 """
340 return self._connection_parameters_get()
341
342 @keyword
343 def library_version_get(self):
344 """
345 Retrieve the version of the currently running library instance.
346
347 *Return*: version string consisting of three dot-separated numbers (x.y.z)
348 """
349 return self.ROBOT_LIBRARY_VERSION