Package ganeti :: Package rapi :: Module testutils
[hide private]
[frames] | no frames]

Source Code for Module ganeti.rapi.testutils

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2012 Google Inc. 
  5  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions are 
  9  # met: 
 10  # 
 11  # 1. Redistributions of source code must retain the above copyright notice, 
 12  # this list of conditions and the following disclaimer. 
 13  # 
 14  # 2. Redistributions in binary form must reproduce the above copyright 
 15  # notice, this list of conditions and the following disclaimer in the 
 16  # documentation and/or other materials provided with the distribution. 
 17  # 
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 29   
 30   
 31  """Remote API test utilities. 
 32   
 33  """ 
 34   
 35  import logging 
 36  import re 
 37  import base64 
 38  import pycurl 
 39  from cStringIO import StringIO 
 40   
 41  from ganeti import errors 
 42  from ganeti import opcodes 
 43  from ganeti import http 
 44  from ganeti import server 
 45  from ganeti import utils 
 46  from ganeti import compat 
 47  from ganeti import luxi 
 48  from ganeti import rapi 
 49   
 50  import ganeti.http.server # pylint: disable=W0611 
 51  import ganeti.server.rapi 
 52  import ganeti.rapi.client 
 53   
 54   
 55  _URI_RE = re.compile(r"https://(?P<host>.*):(?P<port>\d+)(?P<path>/.*)") 
56 57 58 -class VerificationError(Exception):
59 """Dedicated error class for test utilities. 60 61 This class is used to hide all of Ganeti's internal exception, so that 62 external users of these utilities don't have to integrate Ganeti's exception 63 hierarchy. 64 65 """
66
67 68 -def _GetOpById(op_id):
69 """Tries to get an opcode class based on its C{OP_ID}. 70 71 """ 72 try: 73 return opcodes.OP_MAPPING[op_id] 74 except KeyError: 75 raise VerificationError("Unknown opcode ID '%s'" % op_id)
76
77 78 -def _HideInternalErrors(fn):
79 """Hides Ganeti-internal exceptions, see L{VerificationError}. 80 81 """ 82 def wrapper(*args, **kwargs): 83 try: 84 return fn(*args, **kwargs) 85 except (errors.GenericError, rapi.client.GanetiApiError), err: 86 raise VerificationError("Unhandled Ganeti error: %s" % err)
87 88 return wrapper 89
90 91 @_HideInternalErrors 92 -def VerifyOpInput(op_id, data):
93 """Verifies opcode parameters according to their definition. 94 95 @type op_id: string 96 @param op_id: Opcode ID (C{OP_ID} attribute), e.g. C{OP_CLUSTER_VERIFY} 97 @type data: dict 98 @param data: Opcode parameter values 99 @raise VerificationError: Parameter verification failed 100 101 """ 102 op_cls = _GetOpById(op_id) 103 104 try: 105 op = op_cls(**data) # pylint: disable=W0142 106 except TypeError, err: 107 raise VerificationError("Unable to create opcode instance: %s" % err) 108 109 try: 110 op.Validate(False) 111 except errors.OpPrereqError, err: 112 raise VerificationError("Parameter validation for opcode '%s' failed: %s" % 113 (op_id, err))
114
115 116 @_HideInternalErrors 117 -def VerifyOpResult(op_id, result):
118 """Verifies opcode results used in tests (e.g. in a mock). 119 120 @type op_id: string 121 @param op_id: Opcode ID (C{OP_ID} attribute), e.g. C{OP_CLUSTER_VERIFY} 122 @param result: Mocked opcode result 123 @raise VerificationError: Return value verification failed 124 125 """ 126 resultcheck_fn = _GetOpById(op_id).OP_RESULT 127 128 if not resultcheck_fn: 129 logging.warning("Opcode '%s' has no result type definition", op_id) 130 elif not resultcheck_fn(result): 131 raise VerificationError("Given result does not match result description" 132 " for opcode '%s': %s" % (op_id, resultcheck_fn))
133
134 135 -def _GetPathFromUri(uri):
136 """Gets the path and query from a URI. 137 138 """ 139 match = _URI_RE.match(uri) 140 if match: 141 return match.groupdict()["path"] 142 else: 143 return None
144
145 146 -def _FormatHeaders(headers):
147 """Formats HTTP headers. 148 149 @type headers: sequence of strings 150 @rtype: string 151 152 """ 153 assert compat.all(": " in header for header in headers) 154 return "\n".join(headers)
155
156 157 -class FakeCurl(object):
158 """Fake cURL object. 159 160 """
161 - def __init__(self, handler):
162 """Initialize this class 163 164 @param handler: Request handler instance 165 166 """ 167 self._handler = handler 168 self._opts = {} 169 self._info = {}
170
171 - def setopt(self, opt, value):
172 self._opts[opt] = value
173
174 - def getopt(self, opt):
175 return self._opts.get(opt)
176
177 - def unsetopt(self, opt):
178 self._opts.pop(opt, None)
179
180 - def getinfo(self, info):
181 return self._info[info]
182
183 - def perform(self):
184 method = self._opts[pycurl.CUSTOMREQUEST] 185 url = self._opts[pycurl.URL] 186 request_body = self._opts[pycurl.POSTFIELDS] 187 writefn = self._opts[pycurl.WRITEFUNCTION] 188 189 if pycurl.HTTPHEADER in self._opts: 190 baseheaders = _FormatHeaders(self._opts[pycurl.HTTPHEADER]) 191 else: 192 baseheaders = "" 193 194 headers = http.ParseHeaders(StringIO(baseheaders)) 195 196 if request_body: 197 headers[http.HTTP_CONTENT_LENGTH] = str(len(request_body)) 198 199 if self._opts.get(pycurl.HTTPAUTH, 0) & pycurl.HTTPAUTH_BASIC: 200 try: 201 userpwd = self._opts[pycurl.USERPWD] 202 except KeyError: 203 raise errors.ProgrammerError("Basic authentication requires username" 204 " and password") 205 206 headers[http.HTTP_AUTHORIZATION] = \ 207 "%s %s" % (http.auth.HTTP_BASIC_AUTH, base64.b64encode(userpwd)) 208 209 path = _GetPathFromUri(url) 210 (code, _, resp_body) = \ 211 self._handler.FetchResponse(path, method, headers, request_body) 212 213 self._info[pycurl.RESPONSE_CODE] = code 214 if resp_body is not None: 215 writefn(resp_body)
216
217 218 -class _RapiMock(object):
219 """Mocking out the RAPI server parts. 220 221 """
222 - def __init__(self, user_fn, luxi_client, reqauth=False):
223 """Initialize this class. 224 225 @type user_fn: callable 226 @param user_fn: Function to authentication username 227 @param luxi_client: A LUXI client implementation 228 229 """ 230 self.handler = \ 231 server.rapi.RemoteApiHandler(user_fn, reqauth, _client_cls=luxi_client)
232
233 - def FetchResponse(self, path, method, headers, request_body):
234 """This is a callback method used to fetch a response. 235 236 This method is called by the FakeCurl.perform method 237 238 @type path: string 239 @param path: Requested path 240 @type method: string 241 @param method: HTTP method 242 @type request_body: string 243 @param request_body: Request body 244 @type headers: mimetools.Message 245 @param headers: Request headers 246 @return: Tuple containing status code, response headers and response body 247 248 """ 249 req_msg = http.HttpMessage() 250 req_msg.start_line = \ 251 http.HttpClientToServerStartLine(method, path, http.HTTP_1_0) 252 req_msg.headers = headers 253 req_msg.body = request_body 254 255 (_, _, _, resp_msg) = \ 256 http.server.HttpResponder(self.handler)(lambda: (req_msg, None)) 257 258 return (resp_msg.start_line.code, resp_msg.headers, resp_msg.body)
259
260 261 -class _TestLuxiTransport(object):
262 """Mocked LUXI transport. 263 264 Raises L{errors.RapiTestResult} for all method calls, no matter the 265 arguments. 266 267 """
268 - def __init__(self, record_fn, address, timeouts=None): # pylint: disable=W0613
269 """Initializes this class. 270 271 """ 272 self._record_fn = record_fn
273
274 - def Close(self):
275 pass
276
277 - def Call(self, data):
278 """Calls LUXI method. 279 280 In this test class the method is not actually called, but added to a list 281 of called methods and then an exception (L{errors.RapiTestResult}) is 282 raised. There is no return value. 283 284 """ 285 (method, _, _) = luxi.ParseRequest(data) 286 287 # Take a note of called method 288 self._record_fn(method) 289 290 # Everything went fine until here, so let's abort the test 291 raise errors.RapiTestResult
292
293 294 -class _LuxiCallRecorder(object):
295 """Records all called LUXI client methods. 296 297 """
298 - def __init__(self):
299 """Initializes this class. 300 301 """ 302 self._called = set()
303
304 - def Record(self, name):
305 """Records a called function name. 306 307 """ 308 self._called.add(name)
309
310 - def CalledNames(self):
311 """Returns a list of called LUXI methods. 312 313 """ 314 return self._called
315
316 - def __call__(self, address=None):
317 """Creates an instrumented LUXI client. 318 319 The LUXI client will record all method calls (use L{CalledNames} to 320 retrieve them). 321 322 """ 323 return luxi.Client(transport=compat.partial(_TestLuxiTransport, 324 self.Record), 325 address=address)
326
327 328 -def _TestWrapper(fn, *args, **kwargs):
329 """Wrapper for ignoring L{errors.RapiTestResult}. 330 331 """ 332 try: 333 return fn(*args, **kwargs) 334 except errors.RapiTestResult: 335 # Everything was fine up to the point of sending a LUXI request 336 return NotImplemented
337
338 339 -class InputTestClient(object):
340 """Test version of RAPI client. 341 342 Instances of this class can be used to test input arguments for RAPI client 343 calls. See L{rapi.client.GanetiRapiClient} for available methods and their 344 arguments. Functions can return C{NotImplemented} if all arguments are 345 acceptable, but a LUXI request would be necessary to provide an actual return 346 value. In case of an error, L{VerificationError} is raised. 347 348 @see: An example on how to use this class can be found in 349 C{doc/examples/rapi_testutils.py} 350 351 """
352 - def __init__(self):
353 """Initializes this class. 354 355 """ 356 username = utils.GenerateSecret() 357 password = utils.GenerateSecret() 358 359 def user_fn(wanted): 360 """Called to verify user credentials given in HTTP request. 361 362 """ 363 assert username == wanted 364 return http.auth.PasswordFileUser(username, password, 365 [rapi.RAPI_ACCESS_WRITE])
366 367 self._lcr = _LuxiCallRecorder() 368 369 # Create a mock RAPI server 370 handler = _RapiMock(user_fn, self._lcr) 371 372 self._client = \ 373 rapi.client.GanetiRapiClient("master.example.com", 374 username=username, password=password, 375 curl_factory=lambda: FakeCurl(handler))
376
377 - def _GetLuxiCalls(self):
378 """Returns the names of all called LUXI client functions. 379 380 """ 381 return self._lcr.CalledNames()
382
383 - def __getattr__(self, name):
384 """Finds method by name. 385 386 The method is wrapped using L{_TestWrapper} to produce the actual test 387 result. 388 389 """ 390 return _HideInternalErrors(compat.partial(_TestWrapper, 391 getattr(self._client, name)))
392