Drop default URI, make either a session or inspector_url required

While convenient sometimes, the previous default behavior of assuming
127.0.0.1 is too unrealistic for production and may cause confusion.

Change-Id: I32496bc57f0127e680bbd1823401ab0211f93114
This commit is contained in:
Dmitry Tantsur 2017-11-17 13:16:58 +01:00
parent 98ee6670f1
commit e9bcb68bab
7 changed files with 77 additions and 52 deletions

View File

@ -12,4 +12,4 @@
# limitations under the License. # limitations under the License.
from .v1 import ClientV1, DEFAULT_API_VERSION, MAX_API_VERSION # noqa from .v1 import ClientV1, DEFAULT_API_VERSION, MAX_API_VERSION # noqa
from .common.http import ClientError, VersionNotSupported # noqa from .common.http import ClientError, EndpointNotFound, VersionNotSupported # noqa

View File

@ -18,14 +18,12 @@ import logging
from keystoneauth1 import exceptions as ks_exc from keystoneauth1 import exceptions as ks_exc
from keystoneauth1 import session as ks_session from keystoneauth1 import session as ks_session
from oslo_utils import netutils
import requests import requests
import six import six
from ironic_inspector_client.common.i18n import _ from ironic_inspector_client.common.i18n import _
_DEFAULT_URL = 'http://' + netutils.get_my_ipv4() + ':5050'
_ERROR_ENCODING = 'utf-8' _ERROR_ENCODING = 'utf-8'
LOG = logging.getLogger('ironic_inspector_client') LOG = logging.getLogger('ironic_inspector_client')
@ -85,6 +83,19 @@ class VersionNotSupported(Exception):
super(Exception, self).__init__(msg) super(Exception, self).__init__(msg)
class EndpointNotFound(Exception):
"""Denotes that endpoint for the introspection service was not found.
ivar service_type: requested service type
"""
def __init__(self, service_type):
self.service_type = service_type
msg = _('Endpoint of type %s was not found in the service catalog '
'and was not provided explicitly') % service_type
super(Exception, self).__init__(msg)
class BaseClient(object): class BaseClient(object):
"""Base class for clients, provides common HTTP code.""" """Base class for clients, provides common HTTP code."""
@ -105,8 +116,11 @@ class BaseClient(object):
:param interface: interface type (public, internal, etc) to use when :param interface: interface type (public, internal, etc) to use when
looking up the URL looking up the URL
:param region_name: region name to use when looking up the URL :param region_name: region name to use when looking up the URL
:raises: EndpointNotFound if the introspection service endpoint
was not provided via inspector_url and was not found in the
service catalog.
""" """
self._base_url = inspector_url or _DEFAULT_URL self._base_url = inspector_url
if session is None: if session is None:
self._session = ks_session.Session(None) self._session = ks_session.Session(None)
@ -117,11 +131,21 @@ class BaseClient(object):
self._base_url = session.get_endpoint( self._base_url = session.get_endpoint(
service_type=service_type, service_type=service_type,
interface=interface, interface=interface,
region_name=region_name) or _DEFAULT_URL region_name=region_name)
except ks_exc.EndpointNotFound: except ks_exc.CatalogException as exc:
LOG.warning('Endpoint for service %s was not found, ' LOG.error('%(iface)s endpoint for %(stype)s in region '
'falling back to local host on port 5050', '%(region)s was not found in the service '
service_type) 'catalog: %(error)s',
{'iface': interface,
'stype': service_type,
'region': region_name,
'error': exc})
raise EndpointNotFound(service_type=service_type)
if not self._base_url:
# This handles the case when session=None and no inspector_url is
# provided, as well as keystoneauth plugins that may return None.
raise EndpointNotFound(service_type=service_type)
self._base_url = self._base_url.rstrip('/') self._base_url = self._base_url.rstrip('/')
self._api_version = self._check_api_version(api_version) self._api_version = self._check_api_version(api_version)

View File

@ -25,17 +25,21 @@ import unittest
from ironic_inspector.common import swift from ironic_inspector.common import swift
from ironic_inspector import introspection_state as istate from ironic_inspector import introspection_state as istate
from ironic_inspector.test import functional from ironic_inspector.test import functional
from keystoneauth1 import session as ks_session
from keystoneauth1 import token_endpoint
from oslo_concurrency import processutils from oslo_concurrency import processutils
import ironic_inspector_client as client import ironic_inspector_client as client
from ironic_inspector_client.common import http
from ironic_inspector_client import shell from ironic_inspector_client import shell
class TestV1PythonAPI(functional.Base): class TestV1PythonAPI(functional.Base):
def setUp(self): def setUp(self):
super(TestV1PythonAPI, self).setUp() super(TestV1PythonAPI, self).setUp()
self.client = client.ClientV1() self.auth = token_endpoint.Token(endpoint='http://127.0.0.1:5050',
token='token')
self.session = ks_session.Session(self.auth)
self.client = client.ClientV1(session=self.session)
functional.cfg.CONF.set_override('store_data', 'none', 'processing') functional.cfg.CONF.set_override('store_data', 'none', 'processing')
def my_status_index(self, statuses): def my_status_index(self, statuses):
@ -192,15 +196,18 @@ class TestV1PythonAPI(functional.Base):
def test_client_init(self): def test_client_init(self):
self.assertRaises(client.VersionNotSupported, self.assertRaises(client.VersionNotSupported,
client.ClientV1, api_version=(1, 999)) client.ClientV1, session=self.session,
api_version=(1, 999))
self.assertRaises(client.VersionNotSupported, self.assertRaises(client.VersionNotSupported,
client.ClientV1, api_version=2) client.ClientV1, session=self.session,
api_version=2)
self.assertTrue(client.ClientV1(api_version=1).server_api_versions()) self.assertTrue(client.ClientV1(
self.assertTrue(client.ClientV1(api_version='1.0') api_version=1, session=self.session).server_api_versions())
.server_api_versions()) self.assertTrue(client.ClientV1(
self.assertTrue(client.ClientV1(api_version=(1, 0)) api_version='1.0', session=self.session).server_api_versions())
.server_api_versions()) self.assertTrue(client.ClientV1(
api_version=(1, 0), session=self.session).server_api_versions())
self.assertTrue( self.assertTrue(
client.ClientV1(inspector_url='http://127.0.0.1:5050') client.ClientV1(inspector_url='http://127.0.0.1:5050')
@ -455,6 +462,4 @@ class TestCLI(BaseCLITest):
if __name__ == '__main__': if __name__ == '__main__':
with functional.mocked_server(): with functional.mocked_server():
# Make links predictable
http._DEFAULT_URL = 'http://127.0.0.1:5050'
unittest.main(verbosity=2) unittest.main(verbosity=2)

View File

@ -25,7 +25,7 @@ class TestCheckVersion(unittest.TestCase):
@mock.patch.object(http.BaseClient, 'server_api_versions', @mock.patch.object(http.BaseClient, 'server_api_versions',
lambda *args, **kwargs: ((1, 0), (1, 99))) lambda *args, **kwargs: ((1, 0), (1, 99)))
def _check(self, version): def _check(self, version):
cli = http.BaseClient(1) cli = http.BaseClient(1, inspector_url='http://127.0.0.1:5050')
return cli._check_api_version(version) return cli._check_api_version(version)
def test_tuple(self): def test_tuple(self):
@ -64,7 +64,9 @@ FAKE_HEADERS = {
'return_value.headers': FAKE_HEADERS}) 'return_value.headers': FAKE_HEADERS})
class TestServerApiVersions(unittest.TestCase): class TestServerApiVersions(unittest.TestCase):
def _check(self, current=1): def _check(self, current=1):
return http.BaseClient(api_version=current).server_api_versions() return http.BaseClient(
api_version=current,
inspector_url='http://127.0.0.1:5050').server_api_versions()
def test_no_headers(self, mock_get): def test_no_headers(self, mock_get):
mock_get.return_value.headers = {} mock_get.return_value.headers = {}
@ -102,7 +104,7 @@ class TestServerApiVersions(unittest.TestCase):
class TestRequest(unittest.TestCase): class TestRequest(unittest.TestCase):
base_url = http._DEFAULT_URL + '/v1' base_url = 'http://127.0.0.1:5050/v1'
def setUp(self): def setUp(self):
super(TestRequest, self).setUp() super(TestRequest, self).setUp()
@ -131,24 +133,18 @@ class TestRequest(unittest.TestCase):
service_type='baremetal-introspection', service_type='baremetal-introspection',
interface=None, region_name=None) interface=None, region_name=None)
def test_ok_no_endpoint(self): def test_no_endpoint(self):
self.session.get_endpoint.return_value = None self.session.get_endpoint.return_value = None
res = self.get_client().request('get', '/foo/bar') self.assertRaises(http.EndpointNotFound, self.get_client)
self.assertIs(self.req.return_value, res)
self.req.assert_called_once_with(self.base_url + '/foo/bar', 'get',
raise_exc=False, headers=self.headers)
self.session.get_endpoint.assert_called_once_with( self.session.get_endpoint.assert_called_once_with(
service_type='baremetal-introspection', service_type='baremetal-introspection',
interface=None, region_name=None) interface=None, region_name=None)
def test_ok_endpoint_not_found(self): def test_endpoint_not_found(self):
self.session.get_endpoint.side_effect = exceptions.EndpointNotFound() self.session.get_endpoint.side_effect = exceptions.EndpointNotFound()
res = self.get_client().request('get', '/foo/bar') self.assertRaises(http.EndpointNotFound, self.get_client)
self.assertIs(self.req.return_value, res)
self.req.assert_called_once_with(self.base_url + '/foo/bar', 'get',
raise_exc=False, headers=self.headers)
self.session.get_endpoint.assert_called_once_with( self.session.get_endpoint.assert_called_once_with(
service_type='baremetal-introspection', service_type='baremetal-introspection',
interface=None, region_name=None) interface=None, region_name=None)
@ -156,11 +152,13 @@ class TestRequest(unittest.TestCase):
@mock.patch.object(session.Session, 'request', autospec=True, @mock.patch.object(session.Session, 'request', autospec=True,
**{'return_value.status_code': 200}) **{'return_value.status_code': 200})
def test_ok_no_auth(self, mock_req): def test_ok_no_auth(self, mock_req):
res = self.get_client(use_session=False).request('get', '/foo/bar') res = self.get_client(
use_session=False,
inspector_url='http://some/host').request('get', '/foo/bar')
self.assertIs(mock_req.return_value, res) self.assertIs(mock_req.return_value, res)
mock_req.assert_called_once_with(mock.ANY, mock_req.assert_called_once_with(mock.ANY,
self.base_url + '/foo/bar', 'get', 'http://some/host/v1/foo/bar', 'get',
raise_exc=False, headers=self.headers) raise_exc=False, headers=self.headers)
def test_explicit_version(self): def test_explicit_version(self):
@ -171,22 +169,6 @@ class TestRequest(unittest.TestCase):
self.req.assert_called_once_with(self.base_url + '/foo/bar', 'get', self.req.assert_called_once_with(self.base_url + '/foo/bar', 'get',
raise_exc=False, headers=self.headers) raise_exc=False, headers=self.headers)
def test_explicit_url(self):
res = self.get_client(inspector_url='http://host').request(
'get', '/foo/bar')
self.assertIs(self.req.return_value, res)
self.req.assert_called_once_with('http://host/v1/foo/bar', 'get',
raise_exc=False, headers=self.headers)
def test_explicit_url_with_version(self):
res = self.get_client(inspector_url='http://host/v1').request(
'get', '/foo/bar')
self.assertIs(self.req.return_value, res)
self.req.assert_called_once_with('http://host/v1/foo/bar', 'get',
raise_exc=False, headers=self.headers)
def test_error(self): def test_error(self):
self.req.return_value.status_code = 400 self.req.return_value.status_code = 400
self.req.return_value.content = json.dumps( self.req.return_value.content = json.dumps(

View File

@ -23,6 +23,7 @@ class TestExposedAPI(unittest.TestCase):
if not x.startswith('__') and if not x.startswith('__') and
not isinstance(getattr(ironic_inspector_client, x), not isinstance(getattr(ironic_inspector_client, x),
types.ModuleType)} types.ModuleType)}
self.assertEqual({'ClientV1', 'ClientError', 'VersionNotSupported', self.assertEqual({'ClientV1', 'ClientError', 'EndpointNotFound',
'VersionNotSupported',
'MAX_API_VERSION', 'DEFAULT_API_VERSION'}, 'MAX_API_VERSION', 'DEFAULT_API_VERSION'},
exposed) exposed)

View File

@ -38,6 +38,7 @@ class TestInit(unittest.TestCase):
my_ip = 'http://' + netutils.get_my_ipv4() + ':5050' my_ip = 'http://' + netutils.get_my_ipv4() + ':5050'
def get_client(self, **kwargs): def get_client(self, **kwargs):
kwargs.setdefault('inspector_url', self.my_ip)
return ironic_inspector_client.ClientV1(**kwargs) return ironic_inspector_client.ClientV1(**kwargs)
def test_ok(self, mock_get): def test_ok(self, mock_get):
@ -74,6 +75,7 @@ class BaseTest(unittest.TestCase):
@mock.patch.object(http.BaseClient, 'server_api_versions', @mock.patch.object(http.BaseClient, 'server_api_versions',
lambda self: ((1, 0), (1, 99))) lambda self: ((1, 0), (1, 99)))
def get_client(self, **kwargs): def get_client(self, **kwargs):
kwargs.setdefault('inspector_url', self.my_ip)
return ironic_inspector_client.ClientV1(**kwargs) return ironic_inspector_client.ClientV1(**kwargs)

View File

@ -0,0 +1,11 @@
---
upgrade:
- |
There is no longer a default introspection API endpoint. Previously,
if no endpoint was requested and no endpoint found in the service catalog,
``127.0.0.1:5050`` was used by default.
other:
- |
The ``ClientV1`` constructor now raises a new ``EndpointNotFound``
exception when no introspection API endpoint can be detected. Previously
this condition was ignored and ``127.0.0.1:5050`` was used as a fallback.