Martin Cosyns | 0efdc87 | 2021-09-27 16:24:30 +0000 | [diff] [blame] | 1 | # 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 |
| 14 | import os |
| 15 | import grpc |
| 16 | import json |
| 17 | import getpass |
| 18 | import inspect |
| 19 | import logging |
| 20 | import logging.config |
| 21 | import tempfile |
| 22 | import textwrap |
| 23 | import importlib |
| 24 | import pkg_resources |
| 25 | |
| 26 | from .tools.protop import ProtoBufParser |
| 27 | from distutils.version import StrictVersion |
| 28 | from robot.api.deco import keyword |
| 29 | from robot.libraries.BuiltIn import BuiltIn, RobotNotRunningError |
| 30 | |
| 31 | |
| 32 | def _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 | |
| 53 | class 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 |