1"""Library to make sso_client wrapped HTTP requests to skylab service.""" 2 3from apiclient import discovery 4import httplib2 5import logging 6import re 7import subprocess 8 9 10CODE_REGEX = '\d\d\d' 11LINE_SEP = '\r\n' # The line separator used in http response. 12DEFAULT_FETCH_DEADLINE_SEC = 60 13 14 15class BadHttpResponseException(Exception): 16 """Raise when fail to parse the HTTP response.""" 17 pass 18 19 20class BadHttpRequestException(Exception): 21 """Raise when the sso_client based http request failed.""" 22 pass 23 24 25def retry_if_bad_request(exception): 26 """Return True if we should retry, False otherwise""" 27 return isinstance(exception, BadHttpRequestException) 28 29def sso_request(url, method='', body='', headers={}, max_redirects=None): 30 """Create sso_client wrapped http request. 31 32 @param url: String of the URL to request. 33 @param method: The HTTP method to get use, e.g. GET or DELETE. Use POST if 34 there is data, GET otherwise. Default is "". If not specified, 35 GET is the default method for sso_client command. 36 @param body: The data used by POST/UPDATE method. String, default is "". 37 @param headers: Dict of the request headers, default is {}. 38 @param max_redirects: String format of integer representing the maximum 39 redirects. If not specified, sso_client uses 10 as 40 default. 41 42 @returns: The return value is a tuple of (response, content), the first 43 being and instance of the 'Response' class, the second being 44 a string that contains the response entity body. 45 """ 46 try: 47 cmd = ['sso_client', '--url', url, '-dump_header'] 48 if method: 49 cmd.extend(['-method', method]) 50 if body: 51 cmd.extend(['-data', body]) 52 if headers: 53 # Remove the accept-encoding header to disable encoding in order to 54 # receive the raw text. 55 headers.pop('accept-encoding', None) 56 headers_str = ['%s:%s' % (k, v) for k, v in headers.iteritems()] 57 headers_str = ';'.join(headers_str) 58 if headers_str: 59 cmd.extend(['-headers', headers_str]) 60 if max_redirects: 61 cmd.extend(['-max_redirects', max_redirects]) 62 63 logging.debug('Sending SSO request: %s', cmd) 64 result = subprocess.check_output(cmd) 65 except subprocess.CalledProcessError as e: 66 error_msg = ('Fail to make the sso_client request to %s.\nError:\n%s' % 67 (url, e.output)) 68 raise BadHttpRequestException(error_msg) 69 70 result = result.strip() 71 if result.find(LINE_SEP+LINE_SEP) == -1: 72 (headers, body) = (result, '') 73 else: 74 [headers, body] = [s.strip() for s in result.split(LINE_SEP+LINE_SEP)] 75 status = re.search(CODE_REGEX, headers.split(LINE_SEP)[0]) 76 if not status: 77 raise BadHttpResponseException( 78 'Fail to parse the status return code from the HTTP response:\n%s' % 79 result) 80 status = status.group(0) 81 info = {'status': status, 'body': body, 'headers': headers} 82 return (httplib2.Response(info), body) 83 84 85def _new_http(include_sso=True): 86 """Creates httplib2 client that adds SSO cookie.""" 87 http = httplib2.Http(timeout=DEFAULT_FETCH_DEADLINE_SEC) 88 if include_sso: 89 http.request = sso_request 90 91 return http 92 93 94def build_service(service_name, 95 version, 96 discovery_service_url=discovery.DISCOVERY_URI, 97 include_sso=True): 98 """Construct a service wrapped with SSO credentials as required.""" 99 logging.debug('Requesting discovery service for url: %s', 100 discovery_service_url) 101 return discovery.build( 102 service_name, 103 version, 104 http=_new_http(include_sso), 105 discoveryServiceUrl=discovery_service_url) 106 107