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