289 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			289 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# Copyright 2012 OpenStack Foundation.
 | 
						|
# 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)
 | 
						|
        finally:
 | 
						|
            # Temporary Fix for gate failures. RPC calls and HTTP requests
 | 
						|
            # seem to be stepping on each other resulting in bogus fd's being
 | 
						|
            # picked up for making http requests
 | 
						|
            self.connections.clear()
 | 
						|
        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()
 | 
						|
        if not self.endpoint_url:
 | 
						|
            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
 |