Package cm_api :: Module http_client
[hide private]
[frames] | no frames]

Source Code for Module cm_api.http_client

  1  # Licensed to Cloudera, Inc. under one 
  2  # or more contributor license agreements.  See the NOTICE file 
  3  # distributed with this work for additional information 
  4  # regarding copyright ownership.  Cloudera, Inc. licenses this file 
  5  # to you under the Apache License, Version 2.0 (the 
  6  # "License"); you may not use this file except in compliance 
  7  # with the License.  You may obtain a copy of the License at 
  8  # 
  9  #     http://www.apache.org/licenses/LICENSE-2.0 
 10  # 
 11  # Unless required by applicable law or agreed to in writing, software 
 12  # distributed under the License is distributed on an "AS IS" BASIS, 
 13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 14  # See the License for the specific language governing permissions and 
 15  # limitations under the License. 
 16   
 17  import os 
 18  import cookielib 
 19  import logging 
 20  import posixpath 
 21  import types 
 22  import urllib 
 23   
 24  try: 
 25    import socks 
 26    import socket 
 27    socks_server = os.environ.get("SOCKS_SERVER", None) 
 28    if socks_server: 
 29      host, port = socks_server.split(":") 
 30      socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, host, int(port)) 
 31      socket.socket = socks.socksocket 
 32  except ImportError: 
 33    pass 
 34   
 35  import urllib2 
 36   
 37  __docformat__ = "epytext" 
 38   
 39  LOG = logging.getLogger(__name__) 
40 41 -class RestException(Exception):
42 """ 43 Any error result from the Rest API is converted into this exception type. 44 """
45 - def __init__(self, error):
46 Exception.__init__(self, error) 47 self._error = error 48 self._code = None 49 self._message = str(error) 50 # See if there is a code or a message. (For urllib2.HTTPError.) 51 try: 52 self._code = error.code 53 self._message = error.read() 54 except AttributeError: 55 pass
56
57 - def __str__(self):
58 res = self._message or "" 59 if self._code is not None: 60 res += " (error %s)" % (self._code,) 61 return res
62
63 - def get_parent_ex(self):
64 if isinstance(self._error, Exception): 65 return self._error 66 return None
67 68 @property
69 - def code(self):
70 return self._code
71 72 @property
73 - def message(self):
74 return self._message
75
76 77 -class HttpClient(object):
78 """ 79 Basic HTTP client tailored for rest APIs. 80 """
81 - def __init__(self, base_url, exc_class=None, logger=None, ssl_context=None):
82 """ 83 @param base_url: The base url to the API. 84 @param exc_class: An exception class to handle non-200 results. 85 @param ssl_context: A custom SSL context to use for HTTPS (Python 2.7.9+) 86 87 Creates an HTTP(S) client to connect to the Cloudera Manager API. 88 """ 89 self._base_url = base_url.rstrip('/') 90 self._exc_class = exc_class or RestException 91 self._logger = logger or LOG 92 self._headers = { } 93 94 # Make a basic auth handler that does nothing. Set credentials later. 95 self._passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm() 96 authhandler = urllib2.HTTPBasicAuthHandler(self._passmgr) 97 98 # Make a cookie processor 99 cookiejar = cookielib.CookieJar() 100 101 # Python 2.6's HTTPSHandler does not support the context argument, so only 102 # instantiate it if non-None context is given 103 if (ssl_context is not None): 104 self._opener = urllib2.build_opener( 105 urllib2.HTTPSHandler(context=ssl_context), 106 HTTPErrorProcessor(), 107 urllib2.HTTPCookieProcessor(cookiejar), 108 authhandler) 109 else: 110 self._opener = urllib2.build_opener( 111 HTTPErrorProcessor(), 112 urllib2.HTTPCookieProcessor(cookiejar), 113 authhandler)
114
115 - def set_basic_auth(self, username, password, realm):
116 """ 117 Set up basic auth for the client 118 @param username: Login name. 119 @param password: Login password. 120 @param realm: The authentication realm. 121 @return: The current object 122 """ 123 self._passmgr.add_password(realm, self._base_url, username, password) 124 return self
125
126 - def set_headers(self, headers):
127 """ 128 Add headers to the request 129 @param headers: A dictionary with the key value pairs for the headers 130 @return: The current object 131 """ 132 self._headers = headers 133 return self
134 135 136 @property
137 - def base_url(self):
138 return self._base_url
139 140 @property
141 - def logger(self):
142 return self._logger
143
144 - def _get_headers(self, headers):
145 res = self._headers.copy() 146 if headers: 147 res.update(headers) 148 return res
149
150 - def execute(self, http_method, path, params=None, data=None, headers=None):
151 """ 152 Submit an HTTP request. 153 @param http_method: GET, POST, PUT, DELETE 154 @param path: The path of the resource. 155 @param params: Key-value parameter data. 156 @param data: The data to attach to the body of the request. 157 @param headers: The headers to set for this request. 158 159 @return: The result of urllib2.urlopen() 160 """ 161 # Prepare URL and params 162 url = self._make_url(path, params) 163 if http_method in ("GET", "DELETE"): 164 if data is not None: 165 self.logger.warn( 166 "GET method does not pass any data. Path '%s'" % (path,)) 167 data = None 168 169 # Setup the request 170 request = urllib2.Request(url, data) 171 # Hack/workaround because urllib2 only does GET and POST 172 request.get_method = lambda: http_method 173 174 headers = self._get_headers(headers) 175 for k, v in headers.items(): 176 request.add_header(k, v) 177 178 # Call it 179 self.logger.debug("%s %s" % (http_method, url)) 180 try: 181 return self._opener.open(request) 182 except urllib2.HTTPError, ex: 183 raise self._exc_class(ex)
184
185 - def _make_url(self, path, params):
186 res = self._base_url 187 if path: 188 res += posixpath.normpath('/' + path.lstrip('/')) 189 if params: 190 param_str = urllib.urlencode(params, True) 191 res += '?' + param_str 192 return iri_to_uri(res)
193
194 195 -class HTTPErrorProcessor(urllib2.HTTPErrorProcessor):
196 """ 197 Python 2.4 only recognize 200 and 206 as success. It's broken. So we install 198 the following processor to catch the bug. 199 """
200 - def http_response(self, request, response):
201 if 200 <= response.code < 300: 202 return response 203 return urllib2.HTTPErrorProcessor.http_response(self, request, response)
204 205 https_response = http_response
206
207 # 208 # Method copied from Django 209 # 210 -def iri_to_uri(iri):
211 """ 212 Convert an Internationalized Resource Identifier (IRI) portion to a URI 213 portion that is suitable for inclusion in a URL. 214 215 This is the algorithm from section 3.1 of RFC 3987. However, since we are 216 assuming input is either UTF-8 or unicode already, we can simplify things a 217 little from the full method. 218 219 Returns an ASCII string containing the encoded result. 220 """ 221 # The list of safe characters here is constructed from the "reserved" and 222 # "unreserved" characters specified in sections 2.2 and 2.3 of RFC 3986: 223 # reserved = gen-delims / sub-delims 224 # gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" 225 # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 226 # / "*" / "+" / "," / ";" / "=" 227 # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 228 # Of the unreserved characters, urllib.quote already considers all but 229 # the ~ safe. 230 # The % character is also added to the list of safe characters here, as the 231 # end of section 3.1 of RFC 3987 specifically mentions that % must not be 232 # converted. 233 if iri is None: 234 return iri 235 return urllib.quote(smart_str(iri), safe="/#%[]=:;$&()+,!?*@'~")
236
237 # 238 # Method copied from Django 239 # 240 -def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'):
241 """ 242 Returns a bytestring version of 's', encoded as specified in 'encoding'. 243 244 If strings_only is True, don't convert (some) non-string-like objects. 245 """ 246 if strings_only and isinstance(s, (types.NoneType, int)): 247 return s 248 elif not isinstance(s, basestring): 249 try: 250 return str(s) 251 except UnicodeEncodeError: 252 if isinstance(s, Exception): 253 # An Exception subclass containing non-ASCII data that doesn't 254 # know how to print itself properly. We shouldn't raise a 255 # further exception. 256 return ' '.join([smart_str(arg, encoding, strings_only, 257 errors) for arg in s]) 258 return unicode(s).encode(encoding, errors) 259 elif isinstance(s, unicode): 260 return s.encode(encoding, errors) 261 elif s and encoding != 'utf-8': 262 return s.decode('utf-8', errors).encode(encoding, errors) 263 else: 264 return s
265