1import copy, getpass, logging, pprint, re, urllib, urlparse 2import httplib2 3from django.utils import datastructures, simplejson 4from autotest_lib.frontend.afe import rpc_client_lib 5from autotest_lib.client.common_lib import utils 6 7 8_request_headers = {} 9 10 11def _get_request_headers(uri): 12 server = urlparse.urlparse(uri)[0:2] 13 if server in _request_headers: 14 return _request_headers[server] 15 16 headers = rpc_client_lib.authorization_headers(getpass.getuser(), uri) 17 headers['Content-Type'] = 'application/json' 18 19 _request_headers[server] = headers 20 return headers 21 22 23def _clear_request_headers(uri): 24 server = urlparse.urlparse(uri)[0:2] 25 if server in _request_headers: 26 del _request_headers[server] 27 28 29def _site_verify_response_default(headers, response_body): 30 return headers['status'] != '401' 31 32 33class RestClientError(Exception): 34 pass 35 36 37class ClientError(Exception): 38 pass 39 40 41class ServerError(Exception): 42 pass 43 44 45class Response(object): 46 def __init__(self, httplib_response, httplib_content): 47 self.status = int(httplib_response['status']) 48 self.headers = httplib_response 49 self.entity_body = httplib_content 50 51 52 def decoded_body(self): 53 return simplejson.loads(self.entity_body) 54 55 56 def __str__(self): 57 return '\n'.join([str(self.status), self.entity_body]) 58 59 60class Resource(object): 61 def __init__(self, representation_dict, http): 62 self._http = http 63 assert 'href' in representation_dict 64 for key, value in representation_dict.iteritems(): 65 setattr(self, str(key), value) 66 67 68 def __repr__(self): 69 return 'Resource(%r)' % self._representation() 70 71 72 def pprint(self): 73 # pretty-print support for debugging/interactive use 74 pprint.pprint(self._representation()) 75 76 77 @classmethod 78 def load(cls, uri, http=None): 79 if not http: 80 http = httplib2.Http() 81 directory = cls({'href': uri}, http) 82 return directory.get() 83 84 85 def _read_representation(self, value): 86 # recursively convert representation dicts to Resource objects 87 if isinstance(value, list): 88 return [self._read_representation(element) for element in value] 89 if isinstance(value, dict): 90 converted_dict = dict((key, self._read_representation(sub_value)) 91 for key, sub_value in value.iteritems()) 92 if 'href' in converted_dict: 93 return type(self)(converted_dict, http=self._http) 94 return converted_dict 95 return value 96 97 98 def _write_representation(self, value): 99 # recursively convert Resource objects to representation dicts 100 if isinstance(value, list): 101 return [self._write_representation(element) for element in value] 102 if isinstance(value, dict): 103 return dict((key, self._write_representation(sub_value)) 104 for key, sub_value in value.iteritems()) 105 if isinstance(value, Resource): 106 return value._representation() 107 return value 108 109 110 def _representation(self): 111 return dict((key, self._write_representation(value)) 112 for key, value in self.__dict__.iteritems() 113 if not key.startswith('_') 114 and not callable(value)) 115 116 117 def _do_request(self, method, uri, query_parameters, encoded_body): 118 uri_parts = [uri] 119 if query_parameters: 120 if '?' in uri: 121 uri_parts += '&' 122 else: 123 uri_parts += '?' 124 uri_parts += urllib.urlencode(query_parameters, doseq=True) 125 full_uri = ''.join(uri_parts) 126 127 if encoded_body: 128 entity_body = simplejson.dumps(encoded_body) 129 else: 130 entity_body = None 131 132 logging.debug('%s %s', method, full_uri) 133 if entity_body: 134 logging.debug(entity_body) 135 136 site_verify = utils.import_site_function( 137 __file__, 'autotest_lib.frontend.shared.site_rest_client', 138 'site_verify_response', _site_verify_response_default) 139 headers, response_body = self._http.request( 140 full_uri, method, body=entity_body, 141 headers=_get_request_headers(uri)) 142 if not site_verify(headers, response_body): 143 logging.debug('Response verification failed, clearing headers and ' 144 'trying again:\n%s', response_body) 145 _clear_request_headers(uri) 146 headers, response_body = self._http.request( 147 full_uri, method, body=entity_body, 148 headers=_get_request_headers(uri)) 149 150 logging.debug('Response: %s', headers['status']) 151 152 return Response(headers, response_body) 153 154 155 def _request(self, method, query_parameters=None, encoded_body=None): 156 if query_parameters is None: 157 query_parameters = {} 158 159 response = self._do_request(method, self.href, query_parameters, 160 encoded_body) 161 162 if 300 <= response.status < 400: # redirection 163 return self._do_request(method, response.headers['location'], 164 query_parameters, encoded_body) 165 if 400 <= response.status < 500: 166 raise ClientError(str(response)) 167 if 500 <= response.status < 600: 168 raise ServerError(str(response)) 169 return response 170 171 172 def _stringify_query_parameter(self, value): 173 if isinstance(value, (list, tuple)): 174 return ','.join(self._stringify_query_parameter(item) 175 for item in value) 176 return str(value) 177 178 179 def _iterlists(self, mapping): 180 """This effectively lets us treat dicts as MultiValueDicts.""" 181 if hasattr(mapping, 'iterlists'): # mapping is already a MultiValueDict 182 return mapping.iterlists() 183 return ((key, (value,)) for key, value in mapping.iteritems()) 184 185 186 def get(self, query_parameters=None, **kwarg_query_parameters): 187 """ 188 @param query_parameters: a dict or MultiValueDict 189 """ 190 query_parameters = copy.copy(query_parameters) # avoid mutating original 191 if query_parameters is None: 192 query_parameters = {} 193 query_parameters.update(kwarg_query_parameters) 194 195 string_parameters = datastructures.MultiValueDict() 196 for key, values in self._iterlists(query_parameters): 197 string_parameters.setlist( 198 key, [self._stringify_query_parameter(value) 199 for value in values]) 200 201 response = self._request('GET', 202 query_parameters=string_parameters.lists()) 203 assert response.status == 200 204 return self._read_representation(response.decoded_body()) 205 206 207 def get_full(self, results_limit, query_parameters=None, 208 **kwarg_query_parameters): 209 """ 210 Like get() for collections, when the full collection is expected. 211 212 @param results_limit: maxmimum number of results to allow 213 @raises ClientError if there are more than results_limit results. 214 """ 215 result = self.get(query_parameters=query_parameters, 216 items_per_page=results_limit, 217 **kwarg_query_parameters) 218 if result.total_results > results_limit: 219 raise ClientError( 220 'Too many results (%s > %s) for request %s (%s %s)' 221 % (result.total_results, results_limit, self.href, 222 query_parameters, kwarg_query_parameters)) 223 return result 224 225 226 227 def put(self): 228 response = self._request('PUT', encoded_body=self._representation()) 229 assert response.status == 200 230 return self._read_representation(response.decoded_body()) 231 232 233 def delete(self): 234 response = self._request('DELETE') 235 assert response.status == 204 # no content 236 237 238 def post(self, request_dict): 239 # request_dict may still have resources in it 240 request_dict = self._write_representation(request_dict) 241 response = self._request('POST', encoded_body=request_dict) 242 assert response.status == 201 # created 243 return self._read_representation({'href': response.headers['location']}) 244