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