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