3140fe014a
Change-Id: I00065293daf6b2e5d540e056c05f85b82f8bc72c
440 lines
16 KiB
Python
440 lines
16 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.
|
|
#
|
|
|
|
import logging
|
|
import os
|
|
|
|
import debtcollector.renames
|
|
from keystoneauth1 import access
|
|
from keystoneauth1 import adapter
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import importutils
|
|
import requests
|
|
|
|
from neutronclient._i18n import _
|
|
from neutronclient.common import exceptions
|
|
from neutronclient.common import utils
|
|
|
|
osprofiler_web = importutils.try_import("osprofiler.web")
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
if os.environ.get('NEUTRONCLIENT_DEBUG'):
|
|
ch = logging.StreamHandler()
|
|
_logger.setLevel(logging.DEBUG)
|
|
_logger.addHandler(ch)
|
|
_requests_log_level = logging.DEBUG
|
|
else:
|
|
_requests_log_level = logging.WARNING
|
|
|
|
logging.getLogger("requests").setLevel(_requests_log_level)
|
|
MAX_URI_LEN = 8192
|
|
USER_AGENT = 'python-neutronclient'
|
|
REQ_ID_HEADER = 'X-OpenStack-Request-ID'
|
|
|
|
|
|
class HTTPClient(object):
|
|
"""Handles the REST calls and responses, include authn."""
|
|
|
|
CONTENT_TYPE = 'application/json'
|
|
|
|
@debtcollector.renames.renamed_kwarg(
|
|
'tenant_id', 'project_id', replace=True)
|
|
@debtcollector.renames.renamed_kwarg(
|
|
'tenant_name', 'project_name', replace=True)
|
|
def __init__(self, username=None, user_id=None,
|
|
project_name=None, project_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, cert=None,
|
|
log_credentials=False, service_type='network',
|
|
global_request_id=None, **kwargs):
|
|
|
|
self.username = username
|
|
self.user_id = user_id
|
|
self.project_name = project_name
|
|
self.project_id = project_id
|
|
self.password = password
|
|
self.auth_url = auth_url.rstrip('/') if auth_url else None
|
|
self.service_type = service_type
|
|
self.endpoint_type = endpoint_type
|
|
self.region_name = region_name
|
|
self.timeout = timeout
|
|
self.auth_token = token
|
|
self.auth_tenant_id = None
|
|
self.auth_user_id = None
|
|
self.endpoint_url = endpoint_url
|
|
self.auth_strategy = auth_strategy
|
|
self.log_credentials = log_credentials
|
|
self.global_request_id = global_request_id
|
|
self.cert = cert
|
|
if insecure:
|
|
self.verify_cert = False
|
|
else:
|
|
self.verify_cert = ca_cert if ca_cert else True
|
|
|
|
def _cs_request(self, *args, **kwargs):
|
|
kargs = {}
|
|
kargs.setdefault('headers', kwargs.get('headers', {}))
|
|
kargs['headers']['User-Agent'] = USER_AGENT
|
|
|
|
if 'body' in kwargs:
|
|
kargs['body'] = kwargs['body']
|
|
|
|
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 requests.exceptions.SSLError as e:
|
|
raise exceptions.SslCertificateValidationError(reason=str(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)
|
|
_logger.debug("throwing ConnectionFailed : %s", e)
|
|
raise exceptions.ConnectionFailed(reason=str(e))
|
|
utils.http_log_resp(_logger, resp, body)
|
|
|
|
# log request-id for each api call
|
|
request_id = resp.headers.get('x-openstack-request-id')
|
|
if request_id:
|
|
_logger.debug('%(method)s call to neutron for '
|
|
'%(url)s used request id '
|
|
'%(response_request_id)s',
|
|
{'method': resp.request.method,
|
|
'url': resp.url,
|
|
'response_request_id': request_id})
|
|
|
|
if resp.status_code == 401:
|
|
raise exceptions.Unauthorized(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 request(self, url, method, body=None, headers=None, **kwargs):
|
|
"""Request without authentication."""
|
|
|
|
content_type = kwargs.pop('content_type', None) or 'application/json'
|
|
headers = headers or {}
|
|
headers.setdefault('Accept', content_type)
|
|
|
|
if body:
|
|
headers.setdefault('Content-Type', content_type)
|
|
|
|
if self.global_request_id:
|
|
headers.setdefault(REQ_ID_HEADER, self.global_request_id)
|
|
|
|
headers['User-Agent'] = USER_AGENT
|
|
# NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any
|
|
# headers in case if osprofiler is not initialized.
|
|
if osprofiler_web:
|
|
headers.update(osprofiler_web.get_trace_id_headers())
|
|
|
|
resp = requests.request(
|
|
method,
|
|
url,
|
|
data=body,
|
|
headers=headers,
|
|
verify=self.verify_cert,
|
|
cert=self.cert,
|
|
timeout=self.timeout,
|
|
**kwargs)
|
|
|
|
return resp, resp.text
|
|
|
|
def _check_uri_length(self, action):
|
|
uri_len = len(self.endpoint_url) + len(action)
|
|
if uri_len > MAX_URI_LEN:
|
|
raise exceptions.RequestURITooLong(
|
|
excess=uri_len - MAX_URI_LEN)
|
|
|
|
def do_request(self, url, method, **kwargs):
|
|
# Ensure client always has correct uri - do not guesstimate anything
|
|
self.authenticate_and_fetch_endpoint_url()
|
|
self._check_uri_length(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['headers'] = kwargs.get('headers') or {}
|
|
if self.auth_token is None:
|
|
self.auth_token = ""
|
|
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['headers'] = kwargs.get('headers') or {}
|
|
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.auth_ref = access.create(body=body)
|
|
self.service_catalog = self.auth_ref.service_catalog
|
|
self.auth_token = self.auth_ref.auth_token
|
|
self.auth_tenant_id = self.auth_ref.tenant_id
|
|
self.auth_user_id = self.auth_ref.user_id
|
|
|
|
if not self.endpoint_url:
|
|
self.endpoint_url = self.service_catalog.url_for(
|
|
region_name=self.region_name,
|
|
service_type=self.service_type,
|
|
interface=self.endpoint_type)
|
|
|
|
def _authenticate_keystone(self):
|
|
if self.user_id:
|
|
creds = {'userId': self.user_id,
|
|
'password': self.password}
|
|
else:
|
|
creds = {'username': self.username,
|
|
'password': self.password}
|
|
|
|
if self.project_id:
|
|
body = {'auth': {'passwordCredentials': creds,
|
|
'tenantId': self.project_id, }, }
|
|
else:
|
|
body = {'auth': {'passwordCredentials': creds,
|
|
'tenantName': self.project_name, }, }
|
|
|
|
if self.auth_url is None:
|
|
raise exceptions.NoAuthURLProvided()
|
|
|
|
token_url = self.auth_url + "/tokens"
|
|
resp, resp_body = self._cs_request(token_url, "POST",
|
|
body=jsonutils.dumps(body),
|
|
content_type="application/json",
|
|
allow_redirects=True)
|
|
if resp.status_code != 200:
|
|
raise exceptions.Unauthorized(message=resp_body)
|
|
if resp_body:
|
|
try:
|
|
resp_body = jsonutils.loads(resp_body)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
resp_body = None
|
|
self._extract_service_catalog(resp_body)
|
|
|
|
def _authenticate_noauth(self):
|
|
if not self.endpoint_url:
|
|
message = _('For "noauth" authentication strategy, the endpoint '
|
|
'must be specified either in the constructor or '
|
|
'using --os-url')
|
|
raise exceptions.Unauthorized(message=message)
|
|
|
|
def authenticate(self):
|
|
if self.auth_strategy == 'keystone':
|
|
self._authenticate_keystone()
|
|
elif self.auth_strategy == 'noauth':
|
|
self._authenticate_noauth()
|
|
else:
|
|
err_msg = _('Unknown auth strategy: %s') % self.auth_strategy
|
|
raise exceptions.Unauthorized(message=err_msg)
|
|
|
|
def _get_endpoint_url(self):
|
|
if self.auth_url is None:
|
|
raise exceptions.NoAuthURLProvided()
|
|
|
|
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 = jsonutils.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(
|
|
type_=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_auth_ref(self):
|
|
return getattr(self, 'auth_ref', None)
|
|
|
|
|
|
class SessionClient(adapter.Adapter):
|
|
|
|
def request(self, *args, **kwargs):
|
|
kwargs.setdefault('authenticated', False)
|
|
kwargs.setdefault('raise_exc', False)
|
|
|
|
content_type = kwargs.pop('content_type', None) or 'application/json'
|
|
|
|
headers = kwargs.get('headers') or {}
|
|
headers.setdefault('Accept', content_type)
|
|
|
|
# NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any
|
|
# headers in case if osprofiler is not initialized.
|
|
if osprofiler_web:
|
|
headers.update(osprofiler_web.get_trace_id_headers())
|
|
|
|
try:
|
|
kwargs.setdefault('data', kwargs.pop('body'))
|
|
except KeyError:
|
|
pass
|
|
|
|
if kwargs.get('data'):
|
|
headers.setdefault('Content-Type', content_type)
|
|
|
|
kwargs['headers'] = headers
|
|
resp = super(SessionClient, self).request(*args, **kwargs)
|
|
return resp, resp.text
|
|
|
|
def _check_uri_length(self, url):
|
|
uri_len = len(self.endpoint_url) + len(url)
|
|
if uri_len > MAX_URI_LEN:
|
|
raise exceptions.RequestURITooLong(
|
|
excess=uri_len - MAX_URI_LEN)
|
|
|
|
def do_request(self, url, method, **kwargs):
|
|
kwargs.setdefault('authenticated', True)
|
|
self._check_uri_length(url)
|
|
return self.request(url, method, **kwargs)
|
|
|
|
@property
|
|
def endpoint_url(self):
|
|
# NOTE(jamielennox): This is used purely by the CLI and should be
|
|
# removed when the CLI gets smarter.
|
|
return self.get_endpoint()
|
|
|
|
@property
|
|
def auth_token(self):
|
|
# NOTE(jamielennox): This is used purely by the CLI and should be
|
|
# removed when the CLI gets smarter.
|
|
return self.get_token()
|
|
|
|
def authenticate(self):
|
|
# NOTE(jamielennox): This is used purely by the CLI and should be
|
|
# removed when the CLI gets smarter.
|
|
self.get_token()
|
|
|
|
def get_auth_info(self):
|
|
auth_info = {'auth_token': self.auth_token,
|
|
'endpoint_url': self.endpoint_url}
|
|
|
|
# NOTE(jamielennox): This is the best we can do here. It will work
|
|
# with identity plugins which is the primary case but we should
|
|
# deprecate it's usage as much as possible.
|
|
try:
|
|
get_access = (self.auth or self.session.auth).get_access
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
auth_ref = get_access(self.session)
|
|
|
|
auth_info['auth_tenant_id'] = auth_ref.project_id
|
|
auth_info['auth_user_id'] = auth_ref.user_id
|
|
|
|
return auth_info
|
|
|
|
def get_auth_ref(self):
|
|
return self.session.auth.get_auth_ref(self.session)
|
|
|
|
|
|
# FIXME(bklei): Should refactor this to use kwargs and only
|
|
# explicitly list arguments that are not None.
|
|
@debtcollector.renames.renamed_kwarg('tenant_id', 'project_id', replace=True)
|
|
@debtcollector.renames.renamed_kwarg(
|
|
'tenant_name', 'project_name', replace=True)
|
|
def construct_http_client(username=None,
|
|
user_id=None,
|
|
project_name=None,
|
|
project_id=None,
|
|
password=None,
|
|
auth_url=None,
|
|
token=None,
|
|
region_name=None,
|
|
timeout=None,
|
|
endpoint_url=None,
|
|
insecure=False,
|
|
endpoint_type='public',
|
|
log_credentials=None,
|
|
auth_strategy='keystone',
|
|
ca_cert=None,
|
|
cert=None,
|
|
service_type='network',
|
|
session=None,
|
|
global_request_id=None,
|
|
**kwargs):
|
|
|
|
if session:
|
|
kwargs.setdefault('user_agent', USER_AGENT)
|
|
kwargs.setdefault('interface', endpoint_type)
|
|
return SessionClient(session=session,
|
|
service_type=service_type,
|
|
region_name=region_name,
|
|
global_request_id=global_request_id,
|
|
**kwargs)
|
|
else:
|
|
# FIXME(bklei): username and password are now optional. Need
|
|
# to test that they were provided in this mode. Should also
|
|
# refactor to use kwargs.
|
|
return HTTPClient(username=username,
|
|
password=password,
|
|
project_id=project_id,
|
|
project_name=project_name,
|
|
user_id=user_id,
|
|
auth_url=auth_url,
|
|
token=token,
|
|
endpoint_url=endpoint_url,
|
|
insecure=insecure,
|
|
timeout=timeout,
|
|
region_name=region_name,
|
|
endpoint_type=endpoint_type,
|
|
service_type=service_type,
|
|
ca_cert=ca_cert,
|
|
cert=cert,
|
|
log_credentials=log_credentials,
|
|
auth_strategy=auth_strategy,
|
|
global_request_id=global_request_id)
|