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, timeout=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 @param timeout: The connection timeout in seconds to use for requests 87 88 Creates an HTTP(S) client to connect to the Cloudera Manager API. 89 """ 90 self._base_url = base_url.rstrip('/') 91 self._exc_class = exc_class or RestException 92 self._logger = logger or LOG 93 self._headers = { } 94 self._timeout = timeout 95 96 # Make a basic auth handler that does nothing. Set credentials later. 97 self._passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm() 98 authhandler = urllib2.HTTPBasicAuthHandler(self._passmgr) 99 100 # Make a cookie processor 101 cookiejar = cookielib.CookieJar() 102 103 # Python 2.6's HTTPSHandler does not support the context argument, so only 104 # instantiate it if non-None context is given 105 if (ssl_context is not None): 106 self._opener = urllib2.build_opener( 107 urllib2.HTTPSHandler(context=ssl_context), 108 HTTPErrorProcessor(), 109 urllib2.HTTPCookieProcessor(cookiejar), 110 authhandler) 111 else: 112 self._opener = urllib2.build_opener( 113 HTTPErrorProcessor(), 114 urllib2.HTTPCookieProcessor(cookiejar), 115 authhandler)
116
117 - def set_basic_auth(self, username, password, realm):
118 """ 119 Set up basic auth for the client 120 @param username: Login name. 121 @param password: Login password. 122 @param realm: The authentication realm. 123 @return: The current object 124 """ 125 self._passmgr.add_password(realm, self._base_url, username, password) 126 return self
127
128 - def set_headers(self, headers):
129 """ 130 Add headers to the request 131 @param headers: A dictionary with the key value pairs for the headers 132 @return: The current object 133 """ 134 self._headers = headers 135 return self
136 137 138 @property
139 - def base_url(self):
140 return self._base_url
141 142 @property
143 - def logger(self):
144 return self._logger
145
146 - def _get_headers(self, headers):
147 res = self._headers.copy() 148 if headers: 149 res.update(headers) 150 return res
151
152 - def execute(self, http_method, path, params=None, data=None, headers=None):
153 """ 154 Submit an HTTP request. 155 @param http_method: GET, POST, PUT, DELETE 156 @param path: The path of the resource. 157 @param params: Key-value parameter data. 158 @param data: The data to attach to the body of the request. 159 @param headers: The headers to set for this request. 160 161 @return: The result of urllib2.urlopen() 162 """ 163 # Prepare URL and params 164 url = self._make_url(path, params) 165 if http_method in ("GET", "DELETE"): 166 if data is not None: 167 self.logger.warn( 168 "GET method does not pass any data. Path '%s'" % (path,)) 169 data = None 170 171 # Setup the request 172 request = urllib2.Request(url, data) 173 # Hack/workaround because urllib2 only does GET and POST 174 request.get_method = lambda: http_method 175 176 headers = self._get_headers(headers) 177 for k, v in headers.items(): 178 request.add_header(k, v) 179 180 # Call it 181 self.logger.debug("%s %s" % (http_method, url)) 182 try: 183 if self._timeout is not None: 184 try: 185 return self._opener.open(request, timeout=self._timeout) 186 except TypeError, tex: 187 # 'timeout' param available since python 2.6. 188 # So dropping timeout param for python < 2.6 189 return self._opener.open(request) 190 else: 191 return self._opener.open(request) 192 except urllib2.HTTPError, ex: 193 raise self._exc_class(ex)
194
195 - def _make_url(self, path, params):
196 res = self._base_url 197 if path: 198 res += posixpath.normpath('/' + path.lstrip('/')) 199 if params: 200 param_str = urllib.urlencode(params, True) 201 res += '?' + param_str 202 return iri_to_uri(res)
203
204 205 -class HTTPErrorProcessor(urllib2.HTTPErrorProcessor):
206 """ 207 Python 2.4 only recognize 200 and 206 as success. It's broken. So we install 208 the following processor to catch the bug. 209 """
210 - def http_response(self, request, response):
211 if 200 <= response.code < 300: 212 return response 213 return urllib2.HTTPErrorProcessor.http_response(self, request, response)
214 215 https_response = http_response
216
217 # 218 # Method copied from Django 219 # 220 -def iri_to_uri(iri):
221 """ 222 Convert an Internationalized Resource Identifier (IRI) portion to a URI 223 portion that is suitable for inclusion in a URL. 224 225 This is the algorithm from section 3.1 of RFC 3987. However, since we are 226 assuming input is either UTF-8 or unicode already, we can simplify things a 227 little from the full method. 228 229 Returns an ASCII string containing the encoded result. 230 """ 231 # The list of safe characters here is constructed from the "reserved" and 232 # "unreserved" characters specified in sections 2.2 and 2.3 of RFC 3986: 233 # reserved = gen-delims / sub-delims 234 # gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" 235 # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 236 # / "*" / "+" / "," / ";" / "=" 237 # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 238 # Of the unreserved characters, urllib.quote already considers all but 239 # the ~ safe. 240 # The % character is also added to the list of safe characters here, as the 241 # end of section 3.1 of RFC 3987 specifically mentions that % must not be 242 # converted. 243 if iri is None: 244 return iri 245 return urllib.quote(smart_str(iri), safe="/#%[]=:;$&()+,!?*@'~")
246
247 # 248 # Method copied from Django 249 # 250 -def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'):
251 """ 252 Returns a bytestring version of 's', encoded as specified in 'encoding'. 253 254 If strings_only is True, don't convert (some) non-string-like objects. 255 """ 256 if strings_only and isinstance(s, (types.NoneType, int)): 257 return s 258 elif not isinstance(s, basestring): 259 try: 260 return str(s) 261 except UnicodeEncodeError: 262 if isinstance(s, Exception): 263 # An Exception subclass containing non-ASCII data that doesn't 264 # know how to print itself properly. We shouldn't raise a 265 # further exception. 266 return ' '.join([smart_str(arg, encoding, strings_only, 267 errors) for arg in s]) 268 return unicode(s).encode(encoding, errors) 269 elif isinstance(s, unicode): 270 return s.encode(encoding, errors) 271 elif s and encoding != 'utf-8': 272 return s.decode('utf-8', errors).encode(encoding, errors) 273 else: 274 return s
275