# Copyright 2014 # The Cloudscaling Group, Inc. # # 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 mock from oslo.config import cfg from oslotest import base as test_base import testtools import webob from ec2api import exception from ec2api import metadata from ec2api.tests import fakes from ec2api.tests import matchers class ProxyTestCase(test_base.BaseTestCase): def setUp(self): super(ProxyTestCase, self).setUp() self.handler = metadata.MetadataRequestHandler() conf = cfg.CONF self.addCleanup(conf.reset) conf.set_override('nova_metadata_ip', '9.9.9.9', group='metadata') conf.set_override('nova_metadata_port', 8775, group='metadata') conf.set_override('nova_metadata_protocol', 'http', group='metadata') conf.set_override('nova_metadata_insecure', True, group='metadata') conf.set_override('auth_ca_cert', None, group='metadata') conf.set_override('nova_client_cert', 'nova_cert', group='metadata') conf.set_override('nova_client_priv_key', 'nova_priv_key', group='metadata') conf.set_override('admin_user', 'admin', group='metadata') conf.set_override('admin_password', 'password', group='metadata') conf.set_override('admin_tenant_name', 'service', group='metadata') conf.set_override('metadata_proxy_shared_secret', 'secret', group='metadata') @mock.patch('ec2api.metadata.api.get_version_list') def test_callable(self, get_version_list): get_version_list.return_value = 'foo' request = webob.Request.blank('/') response = request.get_response(self.handler) self.assertEqual(200, response.status_int) self.assertEqual('foo', response.body) @mock.patch('ec2api.metadata.api.get_version_list') def test_root(self, get_version_list): get_version_list.return_value = 'fake_version' request = webob.Request.blank('/') response = request.get_response(self.handler) self.assertEqual('fake_version', response.body) response_ctype = response.headers['Content-Type'] self.assertTrue(response_ctype.startswith("text/plain")) get_version_list.assert_called_with() request = webob.Request.blank('/foo/../') response = request.get_response(self.handler) self.assertEqual('fake_version', response.body) @mock.patch.object(metadata.MetadataRequestHandler, '_get_metadata') def test_version_root(self, get_metadata): get_metadata.return_value = 'fake' request = webob.Request.blank('/latest') response = request.get_response(self.handler) self.assertEqual('fake', response.body) response_ctype = response.headers['Content-Type'] self.assertTrue(response_ctype.startswith("text/plain")) get_metadata.assert_called_with(mock.ANY, ['latest']) get_metadata.side_effect = exception.EC2MetadataNotFound() request = webob.Request.blank('/latest') response = request.get_response(self.handler) self.assertEqual(404, response.status_int) with mock.patch.object(metadata, 'LOG') as log: get_metadata.side_effect = Exception() request = webob.Request.blank('/latest') response = request.get_response(self.handler) self.assertEqual(500, response.status_int) self.assertEqual(len(log.mock_calls), 2) @mock.patch('ec2api.metadata.api.get_metadata_item') @mock.patch('ec2api.metadata.api.get_os_instance_and_project_id') @mock.patch.object(metadata.MetadataRequestHandler, '_get_remote_ip') @mock.patch.object(metadata.MetadataRequestHandler, '_get_context') def test_get_metadata_by_ip(self, get_context, get_remote_ip, get_ids, get_metadata_item): get_context.return_value = mock.Mock(project_id='fake_admin_project') get_remote_ip.return_value = 'fake_instance_ip' get_ids.return_value = ('fake_instance_id', 'fake_project_id') get_metadata_item.return_value = 'fake_item' req = mock.Mock(headers={}) retval = self.handler._get_metadata(req, ['fake_ver', 'fake_attr']) self.assertEqual('fake_item', retval) get_context.assert_called_with() get_remote_ip.assert_called_with(req) get_ids.assert_called_with(get_context.return_value, 'fake_instance_ip') get_metadata_item.assert_called_with(get_context.return_value, ['fake_ver', 'fake_attr'], 'fake_instance_id', 'fake_instance_ip') self.assertEqual('fake_project_id', get_context.return_value.project_id) @mock.patch('ec2api.metadata.api.get_metadata_item') @mock.patch.object(metadata.MetadataRequestHandler, '_unpack_request_attributes') @mock.patch.object(metadata.MetadataRequestHandler, '_get_context') def test_get_metadata_by_instance_id(self, get_context, unpack_request, get_metadata_item): get_context.return_value = mock.Mock(project_id='fake_admin_project') unpack_request.return_value = ('fake_instance_id', 'fake_project_id', 'fake_instance_ip') get_metadata_item.return_value = 'fake_item' req = mock.Mock(headers={'X-Instance-ID': 'fake_instance_id'}) retval = self.handler._get_metadata(req, ['fake_ver', 'fake_attr']) self.assertEqual('fake_item', retval) get_context.assert_called_with() unpack_request.assert_called_with(req) get_metadata_item.assert_called_with(get_context.return_value, ['fake_ver', 'fake_attr'], 'fake_instance_id', 'fake_instance_ip') self.assertEqual('fake_project_id', get_context.return_value.project_id) @mock.patch.object(metadata.MetadataRequestHandler, '_proxy_request') def test_proxy_call(self, proxy): req = mock.Mock(path_info='/openstack') proxy.return_value = 'value' retval = self.handler(req) self.assertEqual(retval, 'value') @mock.patch.object(metadata, 'LOG') @mock.patch.object(metadata.MetadataRequestHandler, '_proxy_request') def test_proxy_call_internal_server_error(self, proxy, log): req = mock.Mock(path_info='/openstack') proxy.side_effect = Exception() retval = self.handler(req) self.assertIsInstance(retval, webob.exc.HTTPInternalServerError) self.assertEqual(len(log.mock_calls), 2) proxy.side_effect = exception.EC2MetadataException() retval = self.handler(req) self.assertIsInstance(retval, webob.exc.HTTPInternalServerError) @mock.patch.object(metadata.MetadataRequestHandler, '_proxy_request') def test_proxy_call_no_instance(self, proxy): req = mock.Mock(path_info='/openstack') proxy.side_effect = exception.EC2MetadataNotFound() retval = self.handler(req) self.assertIsInstance(retval, webob.exc.HTTPNotFound) @mock.patch.object(metadata.MetadataRequestHandler, '_build_proxy_request_headers') def _proxy_request_test_helper(self, build_headers, response_code=200, method='GET'): hdrs = {'X-Forwarded-For': '8.8.8.8'} body = 'body' req = mock.Mock(path_info='/openstack', query_string='', headers=hdrs, method=method, body=body) resp = mock.MagicMock(status=response_code) req.response = resp build_headers.return_value = hdrs with mock.patch('httplib2.Http') as mock_http: resp.__getitem__.return_value = "text/plain" mock_http.return_value.request.return_value = (resp, 'content') retval = self.handler._proxy_request(req) mock_http.assert_called_once_with( ca_certs=None, disable_ssl_certificate_validation=True) mock_http.assert_has_calls([ mock.call().add_certificate( cfg.CONF.metadata.nova_client_priv_key, cfg.CONF.metadata.nova_client_cert, "%s:%s" % (cfg.CONF.metadata.nova_metadata_ip, cfg.CONF.metadata.nova_metadata_port) ), mock.call().request( 'http://9.9.9.9:8775/openstack', method=method, headers={ 'X-Forwarded-For': '8.8.8.8', }, body=body )] ) build_headers.assert_called_once_with(req) return retval def test_proxy_request_post(self): response = self._proxy_request_test_helper(method='POST') self.assertEqual(response.content_type, "text/plain") self.assertEqual(response.body, 'content') def test_proxy_request_200(self): response = self._proxy_request_test_helper(response_code=200) self.assertEqual(response.content_type, "text/plain") self.assertEqual(response.body, 'content') def test_proxy_request_400(self): self.assertIsInstance( self._proxy_request_test_helper(response_code=400), webob.exc.HTTPBadRequest) def test_proxy_request_403(self): self.assertIsInstance( self._proxy_request_test_helper(response_code=403), webob.exc.HTTPForbidden) def test_proxy_request_404(self): self.assertIsInstance( self._proxy_request_test_helper(response_code=404), webob.exc.HTTPNotFound) def test_proxy_request_409(self): self.assertIsInstance( self._proxy_request_test_helper(response_code=409), webob.exc.HTTPConflict) def test_proxy_request_500(self): self.assertIsInstance( self._proxy_request_test_helper(response_code=500), webob.exc.HTTPInternalServerError) def test_proxy_request_other_code(self): with testtools.ExpectedException(Exception): self._proxy_request_test_helper(response_code=302) @mock.patch.object(metadata.MetadataRequestHandler, '_sign_instance_id') @mock.patch.object(metadata.MetadataRequestHandler, '_get_context') @mock.patch.object(metadata.MetadataRequestHandler, '_get_remote_ip') def test_build_proxy_request_headers(self, get_remote_ip, get_context, sign_instance_id): req = mock.Mock(headers={}) req.headers = {'X-Instance-ID': 'fake_instance_id', 'fake_key': 'fake_value'} self.assertThat(self.handler._build_proxy_request_headers(req), matchers.DictMatches(req.headers)) req.headers = {'fake_key': 'fake_value'} get_remote_ip.return_value = 'fake_instance_ip' get_context.return_value = 'fake_context' sign_instance_id.return_value = 'signed' with mock.patch('ec2api.metadata.api.' 'get_os_instance_and_project_id') as get_ids: get_ids.return_value = ('fake_instance_id', 'fake_project_id') self.assertThat(self.handler._build_proxy_request_headers(req), matchers.DictMatches( {'X-Forwarded-For': 'fake_instance_ip', 'X-Instance-ID': 'fake_instance_id', 'X-Tenant-ID': 'fake_project_id', 'X-Instance-ID-Signature': 'signed'})) get_remote_ip.assert_called_with(req) get_context.assert_called_with() sign_instance_id.assert_called_with('fake_instance_id') get_ids.assert_called_with('fake_context', 'fake_instance_ip') get_ids.side_effect = exception.EC2MetadataNotFound() self.assertRaises(exception.EC2MetadataNotFound, self.handler._build_proxy_request_headers, req) def test_sign_instance_id(self): self.assertEqual( '773ba44693c7553d6ee20f61ea5d2757a9a4f4a44d2841ae4e95b52e4cd62db4', self.handler._sign_instance_id('foo') ) def test_get_remote_ip(self): req = mock.Mock(remote_addr='fake_addr', headers={}) self.assertEqual('fake_addr', self.handler._get_remote_ip(req)) cfg.CONF.set_override('use_forwarded_for', True) self.assertEqual('fake_addr', self.handler._get_remote_ip(req)) req.headers['X-Forwarded-For'] = 'fake_forwarded_for' self.assertEqual('fake_forwarded_for', self.handler._get_remote_ip(req)) cfg.CONF.set_override('use_forwarded_for', False) self.assertEqual('fake_addr', self.handler._get_remote_ip(req)) @mock.patch('keystoneclient.v2_0.client.Client') def test_get_context(self, keystone): service_catalog = mock.MagicMock() service_catalog.get_data.return_value = 'fake_service_catalog' keystone.return_value = mock.Mock(auth_user_id='fake_user_id', auth_tenant_id='fake_project_id', auth_token='fake_token', service_catalog=service_catalog) context = self.handler._get_context() self.assertEqual('fake_user_id', context.user_id) self.assertEqual('fake_project_id', context.project_id) self.assertEqual('fake_token', context.auth_token) self.assertEqual('fake_service_catalog', context.service_catalog) self.assertTrue(context.is_admin) self.assertTrue(context.cross_tenants) conf = cfg.CONF keystone.assert_called_with( username=conf.metadata.admin_user, password=conf.metadata.admin_password, tenant_name=conf.metadata.admin_tenant_name, auth_url=conf.keystone_url) def test_unpack_request_attributes(self): sign = ( '97e7709481495f1a3a589e5ee03f8b5d51a3e0196768e300c441b58fe0382f4d') req = mock.Mock(headers={'X-Instance-ID': 'fake_instance_id', 'X-Tenant-ID': 'fake_project_id', 'X-Forwarded-For': 'fake_instance_ip', 'X-Instance-ID-Signature': sign}) retval = self.handler._unpack_request_attributes(req) self.assertEqual( ('fake_instance_id', 'fake_project_id', 'fake_instance_ip'), retval) req.headers['X-Instance-ID-Signature'] = 'fake' self.assertRaises(webob.exc.HTTPForbidden, self.handler._unpack_request_attributes, req) req.headers.pop('X-Tenant-ID') self.assertRaises(webob.exc.HTTPBadRequest, self.handler._unpack_request_attributes, req) req.headers.pop('X-Forwarded-For') self.assertRaises(exception.EC2MetadataInvalidAddress, self.handler._unpack_request_attributes, req) @mock.patch('ec2api.utils.constant_time_compare') def test_usage_of_constant_time_compare(self, constant_time_compare): sign = ( '97e7709481495f1a3a589e5ee03f8b5d51a3e0196768e300c441b58fe0382f4d') req = mock.Mock(headers={'X-Instance-ID': 'fake_instance_id', 'X-Tenant-ID': 'fake_project_id', 'X-Forwarded-For': 'fake_instance_ip', 'X-Instance-ID-Signature': sign}) self.handler._unpack_request_attributes(req) self.assertEqual(1, constant_time_compare.call_count) @mock.patch('keystoneclient.v2_0.client.Client') @mock.patch('novaclient.v1_1.client.Client') @mock.patch('ec2api.db.api.IMPL') @mock.patch('ec2api.metadata.api.instance_api') @mock.patch('ec2api.metadata.api.novadb') def test_get_metadata(self, novadb, instance_api, db_api, nova, keystone): service_catalog = mock.MagicMock() service_catalog.get_data.return_value = [] keystone.return_value = mock.Mock(auth_user_id='fake_user_id', auth_tenant_id='fake_project_id', auth_token='fake_token', service_catalog=service_catalog) nova.return_value.fixed_ips.get.return_value = ( mock.Mock(hostname='fake_name')) nova.return_value.servers.list.return_value = [fakes.OS_INSTANCE_1] db_api.get_item_ids.return_value = [ (fakes.ID_EC2_INSTANCE_1, fakes.ID_OS_INSTANCE_1)] instance_api.describe_instances.return_value = { 'reservationSet': [fakes.EC2_RESERVATION_1]} instance_api.describe_instance_attribute.return_value = { 'instanceId': fakes.ID_EC2_INSTANCE_1, 'userData': {'value': 'fake_user_data'}} novadb.instance_get_by_uuid.return_value = fakes.NOVADB_INSTANCE_1 novadb.block_device_mapping_get_all_by_instance.return_value = [] novadb.instance_get_by_uuid.return_value = fakes.NOVADB_INSTANCE_1 def _test_metadata_path(relpath): # recursively confirm a http 200 from all meta-data elements # available at relpath. request = webob.Request.blank( relpath, remote_addr=fakes.IP_NETWORK_INTERFACE_2) response = request.get_response(self.handler) for item in response.body.split('\n'): if 'public-keys' in relpath: # meta-data/public-keys/0=keyname refers to # meta-data/public-keys/0 item = item.split('=')[0] if item.endswith('/'): path = relpath + '/' + item _test_metadata_path(path) continue path = relpath + '/' + item request = webob.Request.blank( path, remote_addr=fakes.IP_NETWORK_INTERFACE_2) response = request.get_response(self.handler) self.assertEqual(200, response.status_int, message=path) _test_metadata_path('/latest')