diff --git a/nova/network/neutronv2/__init__.py b/nova/network/neutronv2/__init__.py index 9bd2e5f821d7..7169890e19bb 100644 --- a/nova/network/neutronv2/__init__.py +++ b/nova/network/neutronv2/__init__.py @@ -15,57 +15,60 @@ # License for the specific language governing permissions and limitations # under the License. -from neutronclient import client from neutronclient.common import exceptions from neutronclient.v2_0 import client as clientv20 from oslo.config import cfg -from nova.openstack.common import excutils -from nova.openstack.common.gettextutils import _ +from nova.openstack.common import local from nova.openstack.common import log as logging CONF = cfg.CONF LOG = logging.getLogger(__name__) -def _get_auth_token(): - try: - httpclient = client.HTTPClient( - username=CONF.neutron_admin_username, - tenant_name=CONF.neutron_admin_tenant_name, - region_name=CONF.neutron_region_name, - password=CONF.neutron_admin_password, - auth_url=CONF.neutron_admin_auth_url, - timeout=CONF.neutron_url_timeout, - auth_strategy=CONF.neutron_auth_strategy, - ca_cert=CONF.neutron_ca_certificates_file, - insecure=CONF.neutron_api_insecure) - httpclient.authenticate() - return httpclient.auth_token - except exceptions.NeutronClientException as e: - with excutils.save_and_reraise_exception(): - LOG.error(_('Neutron client authentication failed: %s'), e) - - def _get_client(token=None): - if not token and CONF.neutron_auth_strategy: - token = _get_auth_token() params = { 'endpoint_url': CONF.neutron_url, 'timeout': CONF.neutron_url_timeout, 'insecure': CONF.neutron_api_insecure, 'ca_cert': CONF.neutron_ca_certificates_file, } + if token: params['token'] = token - else: params['auth_strategy'] = None + else: + params['username'] = CONF.neutron_admin_username + params['tenant_name'] = CONF.neutron_admin_tenant_name + params['password'] = CONF.neutron_admin_password + params['auth_url'] = CONF.neutron_admin_auth_url + params['auth_strategy'] = CONF.neutron_auth_strategy return clientv20.Client(**params) def get_client(context, admin=False): - if admin: - token = None - else: + # NOTE(dprince): In the case where no auth_token is present + # we allow use of neutron admin tenant credentials if + # it is an admin context. + # This is to support some services (metadata API) where + # an admin context is used without an auth token. + if admin or (context.is_admin and not context.auth_token): + # NOTE(dims): We need to use admin token, let us cache a + # thread local copy for re-using this client + # multiple times and to avoid excessive calls + # to neutron to fetch tokens. Some of the hackiness in this code + # will go away once BP auth-plugins is implemented. + # That blue print will ensure that tokens can be shared + # across clients as well + if not hasattr(local.strong_store, 'neutron_client'): + local.strong_store.neutron_client = _get_client(token=None) + return local.strong_store.neutron_client + + # We got a user token that we can use that as-is + if context.auth_token: token = context.auth_token - return _get_client(token=token) + return _get_client(token=token) + + # We did not get a user token and we should not be using + # an admin token so log an error + raise exceptions.Unauthorized() diff --git a/nova/network/neutronv2/api.py b/nova/network/neutronv2/api.py index 3efe77ec6b0e..a6e534452124 100644 --- a/nova/network/neutronv2/api.py +++ b/nova/network/neutronv2/api.py @@ -372,7 +372,8 @@ class API(base.Base): if (not self.last_neutron_extension_sync or ((time.time() - self.last_neutron_extension_sync) >= CONF.neutron_extension_sync_interval)): - neutron = neutronv2.get_client(context.get_admin_context()) + neutron = neutronv2.get_client(context.get_admin_context(), + admin=True) extensions_list = neutron.list_extensions()['extensions'] self.last_neutron_extension_sync = time.time() self.extensions.clear() diff --git a/nova/tests/network/test_neutronv2.py b/nova/tests/network/test_neutronv2.py index a6d150b5c611..37e5ebe8560b 100644 --- a/nova/tests/network/test_neutronv2.py +++ b/nova/tests/network/test_neutronv2.py @@ -33,6 +33,7 @@ from nova.network import neutronv2 from nova.network.neutronv2 import api as neutronapi from nova.network.neutronv2 import constants from nova.openstack.common import jsonutils +from nova.openstack.common import local from nova import test from nova import utils @@ -101,6 +102,7 @@ class TestNeutronClient(test.TestCase): auth_token='token') self.mox.StubOutWithMock(client.Client, "__init__") client.Client.__init__( + auth_strategy=None, endpoint_url=CONF.neutron_url, token=my_context.auth_token, timeout=CONF.neutron_url_timeout, @@ -109,6 +111,33 @@ class TestNeutronClient(test.TestCase): self.mox.ReplayAll() neutronv2.get_client(my_context) + def test_withouttoken(self): + my_context = context.RequestContext('userid', 'my_tenantid') + self.assertRaises(exceptions.Unauthorized, + neutronv2.get_client, + my_context) + + def test_withtoken_context_is_admin(self): + self.flags(neutron_url='http://anyhost/') + self.flags(neutron_url_timeout=30) + my_context = context.RequestContext('userid', + 'my_tenantid', + auth_token='token', + is_admin=True) + self.mox.StubOutWithMock(client.Client, "__init__") + client.Client.__init__( + auth_strategy=None, + endpoint_url=CONF.neutron_url, + token=my_context.auth_token, + timeout=CONF.neutron_url_timeout, + insecure=False, + ca_cert=None).AndReturn(None) + self.mox.ReplayAll() + # Note that although we have admin set in the context we + # are not asking for an admin client, and so we auth with + # our own token + neutronv2.get_client(my_context) + def test_withouttoken_keystone_connection_error(self): self.flags(neutron_auth_strategy='keystone') self.flags(neutron_url='http://anyhost/') @@ -117,21 +146,6 @@ class TestNeutronClient(test.TestCase): neutronv2.get_client, my_context) - def test_withouttoken_keystone_not_auth(self): - self.flags(neutron_auth_strategy=None) - self.flags(neutron_url='http://anyhost/') - self.flags(neutron_url_timeout=30) - my_context = context.RequestContext('userid', 'my_tenantid') - self.mox.StubOutWithMock(client.Client, "__init__") - client.Client.__init__( - endpoint_url=CONF.neutron_url, - auth_strategy=None, - timeout=CONF.neutron_url_timeout, - insecure=False, - ca_cert=None).AndReturn(None) - self.mox.ReplayAll() - neutronv2.get_client(my_context) - class TestNeutronv2Base(test.TestCase): @@ -596,6 +610,12 @@ class TestNeutronv2(TestNeutronv2Base): def test_refresh_neutron_extensions_cache(self): api = neutronapi.API() + + # Note: Don't want the default get_client from setUp() + self.mox.ResetAll() + neutronv2.get_client(mox.IgnoreArg(), + admin=True).AndReturn( + self.moxed_client) self.moxed_client.list_extensions().AndReturn( {'extensions': [{'name': 'nvp-qos'}]}) self.mox.ReplayAll() @@ -604,6 +624,12 @@ class TestNeutronv2(TestNeutronv2Base): def test_populate_neutron_extension_values_rxtx_factor(self): api = neutronapi.API() + + # Note: Don't want the default get_client from setUp() + self.mox.ResetAll() + neutronv2.get_client(mox.IgnoreArg(), + admin=True).AndReturn( + self.moxed_client) self.moxed_client.list_extensions().AndReturn( {'extensions': [{'name': 'nvp-qos'}]}) self.mox.ReplayAll() @@ -797,6 +823,9 @@ class TestNeutronv2(TestNeutronv2Base): {'networks': self.nets2}) self.moxed_client.list_networks(shared=True).AndReturn( {'networks': []}) + neutronv2.get_client(mox.IgnoreArg(), + admin=True).AndReturn( + self.moxed_client) port_req_body = { 'port': { 'network_id': self.nets2[0]['id'], @@ -1709,7 +1738,7 @@ class TestNeutronv2Portbinding(TestNeutronv2Base): def test_populate_neutron_extension_values_binding(self): api = neutronapi.API() - neutronv2.get_client(mox.IgnoreArg()).AndReturn( + neutronv2.get_client(mox.IgnoreArg(), admin=True).AndReturn( self.moxed_client) self.moxed_client.list_extensions().AndReturn( {'extensions': [{'name': constants.PORTBINDING_EXT}]}) @@ -1795,3 +1824,106 @@ class TestNeutronv2ExtraDhcpOpts(TestNeutronv2Base): self._allocate_for_instance(1, dhcp_options=dhcp_opts) CONF.set_override('dhcp_options_enabled', False) + + +class TestNeutronClientForAdminScenarios(test.TestCase): + def test_get_cached_neutron_client_for_admin(self): + self.flags(neutron_url='http://anyhost/') + self.flags(neutron_url_timeout=30) + my_context = context.RequestContext('userid', + 'my_tenantid', + auth_token='token') + + # Make multiple calls and ensure we get the same + # client back again and again + client = neutronv2.get_client(my_context, True) + client2 = neutronv2.get_client(my_context, True) + client3 = neutronv2.get_client(my_context, True) + self.assertEqual(client, client2) + self.assertEqual(client, client3) + + # clear the cache + local.strong_store.neutron_client = None + + # A new client should be created now + client4 = neutronv2.get_client(my_context, True) + self.assertNotEqual(client, client4) + + def test_get_neutron_client_for_non_admin(self): + self.flags(neutron_url='http://anyhost/') + self.flags(neutron_url_timeout=30) + my_context = context.RequestContext('userid', + 'my_tenantid', + auth_token='token') + + # Multiple calls should return different clients + client = neutronv2.get_client(my_context) + client2 = neutronv2.get_client(my_context) + self.assertNotEqual(client, client2) + + def test_get_neutron_client_for_non_admin_and_no_token(self): + self.flags(neutron_url='http://anyhost/') + self.flags(neutron_url_timeout=30) + my_context = context.RequestContext('userid', + 'my_tenantid') + + self.assertRaises(exceptions.Unauthorized, + neutronv2.get_client, + my_context) + + def test_get_client_for_admin(self): + + self.flags(neutron_auth_strategy=None) + self.flags(neutron_url='http://anyhost/') + self.flags(neutron_url_timeout=30) + my_context = context.RequestContext('userid', 'my_tenantid', + auth_token='token') + self.mox.StubOutWithMock(client.Client, "__init__") + client.Client.__init__( + auth_url=CONF.neutron_admin_auth_url, + password=CONF.neutron_admin_password, + tenant_name=CONF.neutron_admin_tenant_name, + username=CONF.neutron_admin_username, + endpoint_url=CONF.neutron_url, + auth_strategy=None, + timeout=CONF.neutron_url_timeout, + insecure=False, + ca_cert=None).AndReturn(None) + self.mox.ReplayAll() + + # clear the cache + if hasattr(local.strong_store, 'neutron_client'): + delattr(local.strong_store, 'neutron_client') + + # Note that the context is not elevated, but the True is passed in + # which will force an elevation to admin credentials even though + # the context has an auth_token. + neutronv2.get_client(my_context, True) + + def test_get_client_for_admin_context(self): + + self.flags(neutron_auth_strategy=None) + self.flags(neutron_url='http://anyhost/') + self.flags(neutron_url_timeout=30) + my_context = context.get_admin_context() + self.mox.StubOutWithMock(client.Client, "__init__") + client.Client.__init__( + auth_url=CONF.neutron_admin_auth_url, + password=CONF.neutron_admin_password, + tenant_name=CONF.neutron_admin_tenant_name, + username=CONF.neutron_admin_username, + endpoint_url=CONF.neutron_url, + auth_strategy=None, + timeout=CONF.neutron_url_timeout, + insecure=False, + ca_cert=None).AndReturn(None) + self.mox.ReplayAll() + + # clear the cache + if hasattr(local.strong_store, 'neutron_client'): + delattr(local.strong_store, 'neutron_client') + + # Note that the context does not contain a token but is + # an admin context which will force an elevation to admin + # credentials. + neutronv2.get_client(my_context)