Revision b4135a1b snf-cyclades-app/synnefo/logic/rapi.py

b/snf-cyclades-app/synnefo/logic/rapi.py
2 2
#
3 3

  
4 4
# Copyright (C) 2010, 2011 Google Inc.
5
# Copyright (C) 2013, GRNET S.A.
5 6
#
6 7
# This program is free software; you can redistribute it and/or modify
7 8
# it under the terms of the GNU General Public License as published by
......
19 20
# 02110-1301, USA.
20 21

  
21 22

  
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
"""Ganeti RAPI client."""
32 24

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

  
28
import requests
36 29
import logging
37 30
import simplejson
38
import socket
39
import urllib
40
import threading
41
import pycurl
42 31
import time
43 32

  
44
try:
45
  from cStringIO import StringIO
46
except ImportError:
47
  from StringIO import StringIO
48

  
49

  
50 33
GANETI_RAPI_PORT = 5080
51 34
GANETI_RAPI_VERSION = 2
52 35

  
......
112 95
_NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
113 96
_NODE_EVAC_RES1 = NODE_EVAC_RES1
114 97

  
115
# Older pycURL versions don't have all error constants
116
try:
117
  _CURLE_SSL_CACERT = pycurl.E_SSL_CACERT
118
  _CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE
119
except AttributeError:
120
  _CURLE_SSL_CACERT = 60
121
  _CURLE_SSL_CACERT_BADFILE = 77
122

  
123
_CURL_SSL_CERT_ERRORS = frozenset([
124
  _CURLE_SSL_CACERT,
125
  _CURLE_SSL_CACERT_BADFILE,
126
  ])
127 98

  
128 99

  
129 100
class Error(Exception):
......
183 154
  return condition
184 155

  
185 156

  
186
def UsesRapiClient(fn):
187
  """Decorator for code using RAPI client to initialize pycURL.
188

  
189
  """
190
  def wrapper(*args, **kwargs):
191
    # curl_global_init(3) and curl_global_cleanup(3) must be called with only
192
    # one thread running. This check is just a safety measure -- it doesn't
193
    # cover all cases.
194
    assert threading.activeCount() == 1, \
195
           "Found active threads when initializing pycURL"
196

  
197
    pycurl.global_init(pycurl.GLOBAL_ALL)
198
    try:
199
      return fn(*args, **kwargs)
200
    finally:
201
      pycurl.global_cleanup()
202

  
203
  return wrapper
204

  
205

  
206
def GenericCurlConfig(verbose=False, use_signal=False,
207
                      use_curl_cabundle=False, cafile=None, capath=None,
208
                      proxy=None, verify_hostname=False,
209
                      connect_timeout=None, timeout=None,
210
                      _pycurl_version_fn=pycurl.version_info):
211
  """Curl configuration function generator.
212

  
213
  @type verbose: bool
214
  @param verbose: Whether to set cURL to verbose mode
215
  @type use_signal: bool
216
  @param use_signal: Whether to allow cURL to use signals
217
  @type use_curl_cabundle: bool
218
  @param use_curl_cabundle: Whether to use cURL's default CA bundle
219
  @type cafile: string
220
  @param cafile: In which file we can find the certificates
221
  @type capath: string
222
  @param capath: In which directory we can find the certificates
223
  @type proxy: string
224
  @param proxy: Proxy to use, None for default behaviour and empty string for
225
                disabling proxies (see curl_easy_setopt(3))
226
  @type verify_hostname: bool
227
  @param verify_hostname: Whether to verify the remote peer certificate's
228
                          commonName
229
  @type connect_timeout: number
230
  @param connect_timeout: Timeout for establishing connection in seconds
231
  @type timeout: number
232
  @param timeout: Timeout for complete transfer in seconds (see
233
                  curl_easy_setopt(3)).
234

  
235
  """
236
  if use_curl_cabundle and (cafile or capath):
237
    raise Error("Can not use default CA bundle when CA file or path is set")
238

  
239
  def _ConfigCurl(curl, logger):
240
    """Configures a cURL object
241

  
242
    @type curl: pycurl.Curl
243
    @param curl: cURL object
244

  
245
    """
246
    logger.debug("Using cURL version %s", pycurl.version)
247

  
248
    # pycurl.version_info returns a tuple with information about the used
249
    # version of libcurl. Item 5 is the SSL library linked to it.
250
    # e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4',
251
    # 0, '1.2.3.3', ...)
252
    sslver = _pycurl_version_fn()[5]
253
    if not sslver:
254
      raise Error("No SSL support in cURL")
255

  
256
    lcsslver = sslver.lower()
257
    if lcsslver.startswith("openssl/"):
258
      pass
259
    elif lcsslver.startswith("nss/"):
260
      # TODO: investigate compatibility beyond a simple test
261
      pass
262
    elif lcsslver.startswith("gnutls/"):
263
      if capath:
264
        raise Error("cURL linked against GnuTLS has no support for a"
265
                    " CA path (%s)" % (pycurl.version, ))
266
    else:
267
      raise NotImplementedError("cURL uses unsupported SSL version '%s'" %
268
                                sslver)
269

  
270
    curl.setopt(pycurl.VERBOSE, verbose)
271
    curl.setopt(pycurl.NOSIGNAL, not use_signal)
272

  
273
    # Whether to verify remote peer's CN
274
    if verify_hostname:
275
      # curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that
276
      # certificate must indicate that the server is the server to which you
277
      # meant to connect, or the connection fails. [...] When the value is 1,
278
      # the certificate must contain a Common Name field, but it doesn't matter
279
      # what name it says. [...]"
280
      curl.setopt(pycurl.SSL_VERIFYHOST, 2)
281
    else:
282
      curl.setopt(pycurl.SSL_VERIFYHOST, 0)
283

  
284
    if cafile or capath or use_curl_cabundle:
285
      # Require certificates to be checked
286
      curl.setopt(pycurl.SSL_VERIFYPEER, True)
287
      if cafile:
288
        curl.setopt(pycurl.CAINFO, str(cafile))
289
      if capath:
290
        curl.setopt(pycurl.CAPATH, str(capath))
291
      # Not changing anything for using default CA bundle
292
    else:
293
      # Disable SSL certificate verification
294
      curl.setopt(pycurl.SSL_VERIFYPEER, False)
295

  
296
    if proxy is not None:
297
      curl.setopt(pycurl.PROXY, str(proxy))
298

  
299
    # Timeouts
300
    if connect_timeout is not None:
301
      curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout)
302
    if timeout is not None:
303
      curl.setopt(pycurl.TIMEOUT, timeout)
304

  
305
  return _ConfigCurl
306

  
307

  
308 157
class GanetiRapiClient(object): # pylint: disable=R0904
309 158
  """Ganeti RAPI client.
310 159

  
......
313 162
  _json_encoder = simplejson.JSONEncoder(sort_keys=True)
314 163

  
315 164
  def __init__(self, host, port=GANETI_RAPI_PORT,
316
               username=None, password=None, logger=logging,
317
               curl_config_fn=None, curl_factory=None):
165
               username=None, password=None, logger=logging):
318 166
    """Initializes this class.
319 167

  
320 168
    @type host: string
......
325 173
    @param username: the username to connect with
326 174
    @type password: string
327 175
    @param password: the password to connect with
328
    @type curl_config_fn: callable
329
    @param curl_config_fn: Function to configure C{pycurl.Curl} object
330 176
    @param logger: Logging object
331 177

  
332 178
    """
333
    self._username = username
334
    self._password = password
335 179
    self._logger = logger
336
    self._curl_config_fn = curl_config_fn
337
    self._curl_factory = curl_factory
338

  
339
    try:
340
      socket.inet_pton(socket.AF_INET6, host)
341
      address = "[%s]:%s" % (host, port)
342
    except socket.error:
343
      address = "%s:%s" % (host, port)
344

  
345
    self._base_url = "https://%s" % address
180
    self._base_url = "https://%s:%s" % (host, port)
346 181

  
347 182
    if username is not None:
348 183
      if password is None:
......
350 185
    elif password:
351 186
      raise Error("Specified password without username")
352 187

  
353
  def _CreateCurl(self):
354
    """Creates a cURL object.
355

  
356
    """
357
    # Create pycURL object if no factory is provided
358
    if self._curl_factory:
359
      curl = self._curl_factory()
360
    else:
361
      curl = pycurl.Curl()
362

  
363
    # Default cURL settings
364
    curl.setopt(pycurl.VERBOSE, False)
365
    curl.setopt(pycurl.FOLLOWLOCATION, False)
366
    curl.setopt(pycurl.MAXREDIRS, 5)
367
    curl.setopt(pycurl.NOSIGNAL, True)
368
    curl.setopt(pycurl.USERAGENT, self.USER_AGENT)
369
    curl.setopt(pycurl.SSL_VERIFYHOST, 0)
370
    curl.setopt(pycurl.SSL_VERIFYPEER, False)
371
    curl.setopt(pycurl.HTTPHEADER, [
372
      "Accept: %s" % HTTP_APP_JSON,
373
      "Content-type: %s" % HTTP_APP_JSON,
374
      ])
375

  
376
    assert ((self._username is None and self._password is None) ^
377
            (self._username is not None and self._password is not None))
378

  
379
    if self._username:
380
      # Setup authentication
381
      curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
382
      curl.setopt(pycurl.USERPWD,
383
                  str("%s:%s" % (self._username, self._password)))
384

  
385
    # Call external configuration function
386
    if self._curl_config_fn:
387
      self._curl_config_fn(curl, self._logger)
388

  
389
    return curl
390

  
391
  @staticmethod
392
  def _EncodeQuery(query):
393
    """Encode query values for RAPI URL.
394

  
395
    @type query: list of two-tuples
396
    @param query: Query arguments
397
    @rtype: list
398
    @return: Query list with encoded values
399

  
400
    """
401
    result = []
402

  
403
    for name, value in query:
404
      if value is None:
405
        result.append((name, ""))
406

  
407
      elif isinstance(value, bool):
408
        # Boolean values must be encoded as 0 or 1
409
        result.append((name, int(value)))
410

  
411
      elif isinstance(value, (list, tuple, dict)):
412
        raise ValueError("Invalid query data type %r" % type(value).__name__)
413

  
414
      else:
415
        result.append((name, value))
416

  
417
    return result
188
    self._auth = (username, password)
418 189

  
419 190
  def _SendRequest(self, method, path, query, content):
420 191
    """Sends an HTTP request.
......
439 210

  
440 211
    """
441 212
    assert path.startswith("/")
213
    url = "%s%s" % (self._base_url, path)
442 214

  
443
    curl = self._CreateCurl()
444

  
215
    headers = {}
445 216
    if content is not None:
446 217
      encoded_content = self._json_encoder.encode(content)
218
      headers = {"content-type": HTTP_APP_JSON,
219
                 "accept": HTTP_APP_JSON}
447 220
    else:
448 221
      encoded_content = ""
449 222

  
450
    # Build URL
451
    urlparts = [self._base_url, path]
452
    if query:
453
      urlparts.append("?")
454
      urlparts.append(urllib.urlencode(self._EncodeQuery(query)))
223
    if query is not None:
224
        query = dict(query)
455 225

  
456
    url = "".join(urlparts)
226
    self._logger.debug("Sending request %s %s (query=%r) (content=%r)",
227
                       method, url, query, encoded_content)
457 228

  
458
    self._logger.debug("Sending request %s %s (content=%r)",
459
                       method, url, encoded_content)
229
    req_method = getattr(requests, method.lower())
230
    r = req_method(url, auth=self._auth, headers=headers, params=query,
231
                   data=encoded_content, verify=False)
460 232

  
461
    # Buffer for response
462
    encoded_resp_body = StringIO()
463 233

  
464
    # Configure cURL
465
    curl.setopt(pycurl.CUSTOMREQUEST, str(method))
466
    curl.setopt(pycurl.URL, str(url))
467
    curl.setopt(pycurl.POSTFIELDS, str(encoded_content))
468
    curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write)
469

  
470
    try:
471
      # Send request and wait for response
472
      try:
473
        curl.perform()
474
      except pycurl.error, err:
475
        if err.args[0] in _CURL_SSL_CERT_ERRORS:
476
          raise CertificateError("SSL certificate error %s" % err,
477
                                 code=err.args[0])
478

  
479
        raise GanetiApiError(str(err), code=err.args[0])
480
    finally:
481
      # Reset settings to not keep references to large objects in memory
482
      # between requests
483
      curl.setopt(pycurl.POSTFIELDS, "")
484
      curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
485

  
486
    # Get HTTP response code
487
    http_code = curl.getinfo(pycurl.RESPONSE_CODE)
488

  
489
    # Was anything written to the response buffer?
490
    if encoded_resp_body.tell():
491
      response_content = simplejson.loads(encoded_resp_body.getvalue())
234
    http_code = r.status_code
235
    if r.content is not None:
236
        response_content = simplejson.loads(r.content)
492 237
    else:
493
      response_content = None
238
        response_content = None
494 239

  
495 240
    if http_code != HTTP_OK:
496 241
      if isinstance(response_content, dict):

Also available in: Unified diff