# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 Piston Cloud Computing, Inc. """ OpenStack Client interface. Handles the REST calls and responses. """ import httplib2 import logging import os import time import urllib import urlparse from novaclient import service_catalog try: import json except ImportError: import simplejson as json # Python 2.5 compat fix if not hasattr(urlparse, 'parse_qsl'): import cgi urlparse.parse_qsl = cgi.parse_qsl from novaclient import exceptions _logger = logging.getLogger(__name__) class HTTPClient(httplib2.Http): USER_AGENT = 'python-novaclient' def __init__(self, user, apikey, projectid, auth_url, timeout=None, token=None, region_name=None): super(HTTPClient, self).__init__(timeout=timeout) self.user = user self.apikey = apikey self.projectid = projectid self.auth_url = auth_url self.version = 'v1.0' self.region_name = region_name self.management_url = None self.auth_token = None self.proxy_token = token # httplib2 overrides self.force_exception_to_status_code = True def http_log(self, args, kwargs, resp, body): if 'NOVACLIENT_DEBUG' in os.environ and os.environ['NOVACLIENT_DEBUG']: ch = logging.StreamHandler() _logger.setLevel(logging.DEBUG) _logger.addHandler(ch) elif not _logger.isEnabledFor(logging.DEBUG): return string_parts = ['curl -i'] for element in args: if element in ('GET', 'POST'): string_parts.append(' -X %s' % element) else: string_parts.append(' %s' % element) for element in kwargs['headers']: header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) string_parts.append(header) _logger.debug("REQ: %s\n" % "".join(string_parts)) if 'body' in kwargs: _logger.debug("REQ BODY: %s\n" % (kwargs['body'])) _logger.debug("RESP:%s %s\n", resp, body) def request(self, *args, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) kwargs['headers']['User-Agent'] = self.USER_AGENT if 'body' in kwargs: kwargs['headers']['Content-Type'] = 'application/json' kwargs['body'] = json.dumps(kwargs['body']) resp, body = super(HTTPClient, self).request(*args, **kwargs) self.http_log(args, kwargs, resp, body) if body: try: body = json.loads(body) except ValueError, e: pass else: body = None if resp.status in (400, 401, 403, 404, 408, 413, 500, 501): raise exceptions.from_response(resp, body) return resp, body def _cs_request(self, url, method, **kwargs): if not self.management_url: self.authenticate() # Perform the request once. If we get a 401 back then it # might be because the auth token expired, so try to # re-authenticate and try again. If it still fails, bail. try: kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token if self.projectid: kwargs['headers']['X-Auth-Project-Id'] = self.projectid resp, body = self.request(self.management_url + url, method, **kwargs) return resp, body except exceptions.Unauthorized, ex: try: self.authenticate() resp, body = self.request(self.management_url + url, method, **kwargs) return resp, body except exceptions.Unauthorized: raise ex def get(self, url, **kwargs): url = self._munge_get_url(url) return self._cs_request(url, 'GET', **kwargs) def post(self, url, **kwargs): return self._cs_request(url, 'POST', **kwargs) def put(self, url, **kwargs): return self._cs_request(url, 'PUT', **kwargs) def delete(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) def _extract_service_catalog(self, url, resp, body): """See what the auth service told us and process the response. We may get redirected to another site, fail or actually get back a service catalog with a token and our endpoints.""" if resp.status == 200: # content must always present try: self.auth_url = url self.service_catalog = \ service_catalog.ServiceCatalog(body) self.auth_token = self.service_catalog.token.id _logger.debug("***** REGION_NAME: %s" % self.region_name) self.management_url = self.service_catalog.url_for( 'nova', 'public', attr='region', filter_value=self.region_name) _logger.debug("***** MANAGEMENT URL: %s" % self.management_url) return None except KeyError: raise exceptions.AuthorizationFailure() elif resp.status == 305: return resp['location'] else: raise exceptions.from_response(resp, body) def _fetch_endpoints_from_auth(self, url): """We have a token, but don't know the final endpoint for the region. We have to go back to the auth service and ask again. This request requires an admin-level token to work. The proxy token supplied could be from a low-level enduser. This will overwrite our admin token with the user token. """ # GET /v2.0/tokens/#####/endpoints token_url = urlparse.urljoin(url, "tokens", self.proxy_token, "endpoints") resp, body = self.request(token_url, "GET", headers={'X-Auth_Token': self.auth_token}, body=body) return self._extract_service_catalog(url, resp, body) def authenticate(self): scheme, netloc, path, query, frag = urlparse.urlsplit( self.auth_url) path_parts = path.split('/') for part in path_parts: if len(part) > 0 and part[0] == 'v': self.version = part break auth_url = self.auth_url if self.version == "v2.0": # FIXME(chris): This should be better. while auth_url: auth_url = self._v2_auth(auth_url) # Are we acting on behalf of another user via an # existing token? If so, our actual endpoints may # be different than that of the admin token. if self.proxy_token: return self._fetch_endpoints_from_auth() else: try: while auth_url: auth_url = self._v1_auth(auth_url) # In some configurations nova makes redirection to # v2.0 keystone endpoint. Also, new location does not contain # real endpoint, only hostname and port. except exceptions.AuthorizationFailure: if auth_url.find('v2.0') < 0: auth_url = urlparse.urljoin(auth_url, 'v2.0/') self._v2_auth(auth_url) def _v1_auth(self, url): if self.proxy_token: raise NoTokenLookupException() headers = {'X-Auth-User': self.user, 'X-Auth-Key': self.apikey} if self.projectid: headers['X-Auth-Project-Id'] = self.projectid resp, body = self.request(url, 'GET', headers=headers) if resp.status in (200, 204): # in some cases we get No Content try: self.management_url = resp['x-server-management-url'] self.auth_token = resp['x-auth-token'] self.auth_url = url except KeyError: raise exceptions.AuthorizationFailure() elif resp.status == 305: return resp['location'] else: raise exceptions.from_response(resp, body) def _v2_auth(self, url): """Authenticate against a v2.0 auth service.""" body = {"passwordCredentials": {"username": self.user, "password": self.apikey}} if self.projectid: body['passwordCredentials']['tenantId'] = self.projectid token_url = urlparse.urljoin(url, "tokens") resp, body = self.request(token_url, "POST", body=body) return self._extract_service_catalog(url, resp, body) def _munge_get_url(self, url): """ Munge GET URLs to always return uncached content. The OpenStack Compute API caches data *very* agressively and doesn't respect cache headers. To avoid stale data, then, we append a little bit of nonsense onto GET parameters; this appears to force the data not to be cached. """ scheme, netloc, path, query, frag = urlparse.urlsplit(url) query = urlparse.parse_qsl(query) query.append(('fresh', str(time.time()))) query = urllib.urlencode(query) return urlparse.urlunsplit((scheme, netloc, path, query, frag))