diff --git a/nova/api/auth.py b/nova/api/auth.py index e93e83f1af98..420e2dc3339a 100644 --- a/nova/api/auth.py +++ b/nova/api/auth.py @@ -81,6 +81,14 @@ class InjectContext(wsgi.Middleware): class NovaKeystoneContext(wsgi.Middleware): """Make a request context from keystone headers.""" + @staticmethod + def _create_context(env, **kwargs): + """Create a context from a request environ. + + This exists to make test stubbing easier. + """ + return context.RequestContext.from_environ(env, **kwargs) + @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): # Build a context, including the auth_token... @@ -101,7 +109,7 @@ class NovaKeystoneContext(wsgi.Middleware): # middleware in newer versions. user_auth_plugin = req.environ.get('keystone.token_auth') - ctx = context.RequestContext.from_environ( + ctx = self._create_context( req.environ, user_auth_plugin=user_auth_plugin, remote_address=remote_address, diff --git a/nova/tests/fixtures.py b/nova/tests/fixtures.py index a1c866879435..86217049ff35 100644 --- a/nova/tests/fixtures.py +++ b/nova/tests/fixtures.py @@ -953,18 +953,21 @@ class OSAPIFixture(fixtures.Fixture): """ def __init__(self, api_version='v2', - project_id='6f70656e737461636b20342065766572'): + project_id='6f70656e737461636b20342065766572', + use_project_id_in_urls=False): """Constructor :param api_version: the API version that we're interested in using. Currently this expects 'v2' or 'v2.1' as possible options. :param project_id: the project id to use on the API. - + :param use_project_id_in_urls: If True, act like the "endpoint" in the + "service catalog" has the legacy format including the project_id. """ super(OSAPIFixture, self).__init__() self.api_version = api_version self.project_id = project_id + self.use_project_id_in_urls = use_project_id_in_urls def setUp(self): super(OSAPIFixture, self).setUp() @@ -980,6 +983,23 @@ class OSAPIFixture(fixtures.Fixture): } self.useFixture(ConfPatcher(**conf_overrides)) + # Stub out authentication middleware + # TODO(efried): Use keystonemiddleware.fixtures.AuthTokenFixture + self.useFixture(fixtures.MockPatch( + 'keystonemiddleware.auth_token.filter_factory', + return_value=lambda _app: _app)) + + # Stub out context middleware + def fake_ctx(env, **kwargs): + user_id = env['HTTP_X_AUTH_USER'] + project_id = env['HTTP_X_AUTH_PROJECT_ID'] + is_admin = user_id == 'admin' + return context.RequestContext( + user_id, project_id, is_admin=is_admin, **kwargs) + + self.useFixture(fixtures.MonkeyPatch( + 'nova.api.auth.NovaKeystoneContext._create_context', fake_ctx)) + # Turn off manipulation of socket_options in TCPKeepAliveAdapter # to keep wsgi-intercept happy. Replace it with the method # from its superclass. @@ -997,12 +1017,15 @@ class OSAPIFixture(fixtures.Fixture): intercept.install_intercept() self.addCleanup(intercept.uninstall_intercept) - self.auth_url = 'http://%(host)s:%(port)s/%(api_version)s' % ({ + base_url = 'http://%(host)s:%(port)s/%(api_version)s' % ({ 'host': hostname, 'port': port, 'api_version': self.api_version}) - self.api = client.TestOpenStackClient('fake', 'fake', self.auth_url, - self.project_id) + if self.use_project_id_in_urls: + base_url += '/' + self.project_id + + self.api = client.TestOpenStackClient( + 'fake', base_url, project_id=self.project_id) self.admin_api = client.TestOpenStackClient( - 'admin', 'admin', self.auth_url, self.project_id) + 'admin', base_url, project_id=self.project_id) # Provide a way to access the wsgi application to tests using # the fixture. self.app = app diff --git a/nova/tests/functional/api/client.py b/nova/tests/functional/api/client.py index 63529ad6fdd6..430a1742aa06 100644 --- a/nova/tests/functional/api/client.py +++ b/nova/tests/functional/api/client.py @@ -124,13 +124,10 @@ class TestOpenStackClient(object): """ - def __init__(self, auth_user, auth_key, auth_url, - project_id=None): + def __init__(self, auth_user, base_url, project_id=None): super(TestOpenStackClient, self).__init__() - self.auth_result = None self.auth_user = auth_user - self.auth_key = auth_key - self.auth_url = auth_url + self.base_url = base_url if project_id is None: self.project_id = "6f70656e737461636b20342065766572" else: @@ -144,49 +141,22 @@ class TestOpenStackClient(object): response = requests.request(method, url, data=body, headers=_headers) return response - def _authenticate(self, retry_count=0): - if self.auth_result: - return self.auth_result - - auth_url = self.auth_url - headers = {'X-Auth-User': self.auth_user, - 'X-Auth-Key': self.auth_key, - 'X-Auth-Project-Id': self.project_id} - response = self.request(auth_url, - headers=headers) - - http_status = response.status_code - LOG.debug("%(auth_url)s => code %(http_status)s", - {'auth_url': auth_url, - 'http_status': http_status}) - - # NOTE(cdent): This is a workaround for an issue where the placement - # API fixture may respond when a request was supposed to go to the - # compute API fixture. Retry a few times, hoping to hit the right - # fixture. - if http_status == 401: - if retry_count <= 3: - return self._authenticate(retry_count=retry_count + 1) - else: - raise OpenStackApiAuthenticationException(response=response) - - self.auth_result = response.headers - return self.auth_result - def api_request(self, relative_uri, check_response_status=None, strip_version=False, **kwargs): - auth_result = self._authenticate() - - # NOTE(justinsb): httplib 'helpfully' converts headers to lower case - base_uri = auth_result['x-server-management-url'] + base_uri = self.base_url if strip_version: - # NOTE(vish): cut out version number and tenant_id - base_uri = '/'.join(base_uri.split('/', 3)[:-1]) + # The base_uri is either http://%(host)s:%(port)s/%(api_version)s + # or http://%(host)s:%(port)s/%(api_version)s/%(project_id)s + # NOTE(efried): Using urlparse was not easier :) + chunked = base_uri.split('/') + base_uri = '/'.join(chunked[:3]) + # Restore the project ID if present + if len(chunked) == 5: + base_uri += '/' + chunked[-1] full_uri = '%s/%s' % (base_uri, relative_uri) headers = kwargs.setdefault('headers', {}) - headers['X-Auth-Token'] = auth_result['x-auth-token'] if ('X-OpenStack-Nova-API-Version' in headers or 'OpenStack-API-Version' in headers): raise Exception('Microversion should be set via ' @@ -195,6 +165,10 @@ class TestOpenStackClient(object): headers['X-OpenStack-Nova-API-Version'] = self.microversion headers['OpenStack-API-Version'] = 'compute %s' % self.microversion + headers.setdefault('X-Auth-User', self.auth_user) + headers.setdefault('X-User-Id', self.auth_user) + headers.setdefault('X-Auth-Project-Id', self.project_id) + response = self.request(full_uri, **kwargs) http_status = response.status_code diff --git a/nova/tests/functional/api_sample_tests/api_sample_base.py b/nova/tests/functional/api_sample_tests/api_sample_base.py index e75bac6c8c7c..8f972f479ce3 100644 --- a/nova/tests/functional/api_sample_tests/api_sample_base.py +++ b/nova/tests/functional/api_sample_tests/api_sample_base.py @@ -62,6 +62,11 @@ class ApiSampleTestBaseV21(testscenarios.WithScenarios, # any additional fixtures needed for this scenario _additional_fixtures = [] sample_dir = None + # Include the project ID in request URLs by default. This is overridden + # for certain `scenarios` and by certain subclasses. + # Note that API sample tests also use this in substitutions to validate + # that URLs in responses (e.g. location of a server just created) are + # correctly constructed. _use_project_id = True # Availability zones for the API samples tests. Can be overridden by # sub-classes. If set, the AvailabilityZoneFilter is not used. diff --git a/nova/tests/functional/api_sample_tests/test_simple_tenant_usage.py b/nova/tests/functional/api_sample_tests/test_simple_tenant_usage.py index 9f67dbd6d1a9..494155bffb95 100644 --- a/nova/tests/functional/api_sample_tests/test_simple_tenant_usage.py +++ b/nova/tests/functional/api_sample_tests/test_simple_tenant_usage.py @@ -74,6 +74,7 @@ class SimpleTenantUsageV240Test(test_servers.ServersSampleBase): sample_dir = 'os-simple-tenant-usage' microversion = '2.40' scenarios = [('v2_40', {'api_major_version': 'v2.1'})] + _use_project_id = False def setUp(self): super(SimpleTenantUsageV240Test, self).setUp() diff --git a/nova/tests/functional/api_sample_tests/test_versions.py b/nova/tests/functional/api_sample_tests/test_versions.py index 96222b572f42..166195769f5a 100644 --- a/nova/tests/functional/api_sample_tests/test_versions.py +++ b/nova/tests/functional/api_sample_tests/test_versions.py @@ -19,6 +19,7 @@ from nova.tests.functional.api_sample_tests import api_sample_base class VersionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21): sample_dir = 'versions' + _use_project_id = False # NOTE(gmann): Setting empty scenario for 'version' API testing # as those does not send request on particular endpoint and running # its tests alone is enough. diff --git a/nova/tests/functional/integrated_helpers.py b/nova/tests/functional/integrated_helpers.py index ee9c31bdd41a..2585a1f2ebea 100644 --- a/nova/tests/functional/integrated_helpers.py +++ b/nova/tests/functional/integrated_helpers.py @@ -80,6 +80,9 @@ class _IntegratedTestBase(test.TestCase): # New tests should rely on Neutron and old ones migrated to use this since # nova-network is deprecated. USE_NEUTRON = True + # This indicates whether to include the project ID in the URL for API + # requests through OSAPIFixture. Overridden by subclasses. + _use_project_id = False def setUp(self): super(_IntegratedTestBase, self).setUp() @@ -124,8 +127,11 @@ class _IntegratedTestBase(test.TestCase): self.scheduler = self._setup_scheduler_service() self.compute = self._setup_compute_service() + self.api_fixture = self.useFixture( - nova_fixtures.OSAPIFixture(self.api_major_version)) + nova_fixtures.OSAPIFixture( + api_version=self.api_major_version, + use_project_id_in_urls=self._use_project_id)) # if the class needs to run as admin, make the api endpoint # the admin, otherwise it's safer to run as non admin user. diff --git a/nova/tests/functional/test_middleware.py b/nova/tests/functional/test_middleware.py index 721a78d79bce..583ef779d2c9 100644 --- a/nova/tests/functional/test_middleware.py +++ b/nova/tests/functional/test_middleware.py @@ -41,6 +41,10 @@ class TestCORSMiddleware(api_sample_base.ApiSampleTestBaseV21): self._original_call_method = cfg.ConfigOpts.GroupAttr.__getattr__ cfg.ConfigOpts.GroupAttr.__getattr__ = _mock_getattr + # With the project_id in the URL, we get the 300 'multiple choices' + # response from nova.api.openstack.compute.versions.Versions. + self.exp_version_status = 300 if self._use_project_id else 200 + # Initialize the application after all the config overrides are in # place. super(TestCORSMiddleware, self).setUp() @@ -103,7 +107,7 @@ class TestCORSMiddleware(api_sample_base.ApiSampleTestBaseV21): 'Access-Control-Request-Method': 'GET' }) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, self.exp_version_status) self.assertIn('Access-Control-Allow-Origin', response.headers) self.assertEqual('http://valid.example.com', response.headers['Access-Control-Allow-Origin']) @@ -116,5 +120,5 @@ class TestCORSMiddleware(api_sample_base.ApiSampleTestBaseV21): 'Access-Control-Request-Method': 'GET' }) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, self.exp_version_status) self.assertNotIn('Access-Control-Allow-Origin', response.headers) diff --git a/nova/tests/unit/api/openstack/test_requestlog.py b/nova/tests/unit/api/openstack/test_requestlog.py index 99e3d34c76a7..386b79a52893 100644 --- a/nova/tests/unit/api/openstack/test_requestlog.py +++ b/nova/tests/unit/api/openstack/test_requestlog.py @@ -63,18 +63,15 @@ class TestRequestLogMiddleware(testtools.TestCase): api = self.useFixture(fixtures.OSAPIFixture()).api resp = api.api_request('/', strip_version=True) - log1 = ('INFO [nova.api.openstack.requestlog] 127.0.0.1 ' - '"GET /v2" status: 204 len: 0 microversion: - time:') - self.assertIn(log1, self.stdlog.logger.output) # the content length might vary, but the important part is # what we log is what we return to the user (which turns out # to excitingly not be the case with eventlet!) content_length = resp.headers['content-length'] - log2 = ('INFO [nova.api.openstack.requestlog] 127.0.0.1 ' + log1 = ('INFO [nova.api.openstack.requestlog] 127.0.0.1 ' '"GET /" status: 200 len: %s' % content_length) - self.assertIn(log2, self.stdlog.logger.output) + self.assertIn(log1, self.stdlog.logger.output) @mock.patch('nova.api.openstack.requestlog.RequestLog._should_emit') def test_logs_mv(self, emit): diff --git a/nova/tests/unit/conf_fixture.py b/nova/tests/unit/conf_fixture.py index 5f21eb87937d..6f601fe1af05 100644 --- a/nova/tests/unit/conf_fixture.py +++ b/nova/tests/unit/conf_fixture.py @@ -45,9 +45,6 @@ class ConfFixture(config_fixture.Config): self.conf.set_default('use_ipv6', True) self.conf.set_default('vlan_interface', 'eth0') - # api group - self.conf.set_default('auth_strategy', 'noauth2', group='api') - # api_database group self.conf.set_default('connection', "sqlite://", group='api_database') self.conf.set_default('sqlite_synchronous', False,