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