Revision 2a7c3583

b/INSTALL
29 29
- `simplejson Python module <http://code.google.com/p/simplejson/>`_
30 30
- `pyparsing Python module <http://pyparsing.wikispaces.com/>`_
31 31
- `pyinotify Python module <http://trac.dbzteam.org/pyinotify/>`_
32
- `PycURL Python module <http://pycurl.sourceforge.net/>`_
32 33
- `socat <http://www.dest-unreach.org/socat/>`_
33 34

  
34 35
These programs are supplied as part of most Linux distributions, so
......
39 40

  
40 41
  $ apt-get install lvm2 ssh bridge-utils iproute iputils-arping \
41 42
                    python python-pyopenssl openssl python-pyparsing \
42
                    python-simplejson python-pyinotify socat
43
                    python-simplejson python-pyinotify python-pycurl \
44
                    socat
43 45

  
44 46
If you want to build from source, please see doc/devnotes.rst for more
45 47
dependencies.
b/daemons/ganeti-watcher
610 610
  @return: Whether RAPI is working properly
611 611

  
612 612
  """
613
  ssl_config = rapi.client.CertAuthorityVerify(constants.RAPI_CERT_FILE)
614
  rapi_client = \
615
    rapi.client.GanetiRapiClient(hostname,
616
                                 config_ssl_verification=ssl_config)
613
  curl_config = rapi.client.GenericCurlConfig(cafile=constants.RAPI_CERT_FILE)
614
  rapi_client = rapi.client.GanetiRapiClient(hostname,
615
                                             curl_config_fn=curl_config)
617 616
  try:
618 617
    master_version = rapi_client.GetVersion()
619 618
  except rapi.client.CertificateError, err:
......
646 645
  return options, args
647 646

  
648 647

  
648
@rapi.client.UsesRapiClient
649 649
def main():
650 650
  """Main function.
651 651

  
b/lib/rapi/client.py
19 19
# 02110-1301, USA.
20 20

  
21 21

  
22
"""Ganeti RAPI client."""
22
"""Ganeti RAPI client.
23

  
24
@attention: To use the RAPI client, the application B{must} call
25
            C{pycurl.global_init} during initialization and
26
            C{pycurl.global_cleanup} before exiting the process. This is very
27
            important in multi-threaded programs. See curl_global_init(3) and
28
            curl_global_cleanup(3) for details. The decorator L{UsesRapiClient}
29
            can be used.
30

  
31
"""
23 32

  
24 33
# No Ganeti-specific modules should be imported. The RAPI client is supposed to
25 34
# be standalone.
26 35

  
27
import sys
28
import httplib
29
import urllib2
30 36
import logging
31 37
import simplejson
32
import socket
33 38
import urllib
34
import OpenSSL
35
import distutils.version
39
import threading
40
import pycurl
41

  
42
try:
43
  from cStringIO import StringIO
44
except ImportError:
45
  from StringIO import StringIO
36 46

  
37 47

  
38 48
GANETI_RAPI_PORT = 5080
......
61 71
_REQ_DATA_VERSION_FIELD = "__version__"
62 72
_INST_CREATE_REQV1 = "instance-create-reqv1"
63 73

  
74
# Older pycURL versions don't have all error constants
75
try:
76
  _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
77
  _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
78
except AttributeError:
79
  _CURLE_SSL_CACERT = 60
80
  _CURLE_SSL_CACERT_BADFILE = 77
81

  
82
_CURL_SSL_CERT_ERRORS = frozenset([
83
  _CURLE_SSL_CACERT,
84
  _CURLE_SSL_CACERT_BADFILE,
85
  ])
86

  
64 87

  
65 88
class Error(Exception):
66 89
  """Base error class for this module.
......
85 108
    self.code = code
86 109

  
87 110

  
88
def FormatX509Name(x509_name):
89
  """Formats an X509 name.
90

  
91
  @type x509_name: OpenSSL.crypto.X509Name
111
def UsesRapiClient(fn):
112
  """Decorator for code using RAPI client to initialize pycURL.
92 113

  
93 114
  """
94
  try:
95
    # Only supported in pyOpenSSL 0.7 and above
96
    get_components_fn = x509_name.get_components
97
  except AttributeError:
98
    return repr(x509_name)
99
  else:
100
    return "".join("/%s=%s" % (name, value)
101
                   for name, value in get_components_fn())
102

  
103

  
104
class CertAuthorityVerify:
105
  """Certificate verificator for SSL context.
106

  
107
  Configures SSL context to verify server's certificate.
115
  def wrapper(*args, **kwargs):
116
    # curl_global_init(3) and curl_global_cleanup(3) must be called with only
117
    # one thread running. This check is just a safety measure -- it doesn't
118
    # cover all cases.
119
    assert threading.activeCount() == 1, \
120
           "Found active threads when initializing pycURL"
121

  
122
    pycurl.global_init(pycurl.GLOBAL_ALL)
123
    try:
124
      return fn(*args, **kwargs)
125
    finally:
126
      pycurl.global_cleanup()
127

  
128
  return wrapper
129

  
130

  
131
def GenericCurlConfig(verbose=False, use_signal=False,
132
                      use_curl_cabundle=False, cafile=None, capath=None,
133
                      proxy=None, verify_hostname=False,
134
                      connect_timeout=None, timeout=None,
135
                      _pycurl_version_fn=pycurl.version_info):
136
  """Curl configuration function generator.
137

  
138
  @type verbose: bool
139
  @param verbose: Whether to set cURL to verbose mode
140
  @type use_signal: bool
141
  @param use_signal: Whether to allow cURL to use signals
142
  @type use_curl_cabundle: bool
143
  @param use_curl_cabundle: Whether to use cURL's default CA bundle
144
  @type cafile: string
145
  @param cafile: In which file we can find the certificates
146
  @type capath: string
147
  @param capath: In which directory we can find the certificates
148
  @type proxy: string
149
  @param proxy: Proxy to use, None for default behaviour and empty string for
150
                disabling proxies (see curl_easy_setopt(3))
151
  @type verify_hostname: bool
152
  @param verify_hostname: Whether to verify the remote peer certificate's
153
                          commonName
154
  @type connect_timeout: number
155
  @param connect_timeout: Timeout for establishing connection in seconds
156
  @type timeout: number
157
  @param timeout: Timeout for complete transfer in seconds (see
158
                  curl_easy_setopt(3)).
108 159

  
109 160
  """
110
  _CAPATH_MINVERSION = "0.9"
111
  _DEFVFYPATHS_MINVERSION = "0.9"
161
  if use_curl_cabundle and (cafile or capath):
162
    raise Error("Can not use default CA bundle when CA file or path is set")
112 163

  
113
  _PYOPENSSL_VERSION = OpenSSL.__version__
114
  _PARSED_PYOPENSSL_VERSION = distutils.version.LooseVersion(_PYOPENSSL_VERSION)
115

  
116
  _SUPPORT_CAPATH = (_PARSED_PYOPENSSL_VERSION >= _CAPATH_MINVERSION)
117
  _SUPPORT_DEFVFYPATHS = (_PARSED_PYOPENSSL_VERSION >= _DEFVFYPATHS_MINVERSION)
118

  
119
  def __init__(self, cafile=None, capath=None, use_default_verify_paths=False):
120
    """Initializes this class.
164
  def _ConfigCurl(curl, logger):
165
    """Configures a cURL object
121 166

  
122
    @type cafile: string
123
    @param cafile: In which file we can find the certificates
124
    @type capath: string
125
    @param capath: In which directory we can find the certificates
126
    @type use_default_verify_paths: bool
127
    @param use_default_verify_paths: Whether the platform provided CA
128
                                     certificates are to be used for
129
                                     verification purposes
167
    @type curl: pycurl.Curl
168
    @param curl: cURL object
130 169

  
131 170
    """
132
    self._cafile = cafile
133
    self._capath = capath
134
    self._use_default_verify_paths = use_default_verify_paths
135

  
136
    if self._capath is not None and not self._SUPPORT_CAPATH:
137
      raise Error(("PyOpenSSL %s has no support for a CA directory,"
138
                   " version %s or above is required") %
139
                  (self._PYOPENSSL_VERSION, self._CAPATH_MINVERSION))
140

  
141
    if self._use_default_verify_paths and not self._SUPPORT_DEFVFYPATHS:
142
      raise Error(("PyOpenSSL %s has no support for using default verification"
143
                   " paths, version %s or above is required") %
144
                  (self._PYOPENSSL_VERSION, self._DEFVFYPATHS_MINVERSION))
145

  
146
  @staticmethod
147
  def _VerifySslCertCb(logger, _, cert, errnum, errdepth, ok):
148
    """Callback for SSL certificate verification.
149

  
150
    @param logger: Logging object
151

  
152
    """
153
    if ok:
154
      log_fn = logger.debug
171
    logger.debug("Using cURL version %s", pycurl.version)
172

  
173
    # pycurl.version_info returns a tuple with information about the used
174
    # version of libcurl. Item 5 is the SSL library linked to it.
175
    # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
176
    # 0, '1.2.3.3', ...)
177
    sslver = _pycurl_version_fn()[5]
178
    if not sslver:
179
      raise Error("No SSL support in cURL")
180

  
181
    lcsslver = sslver.lower()
182
    if lcsslver.startswith("openssl/"):
183
      pass
184
    elif lcsslver.startswith("gnutls/"):
185
      if capath:
186
        raise Error("cURL linked against GnuTLS has no support for a"
187
                    " CA path (%s)" % (pycurl.version, ))
155 188
    else:
156
      log_fn = logger.error
157

  
158
    log_fn("Verifying SSL certificate at depth %s, subject '%s', issuer '%s'",
159
           errdepth, FormatX509Name(cert.get_subject()),
160
           FormatX509Name(cert.get_issuer()))
161

  
162
    if not ok:
163
      try:
164
        # Only supported in pyOpenSSL 0.7 and above
165
        # pylint: disable-msg=E1101
166
        fn = OpenSSL.crypto.X509_verify_cert_error_string
167
      except AttributeError:
168
        errmsg = ""
169
      else:
170
        errmsg = ":%s" % fn(errnum)
171

  
172
      logger.error("verify error:num=%s%s", errnum, errmsg)
173

  
174
    return ok
175

  
176
  def __call__(self, ctx, logger):
177
    """Configures an SSL context to verify certificates.
178

  
179
    @type ctx: OpenSSL.SSL.Context
180
    @param ctx: SSL context
181

  
182
    """
183
    if self._use_default_verify_paths:
184
      ctx.set_default_verify_paths()
185

  
186
    if self._cafile or self._capath:
187
      if self._SUPPORT_CAPATH:
188
        ctx.load_verify_locations(self._cafile, self._capath)
189
      else:
190
        ctx.load_verify_locations(self._cafile)
191

  
192
    ctx.set_verify(OpenSSL.SSL.VERIFY_PEER,
193
                   lambda conn, cert, errnum, errdepth, ok: \
194
                     self._VerifySslCertCb(logger, conn, cert,
195
                                           errnum, errdepth, ok))
196

  
197

  
198
class _HTTPSConnectionOpenSSL(httplib.HTTPSConnection):
199
  """HTTPS Connection handler that verifies the SSL certificate.
200

  
201
  """
202
  # Python before version 2.6 had its own httplib.FakeSocket wrapper for
203
  # sockets
204
  _SUPPORT_FAKESOCKET = (sys.hexversion < 0x2060000)
205

  
206
  def __init__(self, *args, **kwargs):
207
    """Initializes this class.
208

  
209
    """
210
    httplib.HTTPSConnection.__init__(self, *args, **kwargs)
211
    self._logger = None
212
    self._config_ssl_verification = None
213

  
214
  def Setup(self, logger, config_ssl_verification):
215
    """Sets the SSL verification config function.
216

  
217
    @param logger: Logging object
218
    @type config_ssl_verification: callable
219

  
220
    """
221
    assert self._logger is None
222
    assert self._config_ssl_verification is None
223

  
224
    self._logger = logger
225
    self._config_ssl_verification = config_ssl_verification
226

  
227
  def connect(self):
228
    """Connect to the server specified when the object was created.
229

  
230
    This ensures that SSL certificates are verified.
231

  
232
    """
233
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
234

  
235
    ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
236
    ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2)
237

  
238
    if self._config_ssl_verification:
239
      self._config_ssl_verification(ctx, self._logger)
240

  
241
    ssl = OpenSSL.SSL.Connection(ctx, sock)
242
    ssl.connect((self.host, self.port))
243

  
244
    if self._SUPPORT_FAKESOCKET:
245
      self.sock = httplib.FakeSocket(sock, ssl)
189
      raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
190
                                sslver)
191

  
192
    curl.setopt(pycurl.VERBOSE, verbose)
193
    curl.setopt(pycurl.NOSIGNAL, not use_signal)
194

  
195
    # Whether to verify remote peer's CN
196
    if verify_hostname:
197
      # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
198
      # certificate must indicate that the server is the server to which you
199
      # meant to connect, or the connection fails. [...] When the value is 1,
200
      # the certificate must contain a Common Name field, but it doesn't matter
201
      # what name it says. [...]"
202
      curl.setopt(pycurl.SSL_VERIFYHOST, 2)
246 203
    else:
247
      self.sock = _SslSocketWrapper(ssl)
248

  
249

  
250
class _SslSocketWrapper(object):
251
  def __init__(self, sock):
252
    """Initializes this class.
253

  
254
    """
255
    self._sock = sock
256

  
257
  def __getattr__(self, name):
258
    """Forward everything to underlying socket.
259

  
260
    """
261
    return getattr(self._sock, name)
262

  
263
  def makefile(self, mode, bufsize):
264
    """Fake makefile method.
265

  
266
    makefile() on normal file descriptors uses dup2(2), which doesn't work with
267
    SSL sockets and therefore is not implemented by pyOpenSSL. This fake method
268
    works with the httplib module, but might not work for other modules.
269

  
270
    """
271
    # pylint: disable-msg=W0212
272
    return socket._fileobject(self._sock, mode, bufsize)
273

  
274

  
275
class _HTTPSHandler(urllib2.HTTPSHandler):
276
  def __init__(self, logger, config_ssl_verification):
277
    """Initializes this class.
278

  
279
    @param logger: Logging object
280
    @type config_ssl_verification: callable
281
    @param config_ssl_verification: Function to configure SSL context for
282
                                    certificate verification
283

  
284
    """
285
    urllib2.HTTPSHandler.__init__(self)
286
    self._logger = logger
287
    self._config_ssl_verification = config_ssl_verification
288

  
289
  def _CreateHttpsConnection(self, *args, **kwargs):
290
    """Wrapper around L{_HTTPSConnectionOpenSSL} to add SSL verification.
291

  
292
    This wrapper is necessary provide a compatible API to urllib2.
293

  
294
    """
295
    conn = _HTTPSConnectionOpenSSL(*args, **kwargs)
296
    conn.Setup(self._logger, self._config_ssl_verification)
297
    return conn
298

  
299
  def https_open(self, req):
300
    """Creates HTTPS connection.
301

  
302
    Called by urllib2.
303

  
304
    """
305
    return self.do_open(self._CreateHttpsConnection, req)
306

  
307

  
308
class _RapiRequest(urllib2.Request):
309
  def __init__(self, method, url, headers, data):
310
    """Initializes this class.
204
      curl.setopt(pycurl.SSL_VERIFYHOST, 0)
205

  
206
    if cafile or capath or use_curl_cabundle:
207
      # Require certificates to be checked
208
      curl.setopt(pycurl.SSL_VERIFYPEER, True)
209
      if cafile:
210
        curl.setopt(pycurl.CAINFO, str(cafile))
211
      if capath:
212
        curl.setopt(pycurl.CAPATH, str(capath))
213
      # Not changing anything for using default CA bundle
214
    else:
215
      # Disable SSL certificate verification
216
      curl.setopt(pycurl.SSL_VERIFYPEER, False)
311 217

  
312
    """
313
    urllib2.Request.__init__(self, url, data=data, headers=headers)
314
    self._method = method
218
    if proxy is not None:
219
      curl.setopt(pycurl.PROXY, str(proxy))
315 220

  
316
  def get_method(self):
317
    """Returns the HTTP request method.
221
    # Timeouts
222
    if connect_timeout is not None:
223
      curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
224
    if timeout is not None:
225
      curl.setopt(pycurl.TIMEOUT, timeout)
318 226

  
319
    """
320
    return self._method
227
  return _ConfigCurl
321 228

  
322 229

  
323 230
class GanetiRapiClient(object):
......
328 235
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
329 236

  
330 237
  def __init__(self, host, port=GANETI_RAPI_PORT,
331
               username=None, password=None,
332
               config_ssl_verification=None, ignore_proxy=False,
333
               logger=logging):
334
    """Constructor.
238
               username=None, password=None, logger=logging,
239
               curl_config_fn=None, curl=None):
240
    """Initializes this class.
335 241

  
336 242
    @type host: string
337 243
    @param host: the ganeti cluster master to interact with
......
341 247
    @param username: the username to connect with
342 248
    @type password: string
343 249
    @param password: the password to connect with
344
    @type config_ssl_verification: callable
345
    @param config_ssl_verification: Function to configure SSL context for
346
                                    certificate verification
347
    @type ignore_proxy: bool
348
    @param ignore_proxy: Whether to ignore proxy settings
250
    @type curl_config_fn: callable
251
    @param curl_config_fn: Function to configure C{pycurl.Curl} object
349 252
    @param logger: Logging object
350 253

  
351 254
    """
......
355 258

  
356 259
    self._base_url = "https://%s:%s" % (host, port)
357 260

  
358
    handlers = [_HTTPSHandler(self._logger, config_ssl_verification)]
359

  
261
    # Create pycURL object if not supplied
262
    if not curl:
263
      curl = pycurl.Curl()
264

  
265
    # Default cURL settings
266
    curl.setopt(pycurl.VERBOSE, False)
267
    curl.setopt(pycurl.FOLLOWLOCATION, False)
268
    curl.setopt(pycurl.MAXREDIRS, 5)
269
    curl.setopt(pycurl.NOSIGNAL, True)
270
    curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
271
    curl.setopt(pycurl.SSL_VERIFYHOST, 0)
272
    curl.setopt(pycurl.SSL_VERIFYPEER, False)
273
    curl.setopt(pycurl.HTTPHEADER, [
274
      "Accept: %s" % HTTP_APP_JSON,
275
      "Content-type: %s" % HTTP_APP_JSON,
276
      ])
277

  
278
    # Setup authentication
360 279
    if username is not None:
361
      pwmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
362
      pwmgr.add_password(None, self._base_url, username, password)
363
      handlers.append(urllib2.HTTPBasicAuthHandler(pwmgr))
280
      if password is None:
281
        raise Error("Password not specified")
282
      curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
283
      curl.setopt(pycurl.USERPWD, str("%s:%s" % (username, password)))
364 284
    elif password:
365 285
      raise Error("Specified password without username")
366 286

  
367
    if ignore_proxy:
368
      handlers.append(urllib2.ProxyHandler({}))
369

  
370
    self._http = urllib2.build_opener(*handlers) # pylint: disable-msg=W0142
287
    # Call external configuration function
288
    if curl_config_fn:
289
      curl_config_fn(curl, logger)
371 290

  
372
    self._headers = {
373
      "Accept": HTTP_APP_JSON,
374
      "Content-type": HTTP_APP_JSON,
375
      "User-Agent": self.USER_AGENT,
376
      }
291
    self._curl = curl
377 292

  
378 293
  @staticmethod
379 294
  def _EncodeQuery(query):
......
427 342
    """
428 343
    assert path.startswith("/")
429 344

  
345
    curl = self._curl
346

  
430 347
    if content:
431 348
      encoded_content = self._json_encoder.encode(content)
432 349
    else:
433
      encoded_content = None
350
      encoded_content = ""
434 351

  
435 352
    # Build URL
436 353
    urlparts = [self._base_url, path]
......
440 357

  
441 358
    url = "".join(urlparts)
442 359

  
443
    self._logger.debug("Sending request %s %s to %s:%s"
444
                       " (headers=%r, content=%r)",
445
                       method, url, self._host, self._port, self._headers,
446
                       encoded_content)
360
    self._logger.debug("Sending request %s %s to %s:%s (content=%r)",
361
                       method, url, self._host, self._port, encoded_content)
362

  
363
    # Buffer for response
364
    encoded_resp_body = StringIO()
447 365

  
448
    req = _RapiRequest(method, url, self._headers, encoded_content)
366
    # Configure cURL
367
    curl.setopt(pycurl.CUSTOMREQUEST, str(method))
368
    curl.setopt(pycurl.URL, str(url))
369
    curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
370
    curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
449 371

  
450 372
    try:
451
      resp = self._http.open(req)
452
      encoded_response_content = resp.read()
453
    except (OpenSSL.SSL.Error, OpenSSL.crypto.Error), err:
454
      raise CertificateError("SSL issue: %s (%r)" % (err, err))
455
    except urllib2.HTTPError, err:
456
      raise GanetiApiError(str(err), code=err.code)
457
    except urllib2.URLError, err:
458
      raise GanetiApiError(str(err))
459

  
460
    if encoded_response_content:
461
      response_content = simplejson.loads(encoded_response_content)
373
      # Send request and wait for response
374
      try:
375
        curl.perform()
376
      except pycurl.error, err:
377
        if err.args[0] in _CURL_SSL_CERT_ERRORS:
378
          raise CertificateError("SSL certificate error %s" % err)
379

  
380
        raise GanetiApiError(str(err))
381
    finally:
382
      # Reset settings to not keep references to large objects in memory
383
      # between requests
384
      curl.setopt(pycurl.POSTFIELDS, "")
385
      curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
386

  
387
    # Get HTTP response code
388
    http_code = curl.getinfo(pycurl.RESPONSE_CODE)
389

  
390
    # Was anything written to the response buffer?
391
    if encoded_resp_body.tell():
392
      response_content = simplejson.loads(encoded_resp_body.getvalue())
462 393
    else:
463 394
      response_content = None
464 395

  
465
    # TODO: Are there other status codes that are valid? (redirect?)
466
    if resp.code != HTTP_OK:
396
    if http_code != HTTP_OK:
467 397
      if isinstance(response_content, dict):
468 398
        msg = ("%s %s: %s" %
469 399
               (response_content["code"],
......
472 402
      else:
473 403
        msg = str(response_content)
474 404

  
475
      raise GanetiApiError(msg, code=resp.code)
405
      raise GanetiApiError(msg, code=http_code)
476 406

  
477 407
    return response_content
478 408

  
b/qa/ganeti-qa.py
39 39
import qa_utils
40 40

  
41 41
from ganeti import utils
42
from ganeti import rapi
43

  
44
import ganeti.rapi.client
42 45

  
43 46

  
44 47
def RunTest(fn, *args):
......
269 272
            instance, pnode, snode)
270 273

  
271 274

  
275
@rapi.client.UsesRapiClient
272 276
def main():
273 277
  """Main program.
274 278

  
b/qa/qa_rapi.py
72 72
  _rapi_ca.flush()
73 73

  
74 74
  port = qa_config.get("rapi-port", default=constants.DEFAULT_RAPI_PORT)
75
  cfg_ssl = rapi.client.CertAuthorityVerify(cafile=_rapi_ca.name)
75
  cfg_curl = rapi.client.GenericCurlConfig(cafile=_rapi_ca.name,
76
                                           proxy="")
76 77

  
77 78
  _rapi_client = rapi.client.GanetiRapiClient(master["primary"], port=port,
78 79
                                              username=username,
79 80
                                              password=password,
80
                                              config_ssl_verification=cfg_ssl,
81
                                              ignore_proxy=True)
81
                                              curl_config_fn=cfg_curl)
82 82

  
83 83
  print "RAPI protocol version: %s" % _rapi_client.GetVersion()
84 84

  
b/test/ganeti.rapi.client_unittest.py
25 25
import re
26 26
import unittest
27 27
import warnings
28
import pycurl
28 29

  
30
from ganeti import constants
29 31
from ganeti import http
30 32
from ganeti import serializer
31 33

  
......
50 52
    return None
51 53

  
52 54

  
53
class HttpResponseMock:
54
  """Dumb mock of httplib.HTTPResponse.
55

  
56
  """
57

  
58
  def __init__(self, code, data):
59
    self.code = code
60
    self._data = data
55
class FakeCurl:
56
  def __init__(self, rapi):
57
    self._rapi = rapi
58
    self._opts = {}
59
    self._info = {}
61 60

  
62
  def read(self):
63
    return self._data
61
  def setopt(self, opt, value):
62
    self._opts[opt] = value
64 63

  
64
  def getopt(self, opt):
65
    return self._opts.get(opt)
65 66

  
66
class OpenerDirectorMock:
67
  """Mock for urllib.OpenerDirector.
67
  def unsetopt(self, opt):
68
    self._opts.pop(opt, None)
68 69

  
69
  """
70
  def getinfo(self, info):
71
    return self._info[info]
70 72

  
71
  def __init__(self, rapi):
72
    self._rapi = rapi
73
    self.last_request = None
73
  def perform(self):
74
    method = self._opts[pycurl.CUSTOMREQUEST]
75
    url = self._opts[pycurl.URL]
76
    request_body = self._opts[pycurl.POSTFIELDS]
77
    writefn = self._opts[pycurl.WRITEFUNCTION]
74 78

  
75
  def open(self, req):
76
    self.last_request = req
79
    path = _GetPathFromUri(url)
80
    (code, resp_body) = self._rapi.FetchResponse(path, method, request_body)
77 81

  
78
    path = _GetPathFromUri(req.get_full_url())
79
    code, resp_body = self._rapi.FetchResponse(path, req.get_method())
80
    return HttpResponseMock(code, resp_body)
82
    self._info[pycurl.RESPONSE_CODE] = code
83
    if resp_body is not None:
84
      writefn(resp_body)
81 85

  
82 86

  
83 87
class RapiMock(object):
......
85 89
    self._mapper = connector.Mapper()
86 90
    self._responses = []
87 91
    self._last_handler = None
92
    self._last_req_data = None
88 93

  
89 94
  def AddResponse(self, response, code=200):
90 95
    self._responses.insert(0, (code, response))
......
92 97
  def GetLastHandler(self):
93 98
    return self._last_handler
94 99

  
95
  def FetchResponse(self, path, method):
100
  def GetLastRequestData(self):
101
    return self._last_req_data
102

  
103
  def FetchResponse(self, path, method, request_body):
104
    self._last_req_data = request_body
105

  
96 106
    try:
97 107
      HandlerClass, items, args = self._mapper.getController(path)
98 108
      self._last_handler = HandlerClass(items, args, None)
......
111 121
    return code, response
112 122

  
113 123

  
124
class TestConstants(unittest.TestCase):
125
  def test(self):
126
    self.assertEqual(client.GANETI_RAPI_PORT, constants.DEFAULT_RAPI_PORT)
127
    self.assertEqual(client.GANETI_RAPI_VERSION, constants.RAPI_VERSION)
128
    self.assertEqual(client.HTTP_APP_JSON, http.HTTP_APP_JSON)
129
    self.assertEqual(client._REQ_DATA_VERSION_FIELD, rlib2._REQ_DATA_VERSION)
130
    self.assertEqual(client._INST_CREATE_REQV1, rlib2._INST_CREATE_REQV1)
131

  
132

  
114 133
class RapiMockTest(unittest.TestCase):
115 134
  def test(self):
116 135
    rapi = RapiMock()
117 136
    path = "/version"
118
    self.assertEqual((404, None), rapi.FetchResponse("/foo", "GET"))
137
    self.assertEqual((404, None), rapi.FetchResponse("/foo", "GET", None))
119 138
    self.assertEqual((501, "Method not implemented"),
120
                     rapi.FetchResponse("/version", "POST"))
139
                     rapi.FetchResponse("/version", "POST", None))
121 140
    rapi.AddResponse("2")
122
    code, response = rapi.FetchResponse("/version", "GET")
141
    code, response = rapi.FetchResponse("/version", "GET", None)
123 142
    self.assertEqual(200, code)
124 143
    self.assertEqual("2", response)
125 144
    self.failUnless(isinstance(rapi.GetLastHandler(), rlib2.R_version))
126 145

  
127 146

  
147
def _FakeNoSslPycurlVersion():
148
  # Note: incomplete version tuple
149
  return (3, "7.16.0", 462848, "mysystem", 1581, None, 0)
150

  
151

  
152
def _FakeFancySslPycurlVersion():
153
  # Note: incomplete version tuple
154
  return (3, "7.16.0", 462848, "mysystem", 1581, "FancySSL/1.2.3", 0)
155

  
156

  
157
def _FakeOpenSslPycurlVersion():
158
  # Note: incomplete version tuple
159
  return (2, "7.15.5", 462597, "othersystem", 668, "OpenSSL/0.9.8c", 0)
160

  
161

  
162
def _FakeGnuTlsPycurlVersion():
163
  # Note: incomplete version tuple
164
  return (3, "7.18.0", 463360, "somesystem", 1581, "GnuTLS/2.0.4", 0)
165

  
166

  
167
class TestExtendedConfig(unittest.TestCase):
168
  def testAuth(self):
169
    curl = FakeCurl(RapiMock())
170
    cl = client.GanetiRapiClient("master.example.com",
171
                                 username="user", password="pw",
172
                                 curl=curl)
173

  
174
    self.assertEqual(curl.getopt(pycurl.HTTPAUTH), pycurl.HTTPAUTH_BASIC)
175
    self.assertEqual(curl.getopt(pycurl.USERPWD), "user:pw")
176

  
177
  def testInvalidAuth(self):
178
    # No username
179
    self.assertRaises(client.Error, client.GanetiRapiClient,
180
                      "master-a.example.com", password="pw")
181
    # No password
182
    self.assertRaises(client.Error, client.GanetiRapiClient,
183
                      "master-b.example.com", username="user")
184

  
185
  def testCertVerifyInvalidCombinations(self):
186
    self.assertRaises(client.Error, client.GenericCurlConfig,
187
                      use_curl_cabundle=True, cafile="cert1.pem")
188
    self.assertRaises(client.Error, client.GenericCurlConfig,
189
                      use_curl_cabundle=True, capath="certs/")
190
    self.assertRaises(client.Error, client.GenericCurlConfig,
191
                      use_curl_cabundle=True,
192
                      cafile="cert1.pem", capath="certs/")
193

  
194
  def testProxySignalVerifyHostname(self):
195
    for use_gnutls in [False, True]:
196
      if use_gnutls:
197
        pcverfn = _FakeGnuTlsPycurlVersion
198
      else:
199
        pcverfn = _FakeOpenSslPycurlVersion
200

  
201
      for proxy in ["", "http://127.0.0.1:1234"]:
202
        for use_signal in [False, True]:
203
          for verify_hostname in [False, True]:
204
            cfgfn = client.GenericCurlConfig(proxy=proxy, use_signal=use_signal,
205
                                             verify_hostname=verify_hostname,
206
                                             _pycurl_version_fn=pcverfn)
207

  
208
            curl = FakeCurl(RapiMock())
209
            cl = client.GanetiRapiClient("master.example.com",
210
                                         curl_config_fn=cfgfn, curl=curl)
211

  
212
            self.assertEqual(curl.getopt(pycurl.PROXY), proxy)
213
            self.assertEqual(curl.getopt(pycurl.NOSIGNAL), not use_signal)
214

  
215
            if verify_hostname:
216
              self.assertEqual(curl.getopt(pycurl.SSL_VERIFYHOST), 2)
217
            else:
218
              self.assertEqual(curl.getopt(pycurl.SSL_VERIFYHOST), 0)
219

  
220
  def testNoCertVerify(self):
221
    cfgfn = client.GenericCurlConfig()
222

  
223
    curl = FakeCurl(RapiMock())
224
    cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
225
                                 curl=curl)
226

  
227
    self.assertFalse(curl.getopt(pycurl.SSL_VERIFYPEER))
228
    self.assertFalse(curl.getopt(pycurl.CAINFO))
229
    self.assertFalse(curl.getopt(pycurl.CAPATH))
230

  
231
  def testCertVerifyCurlBundle(self):
232
    cfgfn = client.GenericCurlConfig(use_curl_cabundle=True)
233

  
234
    curl = FakeCurl(RapiMock())
235
    cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
236
                                 curl=curl)
237

  
238
    self.assert_(curl.getopt(pycurl.SSL_VERIFYPEER))
239
    self.assertFalse(curl.getopt(pycurl.CAINFO))
240
    self.assertFalse(curl.getopt(pycurl.CAPATH))
241

  
242
  def testCertVerifyCafile(self):
243
    mycert = "/tmp/some/UNUSED/cert/file.pem"
244
    cfgfn = client.GenericCurlConfig(cafile=mycert)
245

  
246
    curl = FakeCurl(RapiMock())
247
    cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
248
                                 curl=curl)
249

  
250
    self.assert_(curl.getopt(pycurl.SSL_VERIFYPEER))
251
    self.assertEqual(curl.getopt(pycurl.CAINFO), mycert)
252
    self.assertFalse(curl.getopt(pycurl.CAPATH))
253

  
254
  def testCertVerifyCapath(self):
255
    certdir = "/tmp/some/UNUSED/cert/directory"
256
    pcverfn = _FakeOpenSslPycurlVersion
257
    cfgfn = client.GenericCurlConfig(capath=certdir,
258
                                     _pycurl_version_fn=pcverfn)
259

  
260
    curl = FakeCurl(RapiMock())
261
    cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
262
                                 curl=curl)
263

  
264
    self.assert_(curl.getopt(pycurl.SSL_VERIFYPEER))
265
    self.assertEqual(curl.getopt(pycurl.CAPATH), certdir)
266
    self.assertFalse(curl.getopt(pycurl.CAINFO))
267

  
268
  def testCertVerifyCapathGnuTls(self):
269
    certdir = "/tmp/some/UNUSED/cert/directory"
270
    pcverfn = _FakeGnuTlsPycurlVersion
271
    cfgfn = client.GenericCurlConfig(capath=certdir,
272
                                     _pycurl_version_fn=pcverfn)
273

  
274
    curl = FakeCurl(RapiMock())
275
    self.assertRaises(client.Error, client.GanetiRapiClient,
276
                      "master.example.com", curl_config_fn=cfgfn, curl=curl)
277

  
278
  def testCertVerifyNoSsl(self):
279
    certdir = "/tmp/some/UNUSED/cert/directory"
280
    pcverfn = _FakeNoSslPycurlVersion
281
    cfgfn = client.GenericCurlConfig(capath=certdir,
282
                                     _pycurl_version_fn=pcverfn)
283

  
284
    curl = FakeCurl(RapiMock())
285
    self.assertRaises(client.Error, client.GanetiRapiClient,
286
                      "master.example.com", curl_config_fn=cfgfn, curl=curl)
287

  
288
  def testCertVerifyFancySsl(self):
289
    certdir = "/tmp/some/UNUSED/cert/directory"
290
    pcverfn = _FakeFancySslPycurlVersion
291
    cfgfn = client.GenericCurlConfig(capath=certdir,
292
                                     _pycurl_version_fn=pcverfn)
293

  
294
    curl = FakeCurl(RapiMock())
295
    self.assertRaises(NotImplementedError, client.GanetiRapiClient,
296
                      "master.example.com", curl_config_fn=cfgfn, curl=curl)
297

  
298
  def testCertVerifyCapath(self):
299
    for connect_timeout in [None, 1, 5, 10, 30, 60, 300]:
300
      for timeout in [None, 1, 30, 60, 3600, 24 * 3600]:
301
        cfgfn = client.GenericCurlConfig(connect_timeout=connect_timeout,
302
                                         timeout=timeout)
303

  
304
        curl = FakeCurl(RapiMock())
305
        cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
306
                                     curl=curl)
307

  
308
        self.assertEqual(curl.getopt(pycurl.CONNECTTIMEOUT), connect_timeout)
309
        self.assertEqual(curl.getopt(pycurl.TIMEOUT), timeout)
310

  
311

  
128 312
class GanetiRapiClientTests(testutils.GanetiTestCase):
129 313
  def setUp(self):
130 314
    testutils.GanetiTestCase.setUp(self)
131 315

  
132 316
    self.rapi = RapiMock()
133
    self.http = OpenerDirectorMock(self.rapi)
134
    self.client = client.GanetiRapiClient('master.foo.com')
135
    self.client._http = self.http
136
    # Hard-code the version for easier testing.
137
    self.client._version = 2
317
    self.curl = FakeCurl(self.rapi)
318
    self.client = client.GanetiRapiClient("master.example.com",
319
                                          curl=self.curl)
320

  
321
    # Signals should be disabled by default
322
    self.assert_(self.curl.getopt(pycurl.NOSIGNAL))
323

  
324
    # No auth and no proxy
325
    self.assertFalse(self.curl.getopt(pycurl.USERPWD))
326
    self.assert_(self.curl.getopt(pycurl.PROXY) is None)
327

  
328
    # Content-type is required for requests
329
    headers = self.curl.getopt(pycurl.HTTPHEADER)
330
    self.assert_("Content-type: application/json" in headers)
138 331

  
139 332
  def assertHandler(self, handler_cls):
140 333
    self.failUnless(isinstance(self.rapi.GetLastHandler(), handler_cls))
......
273 466
    self.assertHandler(rlib2.R_2_instances)
274 467
    self.assertDryRun()
275 468

  
276
    data = serializer.LoadJson(self.http.last_request.data)
469
    data = serializer.LoadJson(self.rapi.GetLastRequestData())
277 470

  
278 471
    for field in ["dry_run", "beparams", "hvparams", "start"]:
279 472
      self.assertFalse(field in data)
......
293 486
    self.assertEqual(job_id, 24740)
294 487
    self.assertHandler(rlib2.R_2_instances)
295 488

  
296
    data = serializer.LoadJson(self.http.last_request.data)
489
    data = serializer.LoadJson(self.rapi.GetLastRequestData())
297 490
    self.assertEqual(data[rlib2._REQ_DATA_VERSION], 1)
298 491
    self.assertEqual(data["name"], "inst2.example.com")
299 492
    self.assertEqual(data["disk_template"], "drbd8")
......
411 604
    self.assertHandler(rlib2.R_2_instances_name_export)
412 605
    self.assertItems(["inst2"])
413 606

  
414
    data = serializer.LoadJson(self.http.last_request.data)
607
    data = serializer.LoadJson(self.rapi.GetLastRequestData())
415 608
    self.assertEqual(data["mode"], "local")
416 609
    self.assertEqual(data["destination"], "nodeX")
417 610
    self.assertEqual(data["shutdown"], True)
......
509 702
    self.assertHandler(rlib2.R_2_nodes_name_role)
510 703
    self.assertItems(["node-foo"])
511 704
    self.assertQuery("force", ["1"])
512
    self.assertEqual("\"master-candidate\"", self.http.last_request.data)
705
    self.assertEqual("\"master-candidate\"", self.rapi.GetLastRequestData())
513 706

  
514 707
  def testGetNodeStorageUnits(self):
515 708
    self.rapi.AddResponse("42")
......
576 769

  
577 770

  
578 771
if __name__ == '__main__':
579
  testutils.GanetiTestProgram()
772
  client.UsesRapiClient(testutils.GanetiTestProgram)()
b/tools/move-instance
148 148
    self.src_cluster_name = src_cluster_name
149 149
    self.dest_cluster_name = dest_cluster_name
150 150

  
151
    # TODO: Implement timeouts for RAPI connections
151 152
    # TODO: Support for using system default paths for verifying SSL certificate
152
    # (already implemented in CertAuthorityVerify)
153 153
    logging.debug("Using '%s' as source CA", options.src_ca_file)
154
    src_ssl_config = rapi.client.CertAuthorityVerify(cafile=options.src_ca_file)
154
    src_curl_config = rapi.client.GenericCurlConfig(cafile=options.src_ca_file)
155 155

  
156 156
    if options.dest_ca_file:
157 157
      logging.debug("Using '%s' as destination CA", options.dest_ca_file)
158
      dest_ssl_config = \
159
        rapi.client.CertAuthorityVerify(cafile=options.dest_ca_file)
158
      dest_curl_config = \
159
        rapi.client.GenericCurlConfig(cafile=options.dest_ca_file)
160 160
    else:
161 161
      logging.debug("Using source CA for destination")
162
      dest_ssl_config = src_ssl_config
162
      dest_curl_config = src_curl_config
163 163

  
164 164
    logging.debug("Source RAPI server is %s:%s",
165 165
                  src_cluster_name, options.src_rapi_port)
......
182 182
    self.GetSourceClient = lambda: \
183 183
      rapi.client.GanetiRapiClient(src_cluster_name,
184 184
                                   port=options.src_rapi_port,
185
                                   config_ssl_verification=src_ssl_config,
185
                                   curl_config_fn=src_curl_config,
186 186
                                   username=src_username,
187 187
                                   password=src_password)
188 188

  
......
212 212
    self.GetDestClient = lambda: \
213 213
      rapi.client.GanetiRapiClient(dest_cluster_name,
214 214
                                   port=dest_rapi_port,
215
                                   config_ssl_verification=dest_ssl_config,
215
                                   curl_config_fn=dest_curl_config,
216 216
                                   username=dest_username,
217 217
                                   password=dest_password)
218 218

  
......
771 771
  return (src_cluster_name, dest_cluster_name, instance_names)
772 772

  
773 773

  
774
@rapi.client.UsesRapiClient
774 775
def main():
775 776
  """Main routine.
776 777

  

Also available in: Unified diff