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 req_reader = type('TestReader', (object, ), {'sock': None})() 256 257 (_, _, _, resp_msg) = \ 258 http.server.HttpResponder(self.handler)(lambda: (req_msg, req_reader)) 259 260 return (resp_msg.start_line.code, resp_msg.headers, resp_msg.body)
261
262 263 -class _TestLuxiTransport(object):
264 """Mocked LUXI transport. 265 266 Raises L{errors.RapiTestResult} for all method calls, no matter the 267 arguments. 268 269 """
270 - def __init__(self, record_fn, address, timeouts=None, # pylint: disable=W0613 271 allow_non_master=None): # pylint: disable=W0613
272 """Initializes this class. 273 274 """ 275 self._record_fn = record_fn
276
277 - def Close(self):
278 pass
279
280 - def Call(self, data):
281 """Calls LUXI method. 282 283 In this test class the method is not actually called, but added to a list 284 of called methods and then an exception (L{errors.RapiTestResult}) is 285 raised. There is no return value. 286 287 """ 288 (method, _, _) = rpccl.ParseRequest(data) 289 290 # Take a note of called method 291 self._record_fn(method) 292 293 # Everything went fine until here, so let's abort the test 294 raise errors.RapiTestResult
295
296 297 -class _LuxiCallRecorder(object):
298 """Records all called LUXI client methods. 299 300 """
301 - def __init__(self):
302 """Initializes this class. 303 304 """ 305 self._called = set()
306
307 - def Record(self, name):
308 """Records a called function name. 309 310 """ 311 self._called.add(name)
312
313 - def CalledNames(self):
314 """Returns a list of called LUXI methods. 315 316 """ 317 return self._called
318
319 - def __call__(self, address=None):
320 """Creates an instrumented LUXI client. 321 322 The LUXI client will record all method calls (use L{CalledNames} to 323 retrieve them). 324 325 """ 326 return luxi.Client(transport=compat.partial(_TestLuxiTransport, 327 self.Record), 328 address=address)
329
330 331 -def _TestWrapper(fn, *args, **kwargs):
332 """Wrapper for ignoring L{errors.RapiTestResult}. 333 334 """ 335 try: 336 return fn(*args, **kwargs) 337 except errors.RapiTestResult: 338 # Everything was fine up to the point of sending a LUXI request 339 return NotImplemented
340
341 342 -class InputTestClient(object):
343 """Test version of RAPI client. 344 345 Instances of this class can be used to test input arguments for RAPI client 346 calls. See L{rapi.client.GanetiRapiClient} for available methods and their 347 arguments. Functions can return C{NotImplemented} if all arguments are 348 acceptable, but a LUXI request would be necessary to provide an actual return 349 value. In case of an error, L{VerificationError} is raised. 350 351 @see: An example on how to use this class can be found in 352 C{doc/examples/rapi_testutils.py} 353 354 """
355 - def __init__(self):
356 """Initializes this class. 357 358 """ 359 username = utils.GenerateSecret() 360 password = utils.GenerateSecret() 361 362 def user_fn(wanted): 363 """Called to verify user credentials given in HTTP request. 364 365 """ 366 assert username == wanted 367 return http.auth.PasswordFileUser(username, password, 368 [rapi.RAPI_ACCESS_WRITE])
369 370 self._lcr = _LuxiCallRecorder() 371 372 # Create a mock RAPI server 373 handler = _RapiMock(user_fn, self._lcr) 374 375 self._client = \ 376 rapi.client.GanetiRapiClient("master.example.com", 377 username=username, password=password, 378 curl_factory=lambda: FakeCurl(handler))
379
380 - def _GetLuxiCalls(self):
381 """Returns the names of all called LUXI client functions. 382 383 """ 384 return self._lcr.CalledNames()
385
386 - def __getattr__(self, name):
387 """Finds method by name. 388 389 The method is wrapped using L{_TestWrapper} to produce the actual test 390 result. 391 392 """ 393 return _HideInternalErrors(compat.partial(_TestWrapper, 394 getattr(self._client, name)))
395