# Copyright 2012 OpenStack LLC. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # # vim: tabstop=4 shiftwidth=4 softtabstop=4 try: import json except ImportError: import simplejson as json import logging import os import urlparse # Python 2.5 compat fix if not hasattr(urlparse, 'parse_qsl'): import cgi urlparse.parse_qsl = cgi.parse_qsl import httplib2 from neutronclient.common import exceptions from neutronclient.common import utils _logger = logging.getLogger(__name__) if os.environ.get('NEUTRONCLIENT_DEBUG'): ch = logging.StreamHandler() _logger.setLevel(logging.DEBUG) _logger.addHandler(ch) class ServiceCatalog(object): """Helper methods for dealing with a Keystone Service Catalog.""" def __init__(self, resource_dict): self.catalog = resource_dict def get_token(self): """Fetch token details fron service catalog.""" token = {'id': self.catalog['access']['token']['id'], 'expires': self.catalog['access']['token']['expires'], } try: token['user_id'] = self.catalog['access']['user']['id'] token['tenant_id'] = ( self.catalog['access']['token']['tenant']['id']) except Exception: # just leave the tenant and user out if it doesn't exist pass return token def url_for(self, attr=None, filter_value=None, service_type='network', endpoint_type='publicURL'): """Fetch the URL from the Neutron service for a particular endpoint type. If none given, return publicURL. """ catalog = self.catalog['access'].get('serviceCatalog', []) matching_endpoints = [] for service in catalog: if service['type'] != service_type: continue endpoints = service['endpoints'] for endpoint in endpoints: if not filter_value or endpoint.get(attr) == filter_value: matching_endpoints.append(endpoint) if not matching_endpoints: raise exceptions.EndpointNotFound() elif len(matching_endpoints) > 1: raise exceptions.AmbiguousEndpoints(message=matching_endpoints) else: if endpoint_type not in matching_endpoints[0]: raise exceptions.EndpointTypeNotFound(message=endpoint_type) return matching_endpoints[0][endpoint_type] class HTTPClient(httplib2.Http): """Handles the REST calls and responses, include authn.""" USER_AGENT = 'python-neutronclient' def __init__(self, username=None, tenant_name=None, tenant_id=None, password=None, auth_url=None, token=None, region_name=None, timeout=None, endpoint_url=None, insecure=False, endpoint_type='publicURL', auth_strategy='keystone', ca_cert=None, log_credentials=False, **kwargs): super(HTTPClient, self).__init__(timeout=timeout, ca_certs=ca_cert) self.username = username self.tenant_name = tenant_name self.tenant_id = tenant_id self.password = password self.auth_url = auth_url.rstrip('/') if auth_url else None self.endpoint_type = endpoint_type self.region_name = region_name self.auth_token = token self.content_type = 'application/json' self.endpoint_url = endpoint_url self.auth_strategy = auth_strategy self.log_credentials = log_credentials # httplib2 overrides self.disable_ssl_certificate_validation = insecure def _cs_request(self, *args, **kwargs): kargs = {} kargs.setdefault('headers', kwargs.get('headers', {})) kargs['headers']['User-Agent'] = self.USER_AGENT if 'content_type' in kwargs: kargs['headers']['Content-Type'] = kwargs['content_type'] kargs['headers']['Accept'] = kwargs['content_type'] else: kargs['headers']['Content-Type'] = self.content_type kargs['headers']['Accept'] = self.content_type if 'body' in kwargs: kargs['body'] = kwargs['body'] args = utils.safe_encode_list(args) kargs = utils.safe_encode_dict(kargs) if self.log_credentials: log_kargs = kargs else: log_kargs = self._strip_credentials(kargs) utils.http_log_req(_logger, args, log_kargs) try: resp, body = self.request(*args, **kargs) except httplib2.SSLHandshakeError as e: raise exceptions.SslCertificateValidationError(reason=e) except Exception as e: # Wrap the low-level connection error (socket timeout, redirect # limit, decompression error, etc) into our custom high-level # connection exception (it is excepted in the upper layers of code) raise exceptions.ConnectionFailed(reason=e) utils.http_log_resp(_logger, resp, body) status_code = self.get_status_code(resp) if status_code == 401: raise exceptions.Unauthorized(message=body) elif status_code == 403: raise exceptions.Forbidden(message=body) return resp, body def _strip_credentials(self, kwargs): if kwargs.get('body') and self.password: log_kwargs = kwargs.copy() log_kwargs['body'] = kwargs['body'].replace(self.password, 'REDACTED') return log_kwargs else: return kwargs def authenticate_and_fetch_endpoint_url(self): if not self.auth_token: self.authenticate() elif not self.endpoint_url: self.endpoint_url = self._get_endpoint_url() def do_request(self, url, method, **kwargs): self.authenticate_and_fetch_endpoint_url() # 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', {}) kwargs['headers']['X-Auth-Token'] = self.auth_token resp, body = self._cs_request(self.endpoint_url + url, method, **kwargs) return resp, body except exceptions.Unauthorized: self.authenticate() kwargs.setdefault('headers', {}) kwargs['headers']['X-Auth-Token'] = self.auth_token resp, body = self._cs_request( self.endpoint_url + url, method, **kwargs) return resp, body def _extract_service_catalog(self, body): """Set the client's service catalog from the response data.""" self.service_catalog = ServiceCatalog(body) try: sc = self.service_catalog.get_token() self.auth_token = sc['id'] self.auth_tenant_id = sc.get('tenant_id') self.auth_user_id = sc.get('user_id') except KeyError: raise exceptions.Unauthorized() self.endpoint_url = self.service_catalog.url_for( attr='region', filter_value=self.region_name, endpoint_type=self.endpoint_type) def authenticate(self): if self.auth_strategy != 'keystone': raise exceptions.Unauthorized(message='unknown auth strategy') if self.tenant_id: body = {'auth': {'passwordCredentials': {'username': self.username, 'password': self.password, }, 'tenantId': self.tenant_id, }, } else: body = {'auth': {'passwordCredentials': {'username': self.username, 'password': self.password, }, 'tenantName': self.tenant_name, }, } token_url = self.auth_url + "/tokens" # Make sure we follow redirects when trying to reach Keystone tmp_follow_all_redirects = self.follow_all_redirects self.follow_all_redirects = True try: resp, body = self._cs_request(token_url, "POST", body=json.dumps(body), content_type="application/json") finally: self.follow_all_redirects = tmp_follow_all_redirects status_code = self.get_status_code(resp) if status_code != 200: raise exceptions.Unauthorized(message=body) if body: try: body = json.loads(body) except ValueError: pass else: body = None self._extract_service_catalog(body) def _get_endpoint_url(self): url = self.auth_url + '/tokens/%s/endpoints' % self.auth_token try: resp, body = self._cs_request(url, "GET") except exceptions.Unauthorized: # rollback to authenticate() to handle case when neutron client # is initialized just before the token is expired self.authenticate() return self.endpoint_url body = json.loads(body) for endpoint in body.get('endpoints', []): if (endpoint['type'] == 'network' and endpoint.get('region') == self.region_name): if self.endpoint_type not in endpoint: raise exceptions.EndpointTypeNotFound( message=self.endpoint_type) return endpoint[self.endpoint_type] raise exceptions.EndpointNotFound() def get_auth_info(self): return {'auth_token': self.auth_token, 'auth_tenant_id': self.auth_tenant_id, 'auth_user_id': self.auth_user_id, 'endpoint_url': self.endpoint_url} def get_status_code(self, response): """Returns the integer status code from the response. Either a Webob.Response (used in testing) or httplib.Response is returned. """ if hasattr(response, 'status_int'): return response.status_int else: return response.status