4 # Copyright (C) 2012 Google Inc.
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.
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.
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
22 """Remote API test utilities.
30 from cStringIO import StringIO
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
41 import ganeti.http.server # pylint: disable=W0611
42 import ganeti.server.rapi
43 import ganeti.rapi.client
46 _URI_RE = re.compile(r"https://(?P<host>.*):(?P<port>\d+)(?P<path>/.*)")
49 class VerificationError(Exception):
50 """Dedicated error class for test utilities.
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
59 def _GetOpById(op_id):
60 """Tries to get an opcode class based on its C{OP_ID}.
64 return opcodes.OP_MAPPING[op_id]
66 raise VerificationError("Unknown opcode ID '%s'" % op_id)
69 def _HideInternalErrors(fn):
70 """Hides Ganeti-internal exceptions, see L{VerificationError}.
73 def wrapper(*args, **kwargs):
75 return fn(*args, **kwargs)
76 except (errors.GenericError, rapi.client.GanetiApiError), err:
77 raise VerificationError("Unhandled Ganeti error: %s" % err)
83 def VerifyOpInput(op_id, data):
84 """Verifies opcode parameters according to their definition.
87 @param op_id: Opcode ID (C{OP_ID} attribute), e.g. C{OP_CLUSTER_VERIFY}
89 @param data: Opcode parameter values
90 @raise VerificationError: Parameter verification failed
93 op_cls = _GetOpById(op_id)
96 op = op_cls(**data) # pylint: disable=W0142
97 except TypeError, err:
98 raise VerificationError("Unable to create opcode instance: %s" % err)
102 except errors.OpPrereqError, err:
103 raise VerificationError("Parameter validation for opcode '%s' failed: %s" %
108 def VerifyOpResult(op_id, result):
109 """Verifies opcode results used in tests (e.g. in a mock).
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
117 resultcheck_fn = _GetOpById(op_id).OP_RESULT
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))
126 def _GetPathFromUri(uri):
127 """Gets the path and query from a URI.
130 match = _URI_RE.match(uri)
132 return match.groupdict()["path"]
137 def _FormatHeaders(headers):
138 """Formats HTTP headers.
140 @type headers: sequence of strings
144 assert compat.all(": " in header for header in headers)
145 return "\n".join(headers)
152 def __init__(self, handler):
153 """Initialize this class
155 @param handler: Request handler instance
158 self._handler = handler
162 def setopt(self, opt, value):
163 self._opts[opt] = value
165 def getopt(self, opt):
166 return self._opts.get(opt)
168 def unsetopt(self, opt):
169 self._opts.pop(opt, None)
171 def getinfo(self, info):
172 return self._info[info]
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]
180 if pycurl.HTTPHEADER in self._opts:
181 baseheaders = _FormatHeaders(self._opts[pycurl.HTTPHEADER])
185 headers = http.ParseHeaders(StringIO(baseheaders))
188 headers[http.HTTP_CONTENT_LENGTH] = str(len(request_body))
190 if self._opts.get(pycurl.HTTPAUTH, 0) & pycurl.HTTPAUTH_BASIC:
192 userpwd = self._opts[pycurl.USERPWD]
194 raise errors.ProgrammerError("Basic authentication requires username"
197 headers[http.HTTP_AUTHORIZATION] = \
198 "%s %s" % (http.auth.HTTP_BASIC_AUTH, base64.b64encode(userpwd))
200 path = _GetPathFromUri(url)
201 (code, _, resp_body) = \
202 self._handler.FetchResponse(path, method, headers, request_body)
204 self._info[pycurl.RESPONSE_CODE] = code
205 if resp_body is not None:
210 """Mocking out the RAPI server parts.
213 def __init__(self, user_fn, luxi_client):
214 """Initialize this class.
216 @type user_fn: callable
217 @param user_fn: Function to authentication username
218 @param luxi_client: A LUXI client implementation
222 server.rapi.RemoteApiHandler(user_fn, _client_cls=luxi_client)
224 def FetchResponse(self, path, method, headers, request_body):
225 """This is a callback method used to fetch a response.
227 This method is called by the FakeCurl.perform method
230 @param path: Requested path
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
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
246 (_, _, _, resp_msg) = \
247 http.server.HttpResponder(self.handler)(lambda: (req_msg, None))
249 return (resp_msg.start_line.code, resp_msg.headers, resp_msg.body)
252 class _TestLuxiTransport:
253 """Mocked LUXI transport.
255 Raises L{errors.RapiTestResult} for all method calls, no matter the
259 def __init__(self, record_fn, address, timeouts=None): # pylint: disable=W0613
260 """Initializes this class.
263 self._record_fn = record_fn
268 def Call(self, data):
269 """Calls LUXI method.
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.
276 (method, _, _) = luxi.ParseRequest(data)
278 # Take a note of called method
279 self._record_fn(method)
281 # Everything went fine until here, so let's abort the test
282 raise errors.RapiTestResult
285 class _LuxiCallRecorder:
286 """Records all called LUXI client methods.
290 """Initializes this class.
295 def Record(self, name):
296 """Records a called function name.
299 self._called.add(name)
301 def CalledNames(self):
302 """Returns a list of called LUXI methods.
307 def __call__(self, address=None):
308 """Creates an instrumented LUXI client.
310 The LUXI client will record all method calls (use L{CalledNames} to
314 return luxi.Client(transport=compat.partial(_TestLuxiTransport,
319 def _TestWrapper(fn, *args, **kwargs):
320 """Wrapper for ignoring L{errors.RapiTestResult}.
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
330 class InputTestClient:
331 """Test version of RAPI client.
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.
339 @see: An example on how to use this class can be found in
340 C{doc/examples/rapi_testutils.py}
344 """Initializes this class.
347 username = utils.GenerateSecret()
348 password = utils.GenerateSecret()
351 """Called to verify user credentials given in HTTP request.
354 assert username == wanted
355 return http.auth.PasswordFileUser(username, password,
356 [rapi.RAPI_ACCESS_WRITE])
358 self._lcr = _LuxiCallRecorder()
360 # Create a mock RAPI server
361 handler = _RapiMock(user_fn, self._lcr)
364 rapi.client.GanetiRapiClient("master.example.com",
365 username=username, password=password,
366 curl_factory=lambda: FakeCurl(handler))
368 def _GetLuxiCalls(self):
369 """Returns the names of all called LUXI client functions.
372 return self._lcr.CalledNames()
374 def __getattr__(self, name):
375 """Finds method by name.
377 The method is wrapped using L{_TestWrapper} to produce the actual test
381 return _HideInternalErrors(compat.partial(_TestWrapper,
382 getattr(self._client, name)))