diff options
Diffstat (limited to 'nova/tests/unit/api/openstack')
150 files changed, 50651 insertions, 0 deletions
diff --git a/nova/tests/unit/api/openstack/__init__.py b/nova/tests/unit/api/openstack/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/nova/tests/unit/api/openstack/__init__.py diff --git a/nova/tests/unit/api/openstack/common.py b/nova/tests/unit/api/openstack/common.py new file mode 100644 index 0000000000..972958a329 --- /dev/null +++ b/nova/tests/unit/api/openstack/common.py @@ -0,0 +1,55 @@ +# Copyright 2011 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. + +from oslo.serialization import jsonutils +import webob + + +def webob_factory(url): + """Factory for removing duplicate webob code from tests.""" + + base_url = url + + def web_request(url, method=None, body=None): + req = webob.Request.blank("%s%s" % (base_url, url)) + if method: + req.content_type = "application/json" + req.method = method + if body: + req.body = jsonutils.dumps(body) + return req + return web_request + + +def compare_links(actual, expected): + """Compare xml atom links.""" + + return compare_tree_to_dict(actual, expected, ('rel', 'href', 'type')) + + +def compare_media_types(actual, expected): + """Compare xml media types.""" + + return compare_tree_to_dict(actual, expected, ('base', 'type')) + + +def compare_tree_to_dict(actual, expected, keys): + """Compare parts of lxml.etree objects to dicts.""" + + for elem, data in zip(actual, expected): + for key in keys: + if elem.get(key) != data.get(key): + return False + return True diff --git a/nova/tests/unit/api/openstack/compute/__init__.py b/nova/tests/unit/api/openstack/compute/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/__init__.py diff --git a/nova/tests/unit/api/openstack/compute/contrib/__init__.py b/nova/tests/unit/api/openstack/compute/contrib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/__init__.py diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_admin_actions.py b/nova/tests/unit/api/openstack/compute/contrib/test_admin_actions.py new file mode 100644 index 0000000000..44bf495b29 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_admin_actions.py @@ -0,0 +1,734 @@ +# Copyright 2011 OpenStack Foundation +# +# 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. + +from oslo.serialization import jsonutils +from oslo.utils import timeutils +import webob + +from nova.api.openstack import common +from nova.api.openstack.compute.contrib import admin_actions as \ + admin_actions_v2 +from nova.api.openstack.compute.plugins.v3 import admin_actions as \ + admin_actions_v21 +from nova.compute import vm_states +import nova.context +from nova import exception +from nova import objects +from nova.openstack.common import uuidutils +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + + +class CommonMixin(object): + admin_actions = None + fake_url = None + + def _make_request(self, url, body): + req = webob.Request.blank(self.fake_url + url) + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.content_type = 'application/json' + return req.get_response(self.app) + + def _stub_instance_get(self, uuid=None): + if uuid is None: + uuid = uuidutils.generate_uuid() + instance = fake_instance.fake_db_instance( + id=1, uuid=uuid, vm_state=vm_states.ACTIVE, + task_state=None, launched_at=timeutils.utcnow()) + instance = objects.Instance._from_db_object( + self.context, objects.Instance(), instance) + self.compute_api.get(self.context, uuid, expected_attrs=None, + want_objects=True).AndReturn(instance) + return instance + + def _stub_instance_get_failure(self, exc_info, uuid=None): + if uuid is None: + uuid = uuidutils.generate_uuid() + self.compute_api.get(self.context, uuid, expected_attrs=None, + want_objects=True).AndRaise(exc_info) + return uuid + + def _test_non_existing_instance(self, action, body_map=None): + uuid = uuidutils.generate_uuid() + self._stub_instance_get_failure( + exception.InstanceNotFound(instance_id=uuid), uuid=uuid) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % uuid, + {action: body_map.get(action)}) + self.assertEqual(404, res.status_int) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_action(self, action, body=None, method=None): + if method is None: + method = action + + instance = self._stub_instance_get() + getattr(self.compute_api, method)(self.context, instance) + + self.mox.ReplayAll() + res = self._make_request('/servers/%s/action' % instance['uuid'], + {action: None}) + self.assertEqual(202, res.status_int) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_invalid_state(self, action, method=None, body_map=None, + compute_api_args_map=None): + if method is None: + method = action + if body_map is None: + body_map = {} + if compute_api_args_map is None: + compute_api_args_map = {} + + instance = self._stub_instance_get() + + args, kwargs = compute_api_args_map.get(action, ((), {})) + + getattr(self.compute_api, method)(self.context, instance, + *args, **kwargs).AndRaise( + exception.InstanceInvalidState( + attr='vm_state', instance_uuid=instance['uuid'], + state='foo', method=method)) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance['uuid'], + {action: body_map.get(action)}) + self.assertEqual(409, res.status_int) + self.assertIn("Cannot \'%(action)s\' instance %(id)s" + % {'id': instance['uuid'], 'action': action}, res.body) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_locked_instance(self, action, method=None, body_map=None, + compute_api_args_map=None): + if method is None: + method = action + + instance = self._stub_instance_get() + + args, kwargs = (), {} + act = None + + if compute_api_args_map: + args, kwargs = compute_api_args_map.get(action, ((), {})) + act = body_map.get(action) + + getattr(self.compute_api, method)(self.context, instance, + *args, **kwargs).AndRaise( + exception.InstanceIsLocked(instance_uuid=instance['uuid'])) + self.mox.ReplayAll() + res = self._make_request('/servers/%s/action' % instance['uuid'], + {action: act}) + self.assertEqual(409, res.status_int) + self.assertIn('Instance %s is locked' % instance['uuid'], res.body) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + +class AdminActionsTestV21(CommonMixin, test.NoDBTestCase): + admin_actions = admin_actions_v21 + fake_url = '/v2/fake' + + def setUp(self): + super(AdminActionsTestV21, self).setUp() + self.controller = self.admin_actions.AdminActionsController() + self.compute_api = self.controller.compute_api + self.context = nova.context.RequestContext('fake', 'fake') + + def _fake_controller(*args, **kwargs): + return self.controller + + self.stubs.Set(self.admin_actions, 'AdminActionsController', + _fake_controller) + + self.app = self._get_app() + self.mox.StubOutWithMock(self.compute_api, 'get') + + def _get_app(self): + return fakes.wsgi_app_v21(init_only=('servers', + 'os-admin-actions'), + fake_auth_context=self.context) + + def test_actions(self): + actions = ['resetNetwork', 'injectNetworkInfo'] + method_translations = {'resetNetwork': 'reset_network', + 'injectNetworkInfo': 'inject_network_info'} + + for action in actions: + method = method_translations.get(action) + self.mox.StubOutWithMock(self.compute_api, method or action) + self._test_action(action, method=method) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_actions_with_non_existed_instance(self): + actions = ['resetNetwork', 'injectNetworkInfo', 'os-resetState'] + body_map = {'os-resetState': {'state': 'active'}} + + for action in actions: + self._test_non_existing_instance(action, + body_map=body_map) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_actions_with_locked_instance(self): + actions = ['resetNetwork', 'injectNetworkInfo'] + method_translations = {'resetNetwork': 'reset_network', + 'injectNetworkInfo': 'inject_network_info'} + + for action in actions: + method = method_translations.get(action) + self.mox.StubOutWithMock(self.compute_api, method or action) + self._test_locked_instance(action, method=method) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + +class AdminActionsTestV2(AdminActionsTestV21): + admin_actions = admin_actions_v2 + + def setUp(self): + super(AdminActionsTestV2, self).setUp() + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Admin_actions']) + + def _get_app(self): + return fakes.wsgi_app(init_only=('servers',), + fake_auth_context=self.context) + + def test_actions(self): + actions = ['pause', 'unpause', 'suspend', 'resume', 'migrate', + 'resetNetwork', 'injectNetworkInfo', 'lock', + 'unlock'] + method_translations = {'migrate': 'resize', + 'resetNetwork': 'reset_network', + 'injectNetworkInfo': 'inject_network_info'} + + for action in actions: + method = method_translations.get(action) + self.mox.StubOutWithMock(self.compute_api, method or action) + self._test_action(action, method=method) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_actions_raise_conflict_on_invalid_state(self): + actions = ['pause', 'unpause', 'suspend', 'resume', 'migrate', + 'os-migrateLive'] + method_translations = {'migrate': 'resize', + 'os-migrateLive': 'live_migrate'} + body_map = {'os-migrateLive': + {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + args_map = {'os-migrateLive': ((False, False, 'hostname'), {})} + + for action in actions: + method = method_translations.get(action) + self.mox.StubOutWithMock(self.compute_api, method or action) + self._test_invalid_state(action, method=method, body_map=body_map, + compute_api_args_map=args_map) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_actions_with_non_existed_instance(self): + actions = ['pause', 'unpause', 'suspend', 'resume', + 'resetNetwork', 'injectNetworkInfo', 'lock', + 'unlock', 'os-resetState', 'migrate', 'os-migrateLive'] + body_map = {'os-resetState': {'state': 'active'}, + 'os-migrateLive': + {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + for action in actions: + self._test_non_existing_instance(action, + body_map=body_map) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_actions_with_locked_instance(self): + actions = ['pause', 'unpause', 'suspend', 'resume', 'migrate', + 'resetNetwork', 'injectNetworkInfo', 'os-migrateLive'] + method_translations = {'migrate': 'resize', + 'resetNetwork': 'reset_network', + 'injectNetworkInfo': 'inject_network_info', + 'os-migrateLive': 'live_migrate'} + args_map = {'os-migrateLive': ((False, False, 'hostname'), {})} + body_map = {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + + for action in actions: + method = method_translations.get(action) + self.mox.StubOutWithMock(self.compute_api, method or action) + self._test_locked_instance(action, method=method, + body_map=body_map, + compute_api_args_map=args_map) + + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + def _test_migrate_exception(self, exc_info, expected_result): + self.mox.StubOutWithMock(self.compute_api, 'resize') + instance = self._stub_instance_get() + self.compute_api.resize(self.context, instance).AndRaise(exc_info) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance['uuid'], + {'migrate': None}) + self.assertEqual(expected_result, res.status_int) + + def _test_migrate_live_succeeded(self, param): + self.mox.StubOutWithMock(self.compute_api, 'live_migrate') + instance = self._stub_instance_get() + self.compute_api.live_migrate(self.context, instance, False, + False, 'hostname') + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance['uuid'], + {'os-migrateLive': param}) + self.assertEqual(202, res.status_int) + + def test_migrate_live_enabled(self): + param = {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False} + self._test_migrate_live_succeeded(param) + + def test_migrate_live_enabled_with_string_param(self): + param = {'host': 'hostname', + 'block_migration': "False", + 'disk_over_commit': "False"} + self._test_migrate_live_succeeded(param) + + def test_migrate_live_missing_dict_param(self): + body = {'os-migrateLive': {'dummy': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + res = self._make_request('/servers/FAKE/action', body) + self.assertEqual(400, res.status_int) + + def test_migrate_live_with_invalid_block_migration(self): + body = {'os-migrateLive': {'host': 'hostname', + 'block_migration': "foo", + 'disk_over_commit': False}} + res = self._make_request('/servers/FAKE/action', body) + self.assertEqual(400, res.status_int) + + def test_migrate_live_with_invalid_disk_over_commit(self): + body = {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': "foo"}} + res = self._make_request('/servers/FAKE/action', body) + self.assertEqual(400, res.status_int) + + def _test_migrate_live_failed_with_exception(self, fake_exc, + uuid=None): + self.mox.StubOutWithMock(self.compute_api, 'live_migrate') + + instance = self._stub_instance_get(uuid=uuid) + self.compute_api.live_migrate(self.context, instance, False, + False, 'hostname').AndRaise(fake_exc) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {'os-migrateLive': + {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}}) + self.assertEqual(400, res.status_int) + self.assertIn(unicode(fake_exc), res.body) + + def test_migrate_live_compute_service_unavailable(self): + self._test_migrate_live_failed_with_exception( + exception.ComputeServiceUnavailable(host='host')) + + def test_migrate_live_invalid_hypervisor_type(self): + self._test_migrate_live_failed_with_exception( + exception.InvalidHypervisorType()) + + def test_migrate_live_invalid_cpu_info(self): + self._test_migrate_live_failed_with_exception( + exception.InvalidCPUInfo(reason="")) + + def test_migrate_live_unable_to_migrate_to_self(self): + uuid = uuidutils.generate_uuid() + self._test_migrate_live_failed_with_exception( + exception.UnableToMigrateToSelf(instance_id=uuid, + host='host'), + uuid=uuid) + + def test_migrate_live_destination_hypervisor_too_old(self): + self._test_migrate_live_failed_with_exception( + exception.DestinationHypervisorTooOld()) + + def test_migrate_live_no_valid_host(self): + self._test_migrate_live_failed_with_exception( + exception.NoValidHost(reason='')) + + def test_migrate_live_invalid_local_storage(self): + self._test_migrate_live_failed_with_exception( + exception.InvalidLocalStorage(path='', reason='')) + + def test_migrate_live_invalid_shared_storage(self): + self._test_migrate_live_failed_with_exception( + exception.InvalidSharedStorage(path='', reason='')) + + def test_migrate_live_hypervisor_unavailable(self): + self._test_migrate_live_failed_with_exception( + exception.HypervisorUnavailable(host="")) + + def test_migrate_live_instance_not_running(self): + self._test_migrate_live_failed_with_exception( + exception.InstanceNotRunning(instance_id="")) + + def test_migrate_live_migration_pre_check_error(self): + self._test_migrate_live_failed_with_exception( + exception.MigrationPreCheckError(reason='')) + + def test_unlock_not_authorized(self): + self.mox.StubOutWithMock(self.compute_api, 'unlock') + + instance = self._stub_instance_get() + + self.compute_api.unlock(self.context, instance).AndRaise( + exception.PolicyNotAuthorized(action='unlock')) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance['uuid'], + {'unlock': None}) + self.assertEqual(403, res.status_int) + + +class CreateBackupTestsV2(CommonMixin, test.NoDBTestCase): + fake_url = '/v2/fake' + + def setUp(self): + super(CreateBackupTestsV2, self).setUp() + self.controller = admin_actions_v2.AdminActionsController() + self.compute_api = self.controller.compute_api + self.context = nova.context.RequestContext('fake', 'fake') + + def _fake_controller(*args, **kwargs): + return self.controller + + self.stubs.Set(admin_actions_v2, 'AdminActionsController', + _fake_controller) + + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Admin_actions']) + + self.app = fakes.wsgi_app(init_only=('servers',), + fake_auth_context=self.context) + self.mox.StubOutWithMock(self.compute_api, 'get') + self.mox.StubOutWithMock(common, + 'check_img_metadata_properties_quota') + self.mox.StubOutWithMock(self.compute_api, + 'backup') + + def _make_url(self, uuid): + return '/servers/%s/action' % uuid + + def test_create_backup_with_metadata(self): + metadata = {'123': 'asdf'} + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + 'metadata': metadata, + }, + } + + image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1', + properties=metadata) + + common.check_img_metadata_properties_quota(self.context, metadata) + instance = self._stub_instance_get() + self.compute_api.backup(self.context, instance, 'Backup 1', + 'daily', 1, + extra_properties=metadata).AndReturn(image) + + self.mox.ReplayAll() + + res = self._make_request(self._make_url(instance['uuid']), body=body) + self.assertEqual(202, res.status_int) + self.assertIn('fake-image-id', res.headers['Location']) + + def test_create_backup_no_name(self): + # Name is required for backups. + body = { + 'createBackup': { + 'backup_type': 'daily', + 'rotation': 1, + }, + } + res = self._make_request(self._make_url('fake'), body=body) + self.assertEqual(400, res.status_int) + + def test_create_backup_no_rotation(self): + # Rotation is required for backup requests. + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + }, + } + res = self._make_request(self._make_url('fake'), body=body) + self.assertEqual(400, res.status_int) + + def test_create_backup_negative_rotation(self): + """Rotation must be greater than or equal to zero + for backup requests + """ + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': -1, + }, + } + res = self._make_request(self._make_url('fake'), body=body) + self.assertEqual(400, res.status_int) + + def test_create_backup_no_backup_type(self): + # Backup Type (daily or weekly) is required for backup requests. + body = { + 'createBackup': { + 'name': 'Backup 1', + 'rotation': 1, + }, + } + res = self._make_request(self._make_url('fake'), body=body) + self.assertEqual(400, res.status_int) + + def test_create_backup_bad_entity(self): + body = {'createBackup': 'go'} + res = self._make_request(self._make_url('fake'), body=body) + self.assertEqual(400, res.status_int) + + def test_create_backup_rotation_is_zero(self): + # The happy path for creating backups if rotation is zero. + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 0, + }, + } + + image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1', + properties={}) + common.check_img_metadata_properties_quota(self.context, {}) + instance = self._stub_instance_get() + self.compute_api.backup(self.context, instance, 'Backup 1', + 'daily', 0, + extra_properties={}).AndReturn(image) + + self.mox.ReplayAll() + + res = self._make_request(self._make_url(instance['uuid']), body=body) + self.assertEqual(202, res.status_int) + self.assertNotIn('Location', res.headers) + + def test_create_backup_rotation_is_positive(self): + # The happy path for creating backups if rotation is positive. + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + }, + } + + image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1', + properties={}) + common.check_img_metadata_properties_quota(self.context, {}) + instance = self._stub_instance_get() + self.compute_api.backup(self.context, instance, 'Backup 1', + 'daily', 1, + extra_properties={}).AndReturn(image) + + self.mox.ReplayAll() + + res = self._make_request(self._make_url(instance['uuid']), body=body) + self.assertEqual(202, res.status_int) + self.assertIn('fake-image-id', res.headers['Location']) + + def test_create_backup_raises_conflict_on_invalid_state(self): + body_map = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + }, + } + args_map = { + 'createBackup': ( + ('Backup 1', 'daily', 1), {'extra_properties': {}} + ), + } + common.check_img_metadata_properties_quota(self.context, {}) + self._test_invalid_state('createBackup', method='backup', + body_map=body_map, + compute_api_args_map=args_map) + + def test_create_backup_with_non_existed_instance(self): + body_map = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + }, + } + common.check_img_metadata_properties_quota(self.context, {}) + self._test_non_existing_instance('createBackup', + body_map=body_map) + + def test_create_backup_with_invalid_createBackup(self): + body = { + 'createBackupup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + }, + } + res = self._make_request(self._make_url('fake'), body=body) + self.assertEqual(400, res.status_int) + + +class ResetStateTestsV21(test.NoDBTestCase): + admin_act = admin_actions_v21 + bad_request = exception.ValidationError + fake_url = '/servers' + + def setUp(self): + super(ResetStateTestsV21, self).setUp() + self.uuid = uuidutils.generate_uuid() + self.admin_api = self.admin_act.AdminActionsController() + self.compute_api = self.admin_api.compute_api + + url = '%s/%s/action' % (self.fake_url, self.uuid) + self.request = self._get_request(url) + self.context = self.request.environ['nova.context'] + + def _get_request(self, url): + return fakes.HTTPRequest.blank(url) + + def test_no_state(self): + self.assertRaises(self.bad_request, + self.admin_api._reset_state, + self.request, self.uuid, + body={"os-resetState": None}) + + def test_bad_state(self): + self.assertRaises(self.bad_request, + self.admin_api._reset_state, + self.request, self.uuid, + body={"os-resetState": {"state": "spam"}}) + + def test_no_instance(self): + self.mox.StubOutWithMock(self.compute_api, 'get') + exc = exception.InstanceNotFound(instance_id='inst_ud') + self.compute_api.get(self.context, self.uuid, expected_attrs=None, + want_objects=True).AndRaise(exc) + self.mox.ReplayAll() + + self.assertRaises(webob.exc.HTTPNotFound, + self.admin_api._reset_state, + self.request, self.uuid, + body={"os-resetState": {"state": "active"}}) + + def _setup_mock(self, expected): + instance = objects.Instance() + instance.uuid = self.uuid + instance.vm_state = 'fake' + instance.task_state = 'fake' + instance.obj_reset_changes() + + self.mox.StubOutWithMock(instance, 'save') + self.mox.StubOutWithMock(self.compute_api, 'get') + + def check_state(admin_state_reset=True): + self.assertEqual(set(expected.keys()), + instance.obj_what_changed()) + for k, v in expected.items(): + self.assertEqual(v, getattr(instance, k), + "Instance.%s doesn't match" % k) + instance.obj_reset_changes() + + self.compute_api.get(self.context, instance.uuid, expected_attrs=None, + want_objects=True).AndReturn(instance) + instance.save(admin_state_reset=True).WithSideEffects(check_state) + + def test_reset_active(self): + self._setup_mock(dict(vm_state=vm_states.ACTIVE, + task_state=None)) + self.mox.ReplayAll() + + body = {"os-resetState": {"state": "active"}} + result = self.admin_api._reset_state(self.request, self.uuid, + body=body) + # NOTE: on v2.1, http status code is set as wsgi_code of API + # method instead of status_int in a response object. + if isinstance(self.admin_api, + admin_actions_v21.AdminActionsController): + status_int = self.admin_api._reset_state.wsgi_code + else: + status_int = result.status_int + self.assertEqual(202, status_int) + + def test_reset_error(self): + self._setup_mock(dict(vm_state=vm_states.ERROR, + task_state=None)) + self.mox.ReplayAll() + body = {"os-resetState": {"state": "error"}} + result = self.admin_api._reset_state(self.request, self.uuid, + body=body) + # NOTE: on v2.1, http status code is set as wsgi_code of API + # method instead of status_int in a response object. + if isinstance(self.admin_api, + admin_actions_v21.AdminActionsController): + status_int = self.admin_api._reset_state.wsgi_code + else: + status_int = result.status_int + self.assertEqual(202, status_int) + + +class ResetStateTestsV2(ResetStateTestsV21): + admin_act = admin_actions_v2 + bad_request = webob.exc.HTTPBadRequest + fake_url = '/fake/servers' diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_admin_password.py b/nova/tests/unit/api/openstack/compute/contrib/test_admin_password.py new file mode 100644 index 0000000000..4ddfc08dcc --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_admin_password.py @@ -0,0 +1,111 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# 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. +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.plugins.v3 import admin_password \ + as admin_password_v21 +from nova.compute import api as compute_api +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + + +def fake_get(self, context, id, expected_attrs=None, want_objects=False): + return {'uuid': id} + + +def fake_get_non_existent(self, context, id, expected_attrs=None, + want_objects=False): + raise exception.InstanceNotFound(instance_id=id) + + +def fake_set_admin_password(self, context, instance, password=None): + pass + + +def fake_set_admin_password_failed(self, context, instance, password=None): + raise exception.InstancePasswordSetFailed(instance=instance, reason='') + + +def fake_set_admin_password_not_implemented(self, context, instance, + password=None): + raise NotImplementedError() + + +class AdminPasswordTestV21(test.NoDBTestCase): + plugin = admin_password_v21 + + def setUp(self): + super(AdminPasswordTestV21, self).setUp() + self.stubs.Set(compute_api.API, 'set_admin_password', + fake_set_admin_password) + self.stubs.Set(compute_api.API, 'get', fake_get) + self.app = fakes.wsgi_app_v21(init_only=('servers', + self.plugin.ALIAS)) + + def _make_request(self, body): + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.content_type = 'application/json' + res = req.get_response(self.app) + return res + + def test_change_password(self): + body = {'changePassword': {'adminPass': 'test'}} + res = self._make_request(body) + self.assertEqual(res.status_int, 202) + + def test_change_password_empty_string(self): + body = {'changePassword': {'adminPass': ''}} + res = self._make_request(body) + self.assertEqual(res.status_int, 202) + + def test_change_password_with_non_implement(self): + body = {'changePassword': {'adminPass': 'test'}} + self.stubs.Set(compute_api.API, 'set_admin_password', + fake_set_admin_password_not_implemented) + res = self._make_request(body) + self.assertEqual(res.status_int, 501) + + def test_change_password_with_non_existed_instance(self): + body = {'changePassword': {'adminPass': 'test'}} + self.stubs.Set(compute_api.API, 'get', fake_get_non_existent) + res = self._make_request(body) + self.assertEqual(res.status_int, 404) + + def test_change_password_with_non_string_password(self): + body = {'changePassword': {'adminPass': 1234}} + res = self._make_request(body) + self.assertEqual(res.status_int, 400) + + def test_change_password_failed(self): + body = {'changePassword': {'adminPass': 'test'}} + self.stubs.Set(compute_api.API, 'set_admin_password', + fake_set_admin_password_failed) + res = self._make_request(body) + self.assertEqual(res.status_int, 409) + + def test_change_password_without_admin_password(self): + body = {'changPassword': {}} + res = self._make_request(body) + self.assertEqual(res.status_int, 400) + + def test_change_password_none(self): + body = {'changePassword': None} + res = self._make_request(body) + self.assertEqual(res.status_int, 400) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_agents.py b/nova/tests/unit/api/openstack/compute/contrib/test_agents.py new file mode 100644 index 0000000000..b8c6f857b6 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_agents.py @@ -0,0 +1,352 @@ +# Copyright 2012 IBM Corp. +# +# 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 +import webob.exc + +from nova.api.openstack.compute.contrib import agents as agents_v2 +from nova.api.openstack.compute.plugins.v3 import agents as agents_v21 +from nova import context +from nova import db +from nova.db.sqlalchemy import models +from nova import exception +from nova import test + +fake_agents_list = [{'hypervisor': 'kvm', 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'http://example.com/path/to/resource', + 'md5hash': 'add6bb58e139be103324d04d82d8f545', + 'id': 1}, + {'hypervisor': 'kvm', 'os': 'linux', + 'architecture': 'x86', + 'version': '16.0', + 'url': 'http://example.com/path/to/resource1', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'id': 2}, + {'hypervisor': 'xen', 'os': 'linux', + 'architecture': 'x86', + 'version': '16.0', + 'url': 'http://example.com/path/to/resource2', + 'md5hash': 'add6bb58e139be103324d04d82d8f547', + 'id': 3}, + {'hypervisor': 'xen', 'os': 'win', + 'architecture': 'power', + 'version': '7.0', + 'url': 'http://example.com/path/to/resource3', + 'md5hash': 'add6bb58e139be103324d04d82d8f548', + 'id': 4}, + ] + + +def fake_agent_build_get_all(context, hypervisor): + agent_build_all = [] + for agent in fake_agents_list: + if hypervisor and hypervisor != agent['hypervisor']: + continue + agent_build_ref = models.AgentBuild() + agent_build_ref.update(agent) + agent_build_all.append(agent_build_ref) + return agent_build_all + + +def fake_agent_build_update(context, agent_build_id, values): + pass + + +def fake_agent_build_destroy(context, agent_update_id): + pass + + +def fake_agent_build_create(context, values): + values['id'] = 1 + agent_build_ref = models.AgentBuild() + agent_build_ref.update(values) + return agent_build_ref + + +class FakeRequest(object): + environ = {"nova.context": context.get_admin_context()} + GET = {} + + +class FakeRequestWithHypervisor(object): + environ = {"nova.context": context.get_admin_context()} + GET = {'hypervisor': 'kvm'} + + +class AgentsTestV21(test.NoDBTestCase): + controller = agents_v21.AgentController() + validation_error = exception.ValidationError + + def setUp(self): + super(AgentsTestV21, self).setUp() + + self.stubs.Set(db, "agent_build_get_all", + fake_agent_build_get_all) + self.stubs.Set(db, "agent_build_update", + fake_agent_build_update) + self.stubs.Set(db, "agent_build_destroy", + fake_agent_build_destroy) + self.stubs.Set(db, "agent_build_create", + fake_agent_build_create) + self.context = context.get_admin_context() + + def test_agents_create(self): + req = FakeRequest() + body = {'agent': {'hypervisor': 'kvm', + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'http://example.com/path/to/resource', + 'md5hash': 'add6bb58e139be103324d04d82d8f545'}} + response = {'agent': {'hypervisor': 'kvm', + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'http://example.com/path/to/resource', + 'md5hash': 'add6bb58e139be103324d04d82d8f545', + 'agent_id': 1}} + res_dict = self.controller.create(req, body=body) + self.assertEqual(res_dict, response) + + def _test_agents_create_key_error(self, key): + req = FakeRequest() + body = {'agent': {'hypervisor': 'kvm', + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'xxx://xxxx/xxx/xxx', + 'md5hash': 'add6bb58e139be103324d04d82d8f545'}} + body['agent'].pop(key) + self.assertRaises(self.validation_error, + self.controller.create, req, body=body) + + def test_agents_create_without_hypervisor(self): + self._test_agents_create_key_error('hypervisor') + + def test_agents_create_without_os(self): + self._test_agents_create_key_error('os') + + def test_agents_create_without_architecture(self): + self._test_agents_create_key_error('architecture') + + def test_agents_create_without_version(self): + self._test_agents_create_key_error('version') + + def test_agents_create_without_url(self): + self._test_agents_create_key_error('url') + + def test_agents_create_without_md5hash(self): + self._test_agents_create_key_error('md5hash') + + def test_agents_create_with_wrong_type(self): + req = FakeRequest() + body = {'agent': None} + self.assertRaises(self.validation_error, + self.controller.create, req, body=body) + + def test_agents_create_with_empty_type(self): + req = FakeRequest() + body = {} + self.assertRaises(self.validation_error, + self.controller.create, req, body=body) + + def test_agents_create_with_existed_agent(self): + def fake_agent_build_create_with_exited_agent(context, values): + raise exception.AgentBuildExists(**values) + + self.stubs.Set(db, 'agent_build_create', + fake_agent_build_create_with_exited_agent) + req = FakeRequest() + body = {'agent': {'hypervisor': 'kvm', + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'xxx://xxxx/xxx/xxx', + 'md5hash': 'add6bb58e139be103324d04d82d8f545'}} + self.assertRaises(webob.exc.HTTPConflict, self.controller.create, req, + body=body) + + def _test_agents_create_with_invalid_length(self, key): + req = FakeRequest() + body = {'agent': {'hypervisor': 'kvm', + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'http://example.com/path/to/resource', + 'md5hash': 'add6bb58e139be103324d04d82d8f545'}} + body['agent'][key] = 'x' * 256 + self.assertRaises(self.validation_error, + self.controller.create, req, body=body) + + def test_agents_create_with_invalid_length_hypervisor(self): + self._test_agents_create_with_invalid_length('hypervisor') + + def test_agents_create_with_invalid_length_os(self): + self._test_agents_create_with_invalid_length('os') + + def test_agents_create_with_invalid_length_architecture(self): + self._test_agents_create_with_invalid_length('architecture') + + def test_agents_create_with_invalid_length_version(self): + self._test_agents_create_with_invalid_length('version') + + def test_agents_create_with_invalid_length_url(self): + self._test_agents_create_with_invalid_length('url') + + def test_agents_create_with_invalid_length_md5hash(self): + self._test_agents_create_with_invalid_length('md5hash') + + def test_agents_delete(self): + req = FakeRequest() + self.controller.delete(req, 1) + + def test_agents_delete_with_id_not_found(self): + with mock.patch.object(db, 'agent_build_destroy', + side_effect=exception.AgentBuildNotFound(id=1)): + req = FakeRequest() + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, req, 1) + + def test_agents_list(self): + req = FakeRequest() + res_dict = self.controller.index(req) + agents_list = [{'hypervisor': 'kvm', 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'http://example.com/path/to/resource', + 'md5hash': 'add6bb58e139be103324d04d82d8f545', + 'agent_id': 1}, + {'hypervisor': 'kvm', 'os': 'linux', + 'architecture': 'x86', + 'version': '16.0', + 'url': 'http://example.com/path/to/resource1', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'agent_id': 2}, + {'hypervisor': 'xen', 'os': 'linux', + 'architecture': 'x86', + 'version': '16.0', + 'url': 'http://example.com/path/to/resource2', + 'md5hash': 'add6bb58e139be103324d04d82d8f547', + 'agent_id': 3}, + {'hypervisor': 'xen', 'os': 'win', + 'architecture': 'power', + 'version': '7.0', + 'url': 'http://example.com/path/to/resource3', + 'md5hash': 'add6bb58e139be103324d04d82d8f548', + 'agent_id': 4}, + ] + self.assertEqual(res_dict, {'agents': agents_list}) + + def test_agents_list_with_hypervisor(self): + req = FakeRequestWithHypervisor() + res_dict = self.controller.index(req) + response = [{'hypervisor': 'kvm', 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'http://example.com/path/to/resource', + 'md5hash': 'add6bb58e139be103324d04d82d8f545', + 'agent_id': 1}, + {'hypervisor': 'kvm', 'os': 'linux', + 'architecture': 'x86', + 'version': '16.0', + 'url': 'http://example.com/path/to/resource1', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'agent_id': 2}, + ] + self.assertEqual(res_dict, {'agents': response}) + + def test_agents_update(self): + req = FakeRequest() + body = {'para': {'version': '7.0', + 'url': 'http://example.com/path/to/resource', + 'md5hash': 'add6bb58e139be103324d04d82d8f545'}} + response = {'agent': {'agent_id': 1, + 'version': '7.0', + 'url': 'http://example.com/path/to/resource', + 'md5hash': 'add6bb58e139be103324d04d82d8f545'}} + res_dict = self.controller.update(req, 1, body=body) + self.assertEqual(res_dict, response) + + def _test_agents_update_key_error(self, key): + req = FakeRequest() + body = {'para': {'version': '7.0', + 'url': 'xxx://xxxx/xxx/xxx', + 'md5hash': 'add6bb58e139be103324d04d82d8f545'}} + body['para'].pop(key) + self.assertRaises(self.validation_error, + self.controller.update, req, 1, body=body) + + def test_agents_update_without_version(self): + self._test_agents_update_key_error('version') + + def test_agents_update_without_url(self): + self._test_agents_update_key_error('url') + + def test_agents_update_without_md5hash(self): + self._test_agents_update_key_error('md5hash') + + def test_agents_update_with_wrong_type(self): + req = FakeRequest() + body = {'agent': None} + self.assertRaises(self.validation_error, + self.controller.update, req, 1, body=body) + + def test_agents_update_with_empty(self): + req = FakeRequest() + body = {} + self.assertRaises(self.validation_error, + self.controller.update, req, 1, body=body) + + def test_agents_update_value_error(self): + req = FakeRequest() + body = {'para': {'version': '7.0', + 'url': 1111, + 'md5hash': 'add6bb58e139be103324d04d82d8f545'}} + self.assertRaises(self.validation_error, + self.controller.update, req, 1, body=body) + + def _test_agents_update_with_invalid_length(self, key): + req = FakeRequest() + body = {'para': {'version': '7.0', + 'url': 'http://example.com/path/to/resource', + 'md5hash': 'add6bb58e139be103324d04d82d8f545'}} + body['para'][key] = 'x' * 256 + self.assertRaises(self.validation_error, + self.controller.update, req, 1, body=body) + + def test_agents_update_with_invalid_length_version(self): + self._test_agents_update_with_invalid_length('version') + + def test_agents_update_with_invalid_length_url(self): + self._test_agents_update_with_invalid_length('url') + + def test_agents_update_with_invalid_length_md5hash(self): + self._test_agents_update_with_invalid_length('md5hash') + + def test_agents_update_with_id_not_found(self): + with mock.patch.object(db, 'agent_build_update', + side_effect=exception.AgentBuildNotFound(id=1)): + req = FakeRequest() + body = {'para': {'version': '7.0', + 'url': 'http://example.com/path/to/resource', + 'md5hash': 'add6bb58e139be103324d04d82d8f545'}} + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, req, 1, body=body) + + +class AgentsTestV2(AgentsTestV21): + controller = agents_v2.AgentController() + validation_error = webob.exc.HTTPBadRequest diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_aggregates.py b/nova/tests/unit/api/openstack/compute/contrib/test_aggregates.py new file mode 100644 index 0000000000..9b52146fa1 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_aggregates.py @@ -0,0 +1,670 @@ +# Copyright (c) 2012 Citrix Systems, Inc. +# 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. + +"""Tests for the aggregates admin api.""" + +import mock +from webob import exc + +from nova.api.openstack.compute.contrib import aggregates as aggregates_v2 +from nova.api.openstack.compute.plugins.v3 import aggregates as aggregates_v21 +from nova import context +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import matchers + +AGGREGATE_LIST = [ + {"name": "aggregate1", "id": "1", "availability_zone": "nova1"}, + {"name": "aggregate2", "id": "2", "availability_zone": "nova1"}, + {"name": "aggregate3", "id": "3", "availability_zone": "nova2"}, + {"name": "aggregate1", "id": "4", "availability_zone": "nova1"}] +AGGREGATE = {"name": "aggregate1", + "id": "1", + "availability_zone": "nova1", + "metadata": {"foo": "bar"}, + "hosts": ["host1, host2"]} + +FORMATTED_AGGREGATE = {"name": "aggregate1", + "id": "1", + "availability_zone": "nova1"} + + +class FakeRequest(object): + environ = {"nova.context": context.get_admin_context()} + + +class AggregateTestCaseV21(test.NoDBTestCase): + """Test Case for aggregates admin api.""" + + add_host = 'self.controller._add_host' + remove_host = 'self.controller._remove_host' + set_metadata = 'self.controller._set_metadata' + bad_request = exception.ValidationError + + def _set_up(self): + self.controller = aggregates_v21.AggregateController() + self.req = fakes.HTTPRequest.blank('/v3/os-aggregates', + use_admin_context=True) + self.user_req = fakes.HTTPRequest.blank('/v3/os-aggregates') + self.context = self.req.environ['nova.context'] + + def setUp(self): + super(AggregateTestCaseV21, self).setUp() + self._set_up() + + def test_index(self): + def stub_list_aggregates(context): + if context is None: + raise Exception() + return AGGREGATE_LIST + self.stubs.Set(self.controller.api, 'get_aggregate_list', + stub_list_aggregates) + + result = self.controller.index(self.req) + + self.assertEqual(AGGREGATE_LIST, result["aggregates"]) + + def test_index_no_admin(self): + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.index, + self.user_req) + + def test_create(self): + def stub_create_aggregate(context, name, availability_zone): + self.assertEqual(context, self.context, "context") + self.assertEqual("test", name, "name") + self.assertEqual("nova1", availability_zone, "availability_zone") + return AGGREGATE + self.stubs.Set(self.controller.api, "create_aggregate", + stub_create_aggregate) + + result = self.controller.create(self.req, body={"aggregate": + {"name": "test", + "availability_zone": "nova1"}}) + self.assertEqual(FORMATTED_AGGREGATE, result["aggregate"]) + + def test_create_no_admin(self): + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.create, self.user_req, + body={"aggregate": + {"name": "test", + "availability_zone": "nova1"}}) + + def test_create_with_duplicate_aggregate_name(self): + def stub_create_aggregate(context, name, availability_zone): + raise exception.AggregateNameExists(aggregate_name=name) + self.stubs.Set(self.controller.api, "create_aggregate", + stub_create_aggregate) + + self.assertRaises(exc.HTTPConflict, self.controller.create, + self.req, body={"aggregate": + {"name": "test", + "availability_zone": "nova1"}}) + + def test_create_with_incorrect_availability_zone(self): + def stub_create_aggregate(context, name, availability_zone): + raise exception.InvalidAggregateAction(action='create_aggregate', + aggregate_id="'N/A'", + reason='invalid zone') + + self.stubs.Set(self.controller.api, "create_aggregate", + stub_create_aggregate) + + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, + self.req, body={"aggregate": + {"name": "test", + "availability_zone": "nova_bad"}}) + + def test_create_with_no_aggregate(self): + self.assertRaises(self.bad_request, self.controller.create, + self.req, body={"foo": + {"name": "test", + "availability_zone": "nova1"}}) + + def test_create_with_no_name(self): + self.assertRaises(self.bad_request, self.controller.create, + self.req, body={"aggregate": + {"foo": "test", + "availability_zone": "nova1"}}) + + def test_create_with_no_availability_zone(self): + def stub_create_aggregate(context, name, availability_zone): + self.assertEqual(context, self.context, "context") + self.assertEqual("test", name, "name") + self.assertIsNone(availability_zone, "availability_zone") + return AGGREGATE + self.stubs.Set(self.controller.api, "create_aggregate", + stub_create_aggregate) + + result = self.controller.create(self.req, + body={"aggregate": {"name": "test"}}) + self.assertEqual(FORMATTED_AGGREGATE, result["aggregate"]) + + def test_create_with_null_name(self): + self.assertRaises(self.bad_request, self.controller.create, + self.req, body={"aggregate": + {"name": "", + "availability_zone": "nova1"}}) + + def test_create_with_name_too_long(self): + self.assertRaises(self.bad_request, self.controller.create, + self.req, body={"aggregate": + {"name": "x" * 256, + "availability_zone": "nova1"}}) + + def test_create_with_availability_zone_too_long(self): + self.assertRaises(self.bad_request, self.controller.create, + self.req, body={"aggregate": + {"name": "test", + "availability_zone": "x" * 256}}) + + def test_create_with_null_availability_zone(self): + aggregate = {"name": "aggregate1", + "id": "1", + "availability_zone": None, + "metadata": {}, + "hosts": []} + + formatted_aggregate = {"name": "aggregate1", + "id": "1", + "availability_zone": None} + + def stub_create_aggregate(context, name, az_name): + self.assertEqual(context, self.context, "context") + self.assertEqual("aggregate1", name, "name") + self.assertIsNone(az_name, "availability_zone") + return aggregate + self.stubs.Set(self.controller.api, 'create_aggregate', + stub_create_aggregate) + + result = self.controller.create(self.req, + body={"aggregate": + {"name": "aggregate1", + "availability_zone": None}}) + self.assertEqual(formatted_aggregate, result["aggregate"]) + + def test_create_with_empty_availability_zone(self): + self.assertRaises(self.bad_request, self.controller.create, + self.req, body={"aggregate": + {"name": "test", + "availability_zone": ""}}) + + def test_create_with_extra_invalid_arg(self): + self.assertRaises(self.bad_request, self.controller.create, + self.req, body={"name": "test", + "availability_zone": "nova1", + "foo": 'bar'}) + + def test_show(self): + def stub_get_aggregate(context, id): + self.assertEqual(context, self.context, "context") + self.assertEqual("1", id, "id") + return AGGREGATE + self.stubs.Set(self.controller.api, 'get_aggregate', + stub_get_aggregate) + + aggregate = self.controller.show(self.req, "1") + + self.assertEqual(AGGREGATE, aggregate["aggregate"]) + + def test_show_no_admin(self): + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.show, + self.user_req, "1") + + def test_show_with_invalid_id(self): + def stub_get_aggregate(context, id): + raise exception.AggregateNotFound(aggregate_id=2) + + self.stubs.Set(self.controller.api, 'get_aggregate', + stub_get_aggregate) + + self.assertRaises(exc.HTTPNotFound, + self.controller.show, self.req, "2") + + def test_update(self): + body = {"aggregate": {"name": "new_name", + "availability_zone": "nova1"}} + + def stub_update_aggregate(context, aggregate, values): + self.assertEqual(context, self.context, "context") + self.assertEqual("1", aggregate, "aggregate") + self.assertEqual(body["aggregate"], values, "values") + return AGGREGATE + self.stubs.Set(self.controller.api, "update_aggregate", + stub_update_aggregate) + + result = self.controller.update(self.req, "1", body=body) + + self.assertEqual(AGGREGATE, result["aggregate"]) + + def test_update_no_admin(self): + body = {"aggregate": {"availability_zone": "nova"}} + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.update, + self.user_req, "1", body=body) + + def test_update_with_only_name(self): + body = {"aggregate": {"name": "new_name"}} + + def stub_update_aggregate(context, aggregate, values): + return AGGREGATE + self.stubs.Set(self.controller.api, "update_aggregate", + stub_update_aggregate) + + result = self.controller.update(self.req, "1", body=body) + + self.assertEqual(AGGREGATE, result["aggregate"]) + + def test_update_with_only_availability_zone(self): + body = {"aggregate": {"availability_zone": "nova1"}} + + def stub_update_aggregate(context, aggregate, values): + return AGGREGATE + self.stubs.Set(self.controller.api, "update_aggregate", + stub_update_aggregate) + result = self.controller.update(self.req, "1", body=body) + self.assertEqual(AGGREGATE, result["aggregate"]) + + def test_update_with_no_updates(self): + test_metadata = {"aggregate": {}} + self.assertRaises(self.bad_request, self.controller.update, + self.req, "2", body=test_metadata) + + def test_update_with_no_update_key(self): + test_metadata = {"asdf": {}} + self.assertRaises(self.bad_request, self.controller.update, + self.req, "2", body=test_metadata) + + def test_update_with_wrong_updates(self): + test_metadata = {"aggregate": {"status": "disable", + "foo": "bar"}} + self.assertRaises(self.bad_request, self.controller.update, + self.req, "2", body=test_metadata) + + def test_update_with_null_name(self): + test_metadata = {"aggregate": {"name": ""}} + self.assertRaises(self.bad_request, self.controller.update, + self.req, "2", body=test_metadata) + + def test_update_with_name_too_long(self): + test_metadata = {"aggregate": {"name": "x" * 256}} + self.assertRaises(self.bad_request, self.controller.update, + self.req, "2", body=test_metadata) + + def test_update_with_availability_zone_too_long(self): + test_metadata = {"aggregate": {"availability_zone": "x" * 256}} + self.assertRaises(self.bad_request, self.controller.update, + self.req, "2", body=test_metadata) + + def test_update_with_empty_availability_zone(self): + test_metadata = {"aggregate": {"availability_zone": ""}} + self.assertRaises(self.bad_request, self.controller.update, + self.req, "2", body=test_metadata) + + def test_update_with_null_availability_zone(self): + body = {"aggregate": {"availability_zone": None}} + aggre = {"name": "aggregate1", + "id": "1", + "availability_zone": None} + + def stub_update_aggregate(context, aggregate, values): + self.assertEqual(context, self.context, "context") + self.assertEqual("1", aggregate, "aggregate") + self.assertIsNone(values["availability_zone"], "availability_zone") + return aggre + self.stubs.Set(self.controller.api, "update_aggregate", + stub_update_aggregate) + + result = self.controller.update(self.req, "1", body=body) + + self.assertEqual(aggre, result["aggregate"]) + + def test_update_with_bad_aggregate(self): + test_metadata = {"aggregate": {"name": "test_name"}} + + def stub_update_aggregate(context, aggregate, metadata): + raise exception.AggregateNotFound(aggregate_id=2) + self.stubs.Set(self.controller.api, "update_aggregate", + stub_update_aggregate) + + self.assertRaises(exc.HTTPNotFound, self.controller.update, + self.req, "2", body=test_metadata) + + def test_update_with_duplicated_name(self): + test_metadata = {"aggregate": {"name": "test_name"}} + + def stub_update_aggregate(context, aggregate, metadata): + raise exception.AggregateNameExists(aggregate_name="test_name") + + self.stubs.Set(self.controller.api, "update_aggregate", + stub_update_aggregate) + self.assertRaises(exc.HTTPConflict, self.controller.update, + self.req, "2", body=test_metadata) + + def test_invalid_action(self): + body = {"append_host": {"host": "host1"}} + self.assertRaises(self.bad_request, + eval(self.add_host), self.req, "1", body=body) + + def test_update_with_invalid_action(self): + with mock.patch.object(self.controller.api, "update_aggregate", + side_effect=exception.InvalidAggregateAction( + action='invalid', aggregate_id='agg1', reason= "not empty")): + body = {"aggregate": {"availability_zone": "nova"}} + self.assertRaises(exc.HTTPBadRequest, self.controller.update, + self.req, "1", body=body) + + def test_add_host(self): + def stub_add_host_to_aggregate(context, aggregate, host): + self.assertEqual(context, self.context, "context") + self.assertEqual("1", aggregate, "aggregate") + self.assertEqual("host1", host, "host") + return AGGREGATE + self.stubs.Set(self.controller.api, "add_host_to_aggregate", + stub_add_host_to_aggregate) + + aggregate = eval(self.add_host)(self.req, "1", + body={"add_host": {"host": + "host1"}}) + + self.assertEqual(aggregate["aggregate"], AGGREGATE) + + def test_add_host_no_admin(self): + self.assertRaises(exception.PolicyNotAuthorized, + eval(self.add_host), + self.user_req, "1", + body={"add_host": {"host": "host1"}}) + + def test_add_host_with_already_added_host(self): + def stub_add_host_to_aggregate(context, aggregate, host): + raise exception.AggregateHostExists(aggregate_id=aggregate, + host=host) + self.stubs.Set(self.controller.api, "add_host_to_aggregate", + stub_add_host_to_aggregate) + + self.assertRaises(exc.HTTPConflict, eval(self.add_host), + self.req, "1", + body={"add_host": {"host": "host1"}}) + + def test_add_host_with_bad_aggregate(self): + def stub_add_host_to_aggregate(context, aggregate, host): + raise exception.AggregateNotFound(aggregate_id=aggregate) + self.stubs.Set(self.controller.api, "add_host_to_aggregate", + stub_add_host_to_aggregate) + + self.assertRaises(exc.HTTPNotFound, eval(self.add_host), + self.req, "bogus_aggregate", + body={"add_host": {"host": "host1"}}) + + def test_add_host_with_bad_host(self): + def stub_add_host_to_aggregate(context, aggregate, host): + raise exception.ComputeHostNotFound(host=host) + self.stubs.Set(self.controller.api, "add_host_to_aggregate", + stub_add_host_to_aggregate) + + self.assertRaises(exc.HTTPNotFound, eval(self.add_host), + self.req, "1", + body={"add_host": {"host": "bogus_host"}}) + + def test_add_host_with_missing_host(self): + self.assertRaises(self.bad_request, eval(self.add_host), + self.req, "1", body={"add_host": {"asdf": "asdf"}}) + + def test_add_host_with_invalid_format_host(self): + self.assertRaises(self.bad_request, eval(self.add_host), + self.req, "1", body={"add_host": {"host": "a" * 300}}) + + def test_add_host_with_multiple_hosts(self): + self.assertRaises(self.bad_request, eval(self.add_host), + self.req, "1", body={"add_host": {"host": ["host1", "host2"]}}) + + def test_add_host_raises_key_error(self): + def stub_add_host_to_aggregate(context, aggregate, host): + raise KeyError + self.stubs.Set(self.controller.api, "add_host_to_aggregate", + stub_add_host_to_aggregate) + self.assertRaises(exc.HTTPInternalServerError, + eval(self.add_host), self.req, "1", + body={"add_host": {"host": "host1"}}) + + def test_add_host_with_invalid_request(self): + self.assertRaises(self.bad_request, eval(self.add_host), + self.req, "1", body={"add_host": "1"}) + + def test_add_host_with_non_string(self): + self.assertRaises(self.bad_request, eval(self.add_host), + self.req, "1", body={"add_host": {"host": 1}}) + + def test_remove_host(self): + def stub_remove_host_from_aggregate(context, aggregate, host): + self.assertEqual(context, self.context, "context") + self.assertEqual("1", aggregate, "aggregate") + self.assertEqual("host1", host, "host") + stub_remove_host_from_aggregate.called = True + return {} + self.stubs.Set(self.controller.api, + "remove_host_from_aggregate", + stub_remove_host_from_aggregate) + eval(self.remove_host)(self.req, "1", + body={"remove_host": {"host": "host1"}}) + + self.assertTrue(stub_remove_host_from_aggregate.called) + + def test_remove_host_no_admin(self): + self.assertRaises(exception.PolicyNotAuthorized, + eval(self.remove_host), + self.user_req, "1", + body={"remove_host": {"host": "host1"}}) + + def test_remove_host_with_bad_aggregate(self): + def stub_remove_host_from_aggregate(context, aggregate, host): + raise exception.AggregateNotFound(aggregate_id=aggregate) + self.stubs.Set(self.controller.api, + "remove_host_from_aggregate", + stub_remove_host_from_aggregate) + + self.assertRaises(exc.HTTPNotFound, eval(self.remove_host), + self.req, "bogus_aggregate", + body={"remove_host": {"host": "host1"}}) + + def test_remove_host_with_host_not_in_aggregate(self): + def stub_remove_host_from_aggregate(context, aggregate, host): + raise exception.AggregateHostNotFound(aggregate_id=aggregate, + host=host) + self.stubs.Set(self.controller.api, + "remove_host_from_aggregate", + stub_remove_host_from_aggregate) + + self.assertRaises(exc.HTTPNotFound, eval(self.remove_host), + self.req, "1", + body={"remove_host": {"host": "host1"}}) + + def test_remove_host_with_bad_host(self): + def stub_remove_host_from_aggregate(context, aggregate, host): + raise exception.ComputeHostNotFound(host=host) + self.stubs.Set(self.controller.api, + "remove_host_from_aggregate", + stub_remove_host_from_aggregate) + + self.assertRaises(exc.HTTPNotFound, eval(self.remove_host), + self.req, "1", body={"remove_host": {"host": "bogushost"}}) + + def test_remove_host_with_missing_host(self): + self.assertRaises(self.bad_request, eval(self.remove_host), + self.req, "1", body={"asdf": "asdf"}) + + def test_remove_host_with_multiple_hosts(self): + self.assertRaises(self.bad_request, eval(self.remove_host), + self.req, "1", body={"remove_host": {"host": + ["host1", "host2"]}}) + + def test_remove_host_with_extra_param(self): + self.assertRaises(self.bad_request, eval(self.remove_host), + self.req, "1", body={"remove_host": {"asdf": "asdf", + "host": "asdf"}}) + + def test_remove_host_with_invalid_request(self): + self.assertRaises(self.bad_request, + eval(self.remove_host), + self.req, "1", body={"remove_host": "1"}) + + def test_remove_host_with_missing_host_empty(self): + self.assertRaises(self.bad_request, + eval(self.remove_host), + self.req, "1", body={"remove_host": {}}) + + def test_set_metadata(self): + body = {"set_metadata": {"metadata": {"foo": "bar"}}} + + def stub_update_aggregate(context, aggregate, values): + self.assertEqual(context, self.context, "context") + self.assertEqual("1", aggregate, "aggregate") + self.assertThat(body["set_metadata"]['metadata'], + matchers.DictMatches(values)) + return AGGREGATE + self.stubs.Set(self.controller.api, + "update_aggregate_metadata", + stub_update_aggregate) + + result = eval(self.set_metadata)(self.req, "1", body=body) + + self.assertEqual(AGGREGATE, result["aggregate"]) + + def test_set_metadata_delete(self): + body = {"set_metadata": {"metadata": {"foo": None}}} + + with mock.patch.object(self.controller.api, + 'update_aggregate_metadata') as mocked: + mocked.return_value = AGGREGATE + result = eval(self.set_metadata)(self.req, "1", body=body) + + self.assertEqual(AGGREGATE, result["aggregate"]) + mocked.assert_called_once_with(self.context, "1", + body["set_metadata"]["metadata"]) + + def test_set_metadata_no_admin(self): + self.assertRaises(exception.PolicyNotAuthorized, + eval(self.set_metadata), + self.user_req, "1", + body={"set_metadata": {"metadata": + {"foo": "bar"}}}) + + def test_set_metadata_with_bad_aggregate(self): + body = {"set_metadata": {"metadata": {"foo": "bar"}}} + + def stub_update_aggregate(context, aggregate, metadata): + raise exception.AggregateNotFound(aggregate_id=aggregate) + self.stubs.Set(self.controller.api, + "update_aggregate_metadata", + stub_update_aggregate) + self.assertRaises(exc.HTTPNotFound, eval(self.set_metadata), + self.req, "bad_aggregate", body=body) + + def test_set_metadata_with_missing_metadata(self): + body = {"asdf": {"foo": "bar"}} + self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata), + self.req, "1", body=body) + + def test_set_metadata_with_extra_params(self): + body = {"metadata": {"foo": "bar"}, "asdf": {"foo": "bar"}} + self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata), + self.req, "1", body=body) + + def test_set_metadata_without_dict(self): + body = {"set_metadata": {'metadata': 1}} + self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata), + self.req, "1", body=body) + + def test_set_metadata_with_empty_key(self): + body = {"set_metadata": {"metadata": {"": "value"}}} + self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata), + self.req, "1", body=body) + + def test_set_metadata_with_key_too_long(self): + body = {"set_metadata": {"metadata": {"x" * 256: "value"}}} + self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata), + self.req, "1", body=body) + + def test_set_metadata_with_value_too_long(self): + body = {"set_metadata": {"metadata": {"key": "x" * 256}}} + self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata), + self.req, "1", body=body) + + def test_set_metadata_with_string(self): + body = {"set_metadata": {"metadata": "test"}} + self.assertRaises(exc.HTTPBadRequest, eval(self.set_metadata), + self.req, "1", body=body) + + def test_delete_aggregate(self): + def stub_delete_aggregate(context, aggregate): + self.assertEqual(context, self.context, "context") + self.assertEqual("1", aggregate, "aggregate") + stub_delete_aggregate.called = True + self.stubs.Set(self.controller.api, "delete_aggregate", + stub_delete_aggregate) + + self.controller.delete(self.req, "1") + self.assertTrue(stub_delete_aggregate.called) + + def test_delete_aggregate_no_admin(self): + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.delete, + self.user_req, "1") + + def test_delete_aggregate_with_bad_aggregate(self): + def stub_delete_aggregate(context, aggregate): + raise exception.AggregateNotFound(aggregate_id=aggregate) + self.stubs.Set(self.controller.api, "delete_aggregate", + stub_delete_aggregate) + + self.assertRaises(exc.HTTPNotFound, self.controller.delete, + self.req, "bogus_aggregate") + + def test_delete_aggregate_with_host(self): + with mock.patch.object(self.controller.api, "delete_aggregate", + side_effect=exception.InvalidAggregateAction( + action="delete", aggregate_id="agg1", + reason="not empty")): + self.assertRaises(exc.HTTPBadRequest, + self.controller.delete, + self.req, "agg1") + + +class AggregateTestCaseV2(AggregateTestCaseV21): + add_host = 'self.controller.action' + remove_host = 'self.controller.action' + set_metadata = 'self.controller.action' + bad_request = exc.HTTPBadRequest + + def _set_up(self): + self.controller = aggregates_v2.AggregateController() + self.req = FakeRequest() + self.user_req = fakes.HTTPRequest.blank('/v2/os-aggregates') + self.context = self.req.environ['nova.context'] + + def test_add_host_raises_key_error(self): + def stub_add_host_to_aggregate(context, aggregate, host): + raise KeyError + self.stubs.Set(self.controller.api, "add_host_to_aggregate", + stub_add_host_to_aggregate) + # NOTE(mtreinish) The check for a KeyError here is to ensure that + # if add_host_to_aggregate() raises a KeyError it propagates. At + # one point the api code would mask the error as a HTTPBadRequest. + # This test is to ensure that this doesn't occur again. + self.assertRaises(KeyError, eval(self.add_host), self.req, "1", + body={"add_host": {"host": "host1"}}) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_attach_interfaces.py b/nova/tests/unit/api/openstack/compute/contrib/test_attach_interfaces.py new file mode 100644 index 0000000000..3b7e0b058a --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_attach_interfaces.py @@ -0,0 +1,455 @@ +# Copyright 2012 SINA Inc. +# 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 mock +from oslo.config import cfg +from oslo.serialization import jsonutils + +from nova.api.openstack.compute.contrib import attach_interfaces \ + as attach_interfaces_v2 +from nova.api.openstack.compute.plugins.v3 import attach_interfaces \ + as attach_interfaces_v3 +from nova.compute import api as compute_api +from nova import context +from nova import exception +from nova.network import api as network_api +from nova import objects +from nova import test +from nova.tests.unit import fake_network_cache_model + +import webob +from webob import exc + + +CONF = cfg.CONF + +FAKE_UUID1 = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +FAKE_UUID2 = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb' + +FAKE_PORT_ID1 = '11111111-1111-1111-1111-111111111111' +FAKE_PORT_ID2 = '22222222-2222-2222-2222-222222222222' +FAKE_PORT_ID3 = '33333333-3333-3333-3333-333333333333' + +FAKE_NET_ID1 = '44444444-4444-4444-4444-444444444444' +FAKE_NET_ID2 = '55555555-5555-5555-5555-555555555555' +FAKE_NET_ID3 = '66666666-6666-6666-6666-666666666666' +FAKE_BAD_NET_ID = '00000000-0000-0000-0000-000000000000' + +port_data1 = { + "id": FAKE_PORT_ID1, + "network_id": FAKE_NET_ID1, + "admin_state_up": True, + "status": "ACTIVE", + "mac_address": "aa:aa:aa:aa:aa:aa", + "fixed_ips": ["10.0.1.2"], + "device_id": FAKE_UUID1, +} + +port_data2 = { + "id": FAKE_PORT_ID2, + "network_id": FAKE_NET_ID2, + "admin_state_up": True, + "status": "ACTIVE", + "mac_address": "bb:bb:bb:bb:bb:bb", + "fixed_ips": ["10.0.2.2"], + "device_id": FAKE_UUID1, +} + +port_data3 = { + "id": FAKE_PORT_ID3, + "network_id": FAKE_NET_ID3, + "admin_state_up": True, + "status": "ACTIVE", + "mac_address": "bb:bb:bb:bb:bb:bb", + "fixed_ips": ["10.0.2.2"], + "device_id": '', +} + +fake_networks = [FAKE_NET_ID1, FAKE_NET_ID2] +ports = [port_data1, port_data2, port_data3] + + +def fake_list_ports(self, *args, **kwargs): + result = [] + for port in ports: + if port['device_id'] == kwargs['device_id']: + result.append(port) + return {'ports': result} + + +def fake_show_port(self, context, port_id, **kwargs): + for port in ports: + if port['id'] == port_id: + return {'port': port} + else: + raise exception.PortNotFound(port_id=port_id) + + +def fake_attach_interface(self, context, instance, network_id, port_id, + requested_ip='192.168.1.3'): + if not network_id: + # if no network_id is given when add a port to an instance, use the + # first default network. + network_id = fake_networks[0] + if network_id == FAKE_BAD_NET_ID: + raise exception.NetworkNotFound(network_id=network_id) + if not port_id: + port_id = ports[fake_networks.index(network_id)]['id'] + vif = fake_network_cache_model.new_vif() + vif['id'] = port_id + vif['network']['id'] = network_id + vif['network']['subnets'][0]['ips'][0]['address'] = requested_ip + return vif + + +def fake_detach_interface(self, context, instance, port_id): + for port in ports: + if port['id'] == port_id: + return + raise exception.PortNotFound(port_id=port_id) + + +def fake_get_instance(self, *args, **kwargs): + return objects.Instance(uuid=FAKE_UUID1) + + +class InterfaceAttachTestsV21(test.NoDBTestCase): + url = '/v3/os-interfaces' + controller_cls = attach_interfaces_v3.InterfaceAttachmentController + validate_exc = exception.ValidationError + + def setUp(self): + super(InterfaceAttachTestsV21, self).setUp() + self.flags(auth_strategy=None, group='neutron') + self.flags(url='http://anyhost/', group='neutron') + self.flags(url_timeout=30, group='neutron') + self.stubs.Set(network_api.API, 'show_port', fake_show_port) + self.stubs.Set(network_api.API, 'list_ports', fake_list_ports) + self.stubs.Set(compute_api.API, 'get', fake_get_instance) + self.context = context.get_admin_context() + self.expected_show = {'interfaceAttachment': + {'net_id': FAKE_NET_ID1, + 'port_id': FAKE_PORT_ID1, + 'mac_addr': port_data1['mac_address'], + 'port_state': port_data1['status'], + 'fixed_ips': port_data1['fixed_ips'], + }} + self.attachments = self.controller_cls() + + @mock.patch.object(compute_api.API, 'get', + side_effect=exception.InstanceNotFound(instance_id='')) + def _test_instance_not_found(self, url, func, args, mock_get, kwargs=None, + method='GET'): + req = webob.Request.blank(url) + req.method = method + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + if not kwargs: + kwargs = {} + self.assertRaises(exc.HTTPNotFound, func, req, *args, **kwargs) + + def test_show_instance_not_found(self): + self._test_instance_not_found(self.url + 'fake', + self.attachments.show, ('fake', 'fake')) + + def test_index_instance_not_found(self): + self._test_instance_not_found(self.url, + self.attachments.index, ('fake', )) + + def test_detach_interface_instance_not_found(self): + self._test_instance_not_found(self.url + '/fake', + self.attachments.delete, + ('fake', 'fake'), method='DELETE') + + def test_attach_interface_instance_not_found(self): + self._test_instance_not_found( + '/v2/fake/os-interfaces', self.attachments.create, ('fake', ), + kwargs={'body': {'interfaceAttachment': {}}}, method='POST') + + def test_show(self): + req = webob.Request.blank(self.url + '/show') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + result = self.attachments.show(req, FAKE_UUID1, FAKE_PORT_ID1) + self.assertEqual(self.expected_show, result) + + def test_show_invalid(self): + req = webob.Request.blank(self.url + '/show') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + self.assertRaises(exc.HTTPNotFound, + self.attachments.show, req, FAKE_UUID2, + FAKE_PORT_ID1) + + @mock.patch.object(network_api.API, 'show_port', + side_effect=exception.Forbidden) + def test_show_forbidden(self, show_port_mock): + req = webob.Request.blank(self.url + '/show') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + self.assertRaises(exc.HTTPForbidden, + self.attachments.show, req, FAKE_UUID1, + FAKE_PORT_ID1) + + def test_delete(self): + self.stubs.Set(compute_api.API, 'detach_interface', + fake_detach_interface) + req = webob.Request.blank(self.url + '/delete') + req.method = 'DELETE' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + result = self.attachments.delete(req, FAKE_UUID1, FAKE_PORT_ID1) + # NOTE: on v2.1, http status code is set as wsgi_code of API + # method instead of status_int in a response object. + if isinstance(self.attachments, + attach_interfaces_v3.InterfaceAttachmentController): + status_int = self.attachments.delete.wsgi_code + else: + status_int = result.status_int + self.assertEqual(202, status_int) + + def test_detach_interface_instance_locked(self): + def fake_detach_interface_from_locked_server(self, context, + instance, port_id): + raise exception.InstanceIsLocked(instance_uuid=FAKE_UUID1) + + self.stubs.Set(compute_api.API, + 'detach_interface', + fake_detach_interface_from_locked_server) + req = webob.Request.blank(self.url + '/delete') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + self.assertRaises(exc.HTTPConflict, + self.attachments.delete, + req, + FAKE_UUID1, + FAKE_PORT_ID1) + + def test_delete_interface_not_found(self): + self.stubs.Set(compute_api.API, 'detach_interface', + fake_detach_interface) + req = webob.Request.blank(self.url + '/delete') + req.method = 'DELETE' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + self.assertRaises(exc.HTTPNotFound, + self.attachments.delete, + req, + FAKE_UUID1, + 'invaid-port-id') + + def test_attach_interface_instance_locked(self): + def fake_attach_interface_to_locked_server(self, context, + instance, network_id, port_id, requested_ip): + raise exception.InstanceIsLocked(instance_uuid=FAKE_UUID1) + + self.stubs.Set(compute_api.API, + 'attach_interface', + fake_attach_interface_to_locked_server) + req = webob.Request.blank(self.url + '/attach') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + self.assertRaises(exc.HTTPConflict, + self.attachments.create, req, FAKE_UUID1, + body=jsonutils.loads(req.body)) + + def test_attach_interface_without_network_id(self): + self.stubs.Set(compute_api.API, 'attach_interface', + fake_attach_interface) + req = webob.Request.blank(self.url + '/attach') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + result = self.attachments.create(req, FAKE_UUID1, + body=jsonutils.loads(req.body)) + self.assertEqual(result['interfaceAttachment']['net_id'], + FAKE_NET_ID1) + + def test_attach_interface_with_network_id(self): + self.stubs.Set(compute_api.API, 'attach_interface', + fake_attach_interface) + req = webob.Request.blank(self.url + '/attach') + req.method = 'POST' + req.body = jsonutils.dumps({'interfaceAttachment': + {'net_id': FAKE_NET_ID2}}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + result = self.attachments.create(req, FAKE_UUID1, + body=jsonutils.loads(req.body)) + self.assertEqual(result['interfaceAttachment']['net_id'], + FAKE_NET_ID2) + + def _attach_interface_bad_request_case(self, body): + self.stubs.Set(compute_api.API, 'attach_interface', + fake_attach_interface) + req = webob.Request.blank(self.url + '/attach') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + self.assertRaises(exc.HTTPBadRequest, + self.attachments.create, req, FAKE_UUID1, + body=jsonutils.loads(req.body)) + + def test_attach_interface_with_port_and_network_id(self): + body = { + 'interfaceAttachment': { + 'port_id': FAKE_PORT_ID1, + 'net_id': FAKE_NET_ID2 + } + } + self._attach_interface_bad_request_case(body) + + def test_attach_interface_with_invalid_data(self): + body = { + 'interfaceAttachment': { + 'net_id': FAKE_BAD_NET_ID + } + } + self._attach_interface_bad_request_case(body) + + def test_attach_interface_with_invalid_state(self): + def fake_attach_interface_invalid_state(*args, **kwargs): + raise exception.InstanceInvalidState( + instance_uuid='', attr='', state='', + method='attach_interface') + + self.stubs.Set(compute_api.API, 'attach_interface', + fake_attach_interface_invalid_state) + req = webob.Request.blank(self.url + '/attach') + req.method = 'POST' + req.body = jsonutils.dumps({'interfaceAttachment': + {'net_id': FAKE_NET_ID1}}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + self.assertRaises(exc.HTTPConflict, + self.attachments.create, req, FAKE_UUID1, + body=jsonutils.loads(req.body)) + + def test_detach_interface_with_invalid_state(self): + def fake_detach_interface_invalid_state(*args, **kwargs): + raise exception.InstanceInvalidState( + instance_uuid='', attr='', state='', + method='detach_interface') + + self.stubs.Set(compute_api.API, 'detach_interface', + fake_detach_interface_invalid_state) + req = webob.Request.blank(self.url + '/attach') + req.method = 'DELETE' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + self.assertRaises(exc.HTTPConflict, + self.attachments.delete, + req, + FAKE_UUID1, + FAKE_NET_ID1) + + def test_attach_interface_invalid_fixed_ip(self): + req = webob.Request.blank(self.url + '/attach') + req.method = 'POST' + body = { + 'interfaceAttachment': { + 'net_id': FAKE_NET_ID1, + 'fixed_ips': [{'ip_address': 'invalid_ip'}] + } + } + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + self.assertRaises(self.validate_exc, + self.attachments.create, req, FAKE_UUID1, + body=jsonutils.loads(req.body)) + + @mock.patch.object(compute_api.API, 'get') + @mock.patch.object(compute_api.API, 'attach_interface') + def test_attach_interface_fixed_ip_already_in_use(self, + attach_mock, + get_mock): + fake_instance = objects.Instance(uuid=FAKE_UUID1) + get_mock.return_value = fake_instance + attach_mock.side_effect = exception.FixedIpAlreadyInUse( + address='10.0.2.2', instance_uuid=FAKE_UUID1) + req = webob.Request.blank(self.url + '/attach') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + self.assertRaises(exc.HTTPBadRequest, + self.attachments.create, req, FAKE_UUID1, + body=jsonutils.loads(req.body)) + attach_mock.assert_called_once_with(self.context, fake_instance, None, + None, None) + get_mock.assert_called_once_with(self.context, FAKE_UUID1, + want_objects=True, + expected_attrs=None) + + def _test_attach_interface_with_invalid_parameter(self, param): + self.stubs.Set(compute_api.API, 'attach_interface', + fake_attach_interface) + req = webob.Request.blank(self.url + '/attach') + req.method = 'POST' + req.body = jsonutils.dumps({'interface_attachment': param}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + self.assertRaises(exception.ValidationError, + self.attachments.create, req, FAKE_UUID1, + body=jsonutils.loads(req.body)) + + def test_attach_interface_instance_with_non_uuid_net_id(self): + param = {'net_id': 'non_uuid'} + self._test_attach_interface_with_invalid_parameter(param) + + def test_attach_interface_instance_with_non_uuid_port_id(self): + param = {'port_id': 'non_uuid'} + self._test_attach_interface_with_invalid_parameter(param) + + def test_attach_interface_instance_with_non_array_fixed_ips(self): + param = {'fixed_ips': 'non_array'} + self._test_attach_interface_with_invalid_parameter(param) + + +class InterfaceAttachTestsV2(InterfaceAttachTestsV21): + url = '/v2/fake/os-interfaces' + controller_cls = attach_interfaces_v2.InterfaceAttachmentController + validate_exc = exc.HTTPBadRequest + + def test_attach_interface_instance_with_non_uuid_net_id(self): + pass + + def test_attach_interface_instance_with_non_uuid_port_id(self): + pass + + def test_attach_interface_instance_with_non_array_fixed_ips(self): + pass diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_availability_zone.py b/nova/tests/unit/api/openstack/compute/contrib/test_availability_zone.py new file mode 100644 index 0000000000..31b20d6861 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_availability_zone.py @@ -0,0 +1,512 @@ +# Copyright 2012 IBM Corp. +# +# 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 datetime + +from lxml import etree +from oslo.config import cfg +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import availability_zone as az_v2 +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import availability_zone as az_v21 +from nova.api.openstack.compute.plugins.v3 import servers as servers_v21 +from nova.api.openstack.compute import servers as servers_v2 +from nova.api.openstack import extensions +from nova import availability_zones +from nova.compute import api as compute_api +from nova.compute import flavors +from nova import context +from nova import db +from nova import exception +from nova import servicegroup +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance +from nova.tests.unit.image import fake +from nova.tests.unit import matchers +from nova.tests.unit.objects import test_service + +FAKE_UUID = fakes.FAKE_UUID + + +def fake_service_get_all(context, disabled=None): + def __fake_service(binary, availability_zone, + created_at, updated_at, host, disabled): + return dict(test_service.fake_service, + binary=binary, + availability_zone=availability_zone, + available_zones=availability_zone, + created_at=created_at, + updated_at=updated_at, + host=host, + disabled=disabled) + + if disabled: + return [__fake_service("nova-compute", "zone-2", + datetime.datetime(2012, 11, 14, 9, 53, 25, 0), + datetime.datetime(2012, 12, 26, 14, 45, 25, 0), + "fake_host-1", True), + __fake_service("nova-scheduler", "internal", + datetime.datetime(2012, 11, 14, 9, 57, 3, 0), + datetime.datetime(2012, 12, 26, 14, 45, 25, 0), + "fake_host-1", True), + __fake_service("nova-network", "internal", + datetime.datetime(2012, 11, 16, 7, 25, 46, 0), + datetime.datetime(2012, 12, 26, 14, 45, 24, 0), + "fake_host-2", True)] + else: + return [__fake_service("nova-compute", "zone-1", + datetime.datetime(2012, 11, 14, 9, 53, 25, 0), + datetime.datetime(2012, 12, 26, 14, 45, 25, 0), + "fake_host-1", False), + __fake_service("nova-sched", "internal", + datetime.datetime(2012, 11, 14, 9, 57, 3, 0), + datetime.datetime(2012, 12, 26, 14, 45, 25, 0), + "fake_host-1", False), + __fake_service("nova-network", "internal", + datetime.datetime(2012, 11, 16, 7, 25, 46, 0), + datetime.datetime(2012, 12, 26, 14, 45, 24, 0), + "fake_host-2", False)] + + +def fake_service_is_up(self, service): + return service['binary'] != u"nova-network" + + +def fake_set_availability_zones(context, services): + return services + + +def fake_get_availability_zones(context): + return ['nova'], [] + + +CONF = cfg.CONF + + +class AvailabilityZoneApiTestV21(test.NoDBTestCase): + availability_zone = az_v21 + url = '/v2/fake/os-availability-zone' + + def setUp(self): + super(AvailabilityZoneApiTestV21, self).setUp() + availability_zones.reset_cache() + self.stubs.Set(db, 'service_get_all', fake_service_get_all) + self.stubs.Set(availability_zones, 'set_availability_zones', + fake_set_availability_zones) + self.stubs.Set(servicegroup.API, 'service_is_up', fake_service_is_up) + + def _get_wsgi_instance(self): + return fakes.wsgi_app_v21(init_only=('os-availability-zone', + 'servers')) + + def test_filtered_availability_zones(self): + az = self.availability_zone.AvailabilityZoneController() + zones = ['zone1', 'internal'] + expected = [{'zoneName': 'zone1', + 'zoneState': {'available': True}, + "hosts": None}] + result = az._get_filtered_availability_zones(zones, True) + self.assertEqual(result, expected) + + expected = [{'zoneName': 'zone1', + 'zoneState': {'available': False}, + "hosts": None}] + result = az._get_filtered_availability_zones(zones, False) + self.assertEqual(result, expected) + + def test_availability_zone_index(self): + req = webob.Request.blank(self.url) + resp = req.get_response(self._get_wsgi_instance()) + self.assertEqual(resp.status_int, 200) + resp_dict = jsonutils.loads(resp.body) + + self.assertIn('availabilityZoneInfo', resp_dict) + zones = resp_dict['availabilityZoneInfo'] + self.assertEqual(len(zones), 2) + self.assertEqual(zones[0]['zoneName'], u'zone-1') + self.assertTrue(zones[0]['zoneState']['available']) + self.assertIsNone(zones[0]['hosts']) + self.assertEqual(zones[1]['zoneName'], u'zone-2') + self.assertFalse(zones[1]['zoneState']['available']) + self.assertIsNone(zones[1]['hosts']) + + def test_availability_zone_detail(self): + def _formatZone(zone_dict): + result = [] + + # Zone tree view item + result.append({'zoneName': zone_dict['zoneName'], + 'zoneState': u'available' + if zone_dict['zoneState']['available'] else + u'not available'}) + + if zone_dict['hosts'] is not None: + for (host, services) in zone_dict['hosts'].items(): + # Host tree view item + result.append({'zoneName': u'|- %s' % host, + 'zoneState': u''}) + for (svc, state) in services.items(): + # Service tree view item + result.append({'zoneName': u'| |- %s' % svc, + 'zoneState': u'%s %s %s' % ( + 'enabled' if state['active'] else + 'disabled', + ':-)' if state['available'] else + 'XXX', + jsonutils.to_primitive( + state['updated_at']))}) + return result + + def _assertZone(zone, name, status): + self.assertEqual(zone['zoneName'], name) + self.assertEqual(zone['zoneState'], status) + + availabilityZone = self.availability_zone.AvailabilityZoneController() + + req_url = self.url + '/detail' + req = webob.Request.blank(req_url) + req.method = 'GET' + req.environ['nova.context'] = context.get_admin_context() + resp_dict = availabilityZone.detail(req) + + self.assertIn('availabilityZoneInfo', resp_dict) + zones = resp_dict['availabilityZoneInfo'] + self.assertEqual(len(zones), 3) + + ''' availabilityZoneInfo field content in response body: + [{'zoneName': 'zone-1', + 'zoneState': {'available': True}, + 'hosts': {'fake_host-1': { + 'nova-compute': {'active': True, 'available': True, + 'updated_at': datetime(2012, 12, 26, 14, 45, 25)}}}}, + {'zoneName': 'internal', + 'zoneState': {'available': True}, + 'hosts': {'fake_host-1': { + 'nova-sched': {'active': True, 'available': True, + 'updated_at': datetime(2012, 12, 26, 14, 45, 25)}}, + 'fake_host-2': { + 'nova-network': {'active': True, 'available': False, + 'updated_at': datetime(2012, 12, 26, 14, 45, 24)}}}}, + {'zoneName': 'zone-2', + 'zoneState': {'available': False}, + 'hosts': None}] + ''' + + l0 = [u'zone-1', u'available'] + l1 = [u'|- fake_host-1', u''] + l2 = [u'| |- nova-compute', u'enabled :-) 2012-12-26T14:45:25.000000'] + l3 = [u'internal', u'available'] + l4 = [u'|- fake_host-1', u''] + l5 = [u'| |- nova-sched', u'enabled :-) 2012-12-26T14:45:25.000000'] + l6 = [u'|- fake_host-2', u''] + l7 = [u'| |- nova-network', u'enabled XXX 2012-12-26T14:45:24.000000'] + l8 = [u'zone-2', u'not available'] + + z0 = _formatZone(zones[0]) + z1 = _formatZone(zones[1]) + z2 = _formatZone(zones[2]) + + self.assertEqual(len(z0), 3) + self.assertEqual(len(z1), 5) + self.assertEqual(len(z2), 1) + + _assertZone(z0[0], l0[0], l0[1]) + _assertZone(z0[1], l1[0], l1[1]) + _assertZone(z0[2], l2[0], l2[1]) + _assertZone(z1[0], l3[0], l3[1]) + _assertZone(z1[1], l4[0], l4[1]) + _assertZone(z1[2], l5[0], l5[1]) + _assertZone(z1[3], l6[0], l6[1]) + _assertZone(z1[4], l7[0], l7[1]) + _assertZone(z2[0], l8[0], l8[1]) + + def test_availability_zone_detail_no_services(self): + expected_response = {'availabilityZoneInfo': + [{'zoneState': {'available': True}, + 'hosts': {}, + 'zoneName': 'nova'}]} + self.stubs.Set(availability_zones, 'get_availability_zones', + fake_get_availability_zones) + availabilityZone = self.availability_zone.AvailabilityZoneController() + + req_url = self.url + '/detail' + req = webob.Request.blank(req_url) + req.method = 'GET' + req.environ['nova.context'] = context.get_admin_context() + resp_dict = availabilityZone.detail(req) + + self.assertThat(resp_dict, + matchers.DictMatches(expected_response)) + + +class AvailabilityZoneApiTestV2(AvailabilityZoneApiTestV21): + availability_zone = az_v2 + + def _get_wsgi_instance(self): + return fakes.wsgi_app() + + +class ServersControllerCreateTestV21(test.TestCase): + base_url = '/v2/fake/' + + def setUp(self): + """Shared implementation for tests below that create instance.""" + super(ServersControllerCreateTestV21, self).setUp() + + self.instance_cache_num = 0 + + self._set_up_controller() + + def instance_create(context, inst): + inst_type = flavors.get_flavor_by_flavor_id(3) + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + def_image_ref = 'http://localhost/images/%s' % image_uuid + self.instance_cache_num += 1 + instance = fake_instance.fake_db_instance(**{ + 'id': self.instance_cache_num, + 'display_name': inst['display_name'] or 'test', + 'uuid': FAKE_UUID, + 'instance_type': inst_type, + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': 'fead::1234', + 'image_ref': inst.get('image_ref', def_image_ref), + 'user_id': 'fake', + 'project_id': 'fake', + 'availability_zone': 'nova', + 'reservation_id': inst['reservation_id'], + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "progress": 0, + "fixed_ips": [], + "task_state": "", + "vm_state": "", + "root_device_name": inst.get('root_device_name', 'vda'), + }) + + return instance + + fake.stub_out_image_service(self.stubs) + self.stubs.Set(db, 'instance_create', instance_create) + + def _set_up_controller(self): + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers_v21.ServersController( + extension_info=ext_info) + CONF.set_override('extensions_blacklist', + 'os-availability-zone', + 'osapi_v3') + self.no_availability_zone_controller = servers_v21.ServersController( + extension_info=ext_info) + + def _verify_no_availability_zone(self, **kwargs): + self.assertNotIn('availability_zone', kwargs) + + def _test_create_extra(self, params, controller): + image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + server = dict(name='server_test', imageRef=image_uuid, flavorRef=2) + server.update(params) + body = dict(server=server) + req = fakes.HTTPRequest.blank(self.base_url + 'servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + server = controller.create(req, body=body).obj['server'] + + def test_create_instance_with_availability_zone_disabled(self): + params = {'availability_zone': 'foo'} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self._verify_no_availability_zone(**kwargs) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params, self.no_availability_zone_controller) + + def _create_instance_with_availability_zone(self, zone_name): + def create(*args, **kwargs): + self.assertIn('availability_zone', kwargs) + self.assertEqual('nova', kwargs['availability_zone']) + return old_create(*args, **kwargs) + + old_create = compute_api.API.create + self.stubs.Set(compute_api.API, 'create', create) + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = ('http://localhost' + self.base_url + 'flavors/3') + body = { + 'server': { + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'availability_zone': zone_name, + }, + } + + req = fakes.HTTPRequest.blank(self.base_url + 'servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + admin_context = context.get_admin_context() + db.service_create(admin_context, {'host': 'host1_zones', + 'binary': "nova-compute", + 'topic': 'compute', + 'report_count': 0}) + agg = db.aggregate_create(admin_context, + {'name': 'agg1'}, {'availability_zone': 'nova'}) + db.aggregate_host_add(admin_context, agg['id'], 'host1_zones') + return req, body + + def test_create_instance_with_availability_zone(self): + zone_name = 'nova' + req, body = self._create_instance_with_availability_zone(zone_name) + res = self.controller.create(req, body=body).obj + server = res['server'] + self.assertEqual(fakes.FAKE_UUID, server['id']) + + def test_create_instance_with_invalid_availability_zone_too_long(self): + zone_name = 'a' * 256 + req, body = self._create_instance_with_availability_zone(zone_name) + self.assertRaises(exception.ValidationError, + self.controller.create, req, body=body) + + def test_create_instance_with_invalid_availability_zone_too_short(self): + zone_name = '' + req, body = self._create_instance_with_availability_zone(zone_name) + self.assertRaises(exception.ValidationError, + self.controller.create, req, body=body) + + def test_create_instance_with_invalid_availability_zone_not_str(self): + zone_name = 111 + req, body = self._create_instance_with_availability_zone(zone_name) + self.assertRaises(exception.ValidationError, + self.controller.create, req, body=body) + + def test_create_instance_without_availability_zone(self): + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = ('http://localhost' + self.base_url + 'flavors/3') + body = { + 'server': { + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + }, + } + + req = fakes.HTTPRequest.blank(self.base_url + 'servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body=body).obj + server = res['server'] + self.assertEqual(fakes.FAKE_UUID, server['id']) + + +class ServersControllerCreateTestV2(ServersControllerCreateTestV21): + + def _set_up_controller(self): + ext_mgr = extensions.ExtensionManager() + ext_mgr.extensions = {'os-availability-zone': 'fake'} + self.controller = servers_v2.Controller(ext_mgr) + ext_mgr_no_az = extensions.ExtensionManager() + ext_mgr_no_az.extensions = {} + self.no_availability_zone_controller = servers_v2.Controller( + ext_mgr_no_az) + + def _verify_no_availability_zone(self, **kwargs): + self.assertIsNone(kwargs['availability_zone']) + + def test_create_instance_with_invalid_availability_zone_too_long(self): + # NOTE: v2.0 API does not check this bad request case. + # So we skip this test for v2.0 API. + pass + + def test_create_instance_with_invalid_availability_zone_too_short(self): + # NOTE: v2.0 API does not check this bad request case. + # So we skip this test for v2.0 API. + pass + + def test_create_instance_with_invalid_availability_zone_not_str(self): + # NOTE: v2.0 API does not check this bad request case. + # So we skip this test for v2.0 API. + pass + + +class AvailabilityZoneSerializerTest(test.NoDBTestCase): + def test_availability_zone_index_detail_serializer(self): + def _verify_zone(zone_dict, tree): + self.assertEqual(tree.tag, 'availabilityZone') + self.assertEqual(zone_dict['zoneName'], tree.get('name')) + self.assertEqual(str(zone_dict['zoneState']['available']), + tree[0].get('available')) + + for _idx, host_child in enumerate(tree[1]): + self.assertIn(host_child.get('name'), zone_dict['hosts']) + svcs = zone_dict['hosts'][host_child.get('name')] + for _idx, svc_child in enumerate(host_child[0]): + self.assertIn(svc_child.get('name'), svcs) + svc = svcs[svc_child.get('name')] + self.assertEqual(len(svc_child), 1) + + self.assertEqual(str(svc['available']), + svc_child[0].get('available')) + self.assertEqual(str(svc['active']), + svc_child[0].get('active')) + self.assertEqual(str(svc['updated_at']), + svc_child[0].get('updated_at')) + + serializer = az_v2.AvailabilityZonesTemplate() + raw_availability_zones = \ + [{'zoneName': 'zone-1', + 'zoneState': {'available': True}, + 'hosts': {'fake_host-1': { + 'nova-compute': {'active': True, 'available': True, + 'updated_at': + datetime.datetime( + 2012, 12, 26, 14, 45, 25)}}}}, + {'zoneName': 'internal', + 'zoneState': {'available': True}, + 'hosts': {'fake_host-1': { + 'nova-sched': {'active': True, 'available': True, + 'updated_at': + datetime.datetime( + 2012, 12, 26, 14, 45, 25)}}, + 'fake_host-2': { + 'nova-network': {'active': True, + 'available': False, + 'updated_at': + datetime.datetime( + 2012, 12, 26, 14, 45, 24)}}}}, + {'zoneName': 'zone-2', + 'zoneState': {'available': False}, + 'hosts': None}] + + text = serializer.serialize( + dict(availabilityZoneInfo=raw_availability_zones)) + tree = etree.fromstring(text) + + self.assertEqual('availabilityZones', tree.tag) + self.assertEqual(len(raw_availability_zones), len(tree)) + for idx, child in enumerate(tree): + _verify_zone(raw_availability_zones[idx], child) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_baremetal_nodes.py b/nova/tests/unit/api/openstack/compute/contrib/test_baremetal_nodes.py new file mode 100644 index 0000000000..451c92a40b --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_baremetal_nodes.py @@ -0,0 +1,159 @@ +# Copyright (c) 2013 NTT DOCOMO, INC. +# 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 mock +from webob import exc + +from nova.api.openstack.compute.contrib import baremetal_nodes as b_nodes_v2 +from nova.api.openstack.compute.plugins.v3 import baremetal_nodes \ + as b_nodes_v21 +from nova.api.openstack import extensions +from nova import context +from nova import test +from nova.tests.unit.virt.ironic import utils as ironic_utils + + +class FakeRequest(object): + + def __init__(self, context): + self.environ = {"nova.context": context} + + +def fake_node(**updates): + node = { + 'id': 1, + 'service_host': "host", + 'cpus': 8, + 'memory_mb': 8192, + 'local_gb': 128, + 'pm_address': "10.1.2.3", + 'pm_user': "pm_user", + 'pm_password': "pm_pass", + 'terminal_port': 8000, + 'interfaces': [], + 'instance_uuid': 'fake-instance-uuid', + } + if updates: + node.update(updates) + return node + + +def fake_node_ext_status(**updates): + node = fake_node(uuid='fake-uuid', + task_state='fake-task-state', + updated_at='fake-updated-at', + pxe_config_path='fake-pxe-config-path') + if updates: + node.update(updates) + return node + + +FAKE_IRONIC_CLIENT = ironic_utils.FakeClient() + + +@mock.patch.object(b_nodes_v21, '_get_ironic_client', + lambda *_: FAKE_IRONIC_CLIENT) +class BareMetalNodesTestV21(test.NoDBTestCase): + def setUp(self): + super(BareMetalNodesTestV21, self).setUp() + + self._setup() + self.context = context.get_admin_context() + self.request = FakeRequest(self.context) + + def _setup(self): + self.controller = b_nodes_v21.BareMetalNodeController() + + @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'list') + def test_index_ironic(self, mock_list): + properties = {'cpus': 2, 'memory_mb': 1024, 'local_gb': 20} + node = ironic_utils.get_test_node(properties=properties) + mock_list.return_value = [node] + + res_dict = self.controller.index(self.request) + expected_output = {'nodes': + [{'memory_mb': properties['memory_mb'], + 'host': 'IRONIC MANAGED', + 'disk_gb': properties['local_gb'], + 'interfaces': [], + 'task_state': None, + 'id': node.uuid, + 'cpus': properties['cpus']}]} + self.assertEqual(expected_output, res_dict) + mock_list.assert_called_once_with(detail=True) + + @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'list_ports') + @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'get') + def test_show_ironic(self, mock_get, mock_list_ports): + properties = {'cpus': 1, 'memory_mb': 512, 'local_gb': 10} + node = ironic_utils.get_test_node(properties=properties) + port = ironic_utils.get_test_port() + mock_get.return_value = node + mock_list_ports.return_value = [port] + + res_dict = self.controller.show(self.request, node.uuid) + expected_output = {'node': + {'memory_mb': properties['memory_mb'], + 'instance_uuid': None, + 'host': 'IRONIC MANAGED', + 'disk_gb': properties['local_gb'], + 'interfaces': [{'address': port.address}], + 'task_state': None, + 'id': node.uuid, + 'cpus': properties['cpus']}} + self.assertEqual(expected_output, res_dict) + mock_get.assert_called_once_with(node.uuid) + mock_list_ports.assert_called_once_with(node.uuid) + + @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'list_ports') + @mock.patch.object(FAKE_IRONIC_CLIENT.node, 'get') + def test_show_ironic_no_interfaces(self, mock_get, mock_list_ports): + properties = {'cpus': 1, 'memory_mb': 512, 'local_gb': 10} + node = ironic_utils.get_test_node(properties=properties) + mock_get.return_value = node + mock_list_ports.return_value = [] + + res_dict = self.controller.show(self.request, node.uuid) + self.assertEqual([], res_dict['node']['interfaces']) + mock_get.assert_called_once_with(node.uuid) + mock_list_ports.assert_called_once_with(node.uuid) + + def test_create_ironic_not_supported(self): + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, + self.request, {'node': object()}) + + def test_delete_ironic_not_supported(self): + self.assertRaises(exc.HTTPBadRequest, + self.controller.delete, + self.request, 'fake-id') + + def test_add_interface_ironic_not_supported(self): + self.assertRaises(exc.HTTPBadRequest, + self.controller._add_interface, + self.request, 'fake-id', 'fake-body') + + def test_remove_interface_ironic_not_supported(self): + self.assertRaises(exc.HTTPBadRequest, + self.controller._remove_interface, + self.request, 'fake-id', 'fake-body') + + +@mock.patch.object(b_nodes_v2, '_get_ironic_client', + lambda *_: FAKE_IRONIC_CLIENT) +class BareMetalNodesTestV2(BareMetalNodesTestV21): + def _setup(self): + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.controller = b_nodes_v2.BareMetalNodeController(self.ext_mgr) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping.py b/nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping.py new file mode 100644 index 0000000000..ab20ad85c3 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping.py @@ -0,0 +1,359 @@ +# Copyright 2013 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 mock +import mox +from oslo.config import cfg +from oslo.serialization import jsonutils +from webob import exc + +from nova.api.openstack.compute import extensions +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import block_device_mapping +from nova.api.openstack.compute.plugins.v3 import servers as servers_v3 +from nova.api.openstack.compute import servers as servers_v2 +from nova import block_device +from nova.compute import api as compute_api +from nova import exception +from nova import objects +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit.image import fake +from nova.tests.unit import matchers + +CONF = cfg.CONF + + +class BlockDeviceMappingTestV21(test.TestCase): + + def _setup_controller(self): + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers_v3.ServersController(extension_info=ext_info) + CONF.set_override('extensions_blacklist', 'os-block-device-mapping', + 'osapi_v3') + self.no_bdm_v2_controller = servers_v3.ServersController( + extension_info=ext_info) + CONF.set_override('extensions_blacklist', '', 'osapi_v3') + + def setUp(self): + super(BlockDeviceMappingTestV21, self).setUp() + self._setup_controller() + fake.stub_out_image_service(self.stubs) + + self.bdm = [{ + 'no_device': None, + 'source_type': 'volume', + 'destination_type': 'volume', + 'uuid': 'fake', + 'device_name': 'vda', + 'delete_on_termination': False, + }] + + def _get_servers_body(self, no_image=False): + body = { + 'server': { + 'name': 'server_test', + 'imageRef': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + 'flavorRef': 'http://localhost/123/flavors/3', + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + }, + } + if no_image: + del body['server']['imageRef'] + return body + + def _test_create(self, params, no_image=False, override_controller=None): + body = self._get_servers_body(no_image) + body['server'].update(params) + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.headers['content-type'] = 'application/json' + + req.body = jsonutils.dumps(body) + + if override_controller: + override_controller.create(req, body=body).obj['server'] + else: + self.controller.create(req, body=body).obj['server'] + + def test_create_instance_with_block_device_mapping_disabled(self): + bdm = [{'device_name': 'foo'}] + + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertNotIn('block_device_mapping', kwargs) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + + params = {block_device_mapping.ATTRIBUTE_NAME: bdm} + self._test_create(params, + override_controller=self.no_bdm_v2_controller) + + def test_create_instance_with_volumes_enabled_no_image(self): + """Test that the create will fail if there is no image + and no bdms supplied in the request + """ + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertNotIn('imageRef', kwargs) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + + self.assertRaises(exc.HTTPBadRequest, + self._test_create, {}, no_image=True) + + def test_create_instance_with_bdms_and_no_image(self): + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertThat( + block_device.BlockDeviceDict(self.bdm[0]), + matchers.DictMatches(kwargs['block_device_mapping'][0]) + ) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + + self.mox.StubOutWithMock(compute_api.API, '_validate_bdm') + self.mox.StubOutWithMock(compute_api.API, '_get_bdm_image_metadata') + + compute_api.API._validate_bdm( + mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(True) + compute_api.API._get_bdm_image_metadata( + mox.IgnoreArg(), mox.IgnoreArg(), False).AndReturn({}) + self.mox.ReplayAll() + + params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm} + self._test_create(params, no_image=True) + + def test_create_instance_with_device_name_not_string(self): + self.bdm[0]['device_name'] = 123 + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], self.bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + + params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm} + self.assertRaises(exc.HTTPBadRequest, + self._test_create, params, no_image=True) + + @mock.patch.object(compute_api.API, 'create') + def test_create_instance_with_bdm_param_not_list(self, mock_create): + self.params = {'block_device_mapping': '/dev/vdb'} + self.assertRaises(exc.HTTPBadRequest, + self._test_create, self.params) + + def test_create_instance_with_device_name_empty(self): + self.bdm[0]['device_name'] = '' + + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], self.bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + + params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm} + self.assertRaises(exc.HTTPBadRequest, + self._test_create, params, no_image=True) + + def test_create_instance_with_device_name_too_long(self): + self.bdm[0]['device_name'] = 'a' * 256 + + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], self.bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + + params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm} + self.assertRaises(exc.HTTPBadRequest, + self._test_create, params, no_image=True) + + def test_create_instance_with_space_in_device_name(self): + self.bdm[0]['device_name'] = 'v da' + + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertTrue(kwargs['legacy_bdm']) + self.assertEqual(kwargs['block_device_mapping'], self.bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + + params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm} + self.assertRaises(exc.HTTPBadRequest, + self._test_create, params, no_image=True) + + def test_create_instance_with_invalid_size(self): + self.bdm[0]['volume_size'] = 'hello world' + + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], self.bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + + params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm} + self.assertRaises(exc.HTTPBadRequest, + self._test_create, params, no_image=True) + + def test_create_instance_bdm(self): + bdm = [{ + 'source_type': 'volume', + 'device_name': 'fake_dev', + 'uuid': 'fake_vol' + }] + bdm_expected = [{ + 'source_type': 'volume', + 'device_name': 'fake_dev', + 'volume_id': 'fake_vol' + }] + + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertFalse(kwargs['legacy_bdm']) + for expected, received in zip(bdm_expected, + kwargs['block_device_mapping']): + self.assertThat(block_device.BlockDeviceDict(expected), + matchers.DictMatches(received)) + return old_create(*args, **kwargs) + + def _validate_bdm(*args, **kwargs): + pass + + self.stubs.Set(compute_api.API, 'create', create) + self.stubs.Set(compute_api.API, '_validate_bdm', _validate_bdm) + + params = {block_device_mapping.ATTRIBUTE_NAME: bdm} + self._test_create(params, no_image=True) + + def test_create_instance_bdm_missing_device_name(self): + del self.bdm[0]['device_name'] + + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertFalse(kwargs['legacy_bdm']) + self.assertNotIn(None, + kwargs['block_device_mapping'][0]['device_name']) + return old_create(*args, **kwargs) + + def _validate_bdm(*args, **kwargs): + pass + + self.stubs.Set(compute_api.API, 'create', create) + self.stubs.Set(compute_api.API, '_validate_bdm', _validate_bdm) + + params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm} + self._test_create(params, no_image=True) + + def test_create_instance_bdm_validation_error(self): + def _validate(*args, **kwargs): + raise exception.InvalidBDMFormat(details='Wrong BDM') + + self.stubs.Set(block_device.BlockDeviceDict, + '_validate', _validate) + + params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm} + self.assertRaises(exc.HTTPBadRequest, + self._test_create, params, no_image=True) + + @mock.patch('nova.compute.api.API._get_bdm_image_metadata') + def test_create_instance_non_bootable_volume_fails(self, fake_bdm_meta): + params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm} + fake_bdm_meta.side_effect = exception.InvalidBDMVolumeNotBootable(id=1) + self.assertRaises(exc.HTTPBadRequest, self._test_create, params, + no_image=True) + + def test_create_instance_bdm_api_validation_fails(self): + self.validation_fail_test_validate_called = False + self.validation_fail_instance_destroy_called = False + + bdm_exceptions = ((exception.InvalidBDMSnapshot, {'id': 'fake'}), + (exception.InvalidBDMVolume, {'id': 'fake'}), + (exception.InvalidBDMImage, {'id': 'fake'}), + (exception.InvalidBDMBootSequence, {}), + (exception.InvalidBDMLocalsLimit, {})) + + ex_iter = iter(bdm_exceptions) + + def _validate_bdm(*args, **kwargs): + self.validation_fail_test_validate_called = True + ex, kargs = ex_iter.next() + raise ex(**kargs) + + def _instance_destroy(*args, **kwargs): + self.validation_fail_instance_destroy_called = True + + self.stubs.Set(compute_api.API, '_validate_bdm', _validate_bdm) + self.stubs.Set(objects.Instance, 'destroy', _instance_destroy) + + for _unused in xrange(len(bdm_exceptions)): + params = {block_device_mapping.ATTRIBUTE_NAME: + [self.bdm[0].copy()]} + self.assertRaises(exc.HTTPBadRequest, + self._test_create, params) + self.assertTrue(self.validation_fail_test_validate_called) + self.assertTrue(self.validation_fail_instance_destroy_called) + self.validation_fail_test_validate_called = False + self.validation_fail_instance_destroy_called = False + + +class BlockDeviceMappingTestV2(BlockDeviceMappingTestV21): + + def _setup_controller(self): + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {'os-volumes': 'fake', + 'os-block-device-mapping-v2-boot': 'fake'} + self.controller = servers_v2.Controller(self.ext_mgr) + self.ext_mgr_bdm_v2 = extensions.ExtensionManager() + self.ext_mgr_bdm_v2.extensions = {'os-volumes': 'fake'} + self.no_bdm_v2_controller = servers_v2.Controller( + self.ext_mgr_bdm_v2) + + def test_create_instance_with_block_device_mapping_disabled(self): + bdm = [{'device_name': 'foo'}] + + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertIsNone(kwargs['block_device_mapping'], None) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + + params = {block_device_mapping.ATTRIBUTE_NAME: bdm} + self._test_create(params, + override_controller=self.no_bdm_v2_controller) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping_v1.py b/nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping_v1.py new file mode 100644 index 0000000000..2f73f00952 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_block_device_mapping_v1.py @@ -0,0 +1,421 @@ +# Copyright (c) 2014 IBM Corp. +# 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 mock +import mox +from oslo.config import cfg +from oslo.serialization import jsonutils +from webob import exc + +from nova.api.openstack.compute import extensions +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import block_device_mapping_v1 as \ + block_device_mapping +from nova.api.openstack.compute.plugins.v3 import servers as servers_v3 +from nova.api.openstack.compute import servers as servers_v2 +from nova.compute import api as compute_api +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit.image import fake + +CONF = cfg.CONF + + +class BlockDeviceMappingTestV21(test.TestCase): + + def _setup_controller(self): + ext_info = plugins.LoadedExtensionInfo() + CONF.set_override('extensions_blacklist', 'os-block-device-mapping', + 'osapi_v3') + self.controller = servers_v3.ServersController(extension_info=ext_info) + CONF.set_override('extensions_blacklist', + ['os-block-device-mapping-v1', + 'os-block-device-mapping'], + 'osapi_v3') + self.no_volumes_controller = servers_v3.ServersController( + extension_info=ext_info) + CONF.set_override('extensions_blacklist', '', 'osapi_v3') + + def setUp(self): + super(BlockDeviceMappingTestV21, self).setUp() + self._setup_controller() + fake.stub_out_image_service(self.stubs) + self.volume_id = fakes.FAKE_UUID + self.bdm = [{ + 'id': 1, + 'no_device': None, + 'virtual_name': None, + 'snapshot_id': None, + 'volume_id': self.volume_id, + 'status': 'active', + 'device_name': 'vda', + 'delete_on_termination': False, + 'volume_image_metadata': + {'test_key': 'test_value'} + }] + + def _get_servers_body(self, no_image=False): + body = { + 'server': { + 'name': 'server_test', + 'imageRef': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + 'flavorRef': 'http://localhost/123/flavors/3', + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + }, + } + if no_image: + del body['server']['imageRef'] + return body + + def _test_create(self, params, no_image=False, override_controller=None): + body = self._get_servers_body(no_image) + body['server'].update(params) + + req = fakes.HTTPRequest.blank('/v2/fake/servers') + req.method = 'POST' + req.headers['content-type'] = 'application/json' + + req.body = jsonutils.dumps(body) + + if override_controller: + override_controller.create(req, body=body).obj['server'] + else: + self.controller.create(req, body=body).obj['server'] + + def test_create_instance_with_volumes_enabled(self): + params = {'block_device_mapping': self.bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], self.bdm) + return old_create(*args, **kwargs) + + def _validate_bdm(*args, **kwargs): + pass + + self.stubs.Set(compute_api.API, 'create', create) + self.stubs.Set(compute_api.API, '_validate_bdm', _validate_bdm) + self._test_create(params) + + def test_create_instance_with_volumes_enabled_and_bdms_no_image(self): + """Test that the create works if there is no image supplied but + os-volumes extension is enabled and bdms are supplied + """ + self.mox.StubOutWithMock(compute_api.API, '_validate_bdm') + self.mox.StubOutWithMock(compute_api.API, '_get_bdm_image_metadata') + volume = self.bdm[0] + compute_api.API._validate_bdm(mox.IgnoreArg(), + mox.IgnoreArg(), mox.IgnoreArg(), + mox.IgnoreArg()).AndReturn(True) + compute_api.API._get_bdm_image_metadata(mox.IgnoreArg(), + self.bdm, + True).AndReturn(volume) + params = {'block_device_mapping': self.bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], self.bdm) + self.assertNotIn('imageRef', kwargs) + return old_create(*args, **kwargs) + + def _validate_bdm(*args, **kwargs): + pass + + self.stubs.Set(compute_api.API, 'create', create) + self.mox.ReplayAll() + self._test_create(params, no_image=True) + + def test_create_instance_with_volumes_disabled(self): + bdm = [{'device_name': 'foo'}] + params = {'block_device_mapping': bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertNotIn(block_device_mapping, kwargs) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create(params, + override_controller=self.no_volumes_controller) + + @mock.patch('nova.compute.api.API._get_bdm_image_metadata') + def test_create_instance_non_bootable_volume_fails(self, fake_bdm_meta): + bdm = [{ + 'id': 1, + 'bootable': False, + 'volume_id': self.volume_id, + 'status': 'active', + 'device_name': 'vda', + }] + params = {'block_device_mapping': bdm} + fake_bdm_meta.side_effect = exception.InvalidBDMVolumeNotBootable(id=1) + self.assertRaises(exc.HTTPBadRequest, + self._test_create, params, no_image=True) + + def test_create_instance_with_device_name_not_string(self): + old_create = compute_api.API.create + self.params = {'block_device_mapping': self.bdm} + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], self.bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(exc.HTTPBadRequest, + self._test_create, self.params) + + @mock.patch.object(compute_api.API, 'create') + def test_create_instance_with_bdm_param_not_list(self, mock_create): + self.params = {'block_device_mapping': '/dev/vdb'} + self.assertRaises(exc.HTTPBadRequest, + self._test_create, self.params) + + def test_create_instance_with_device_name_empty(self): + self.bdm[0]['device_name'] = '' + params = {'block_device_mapping': self.bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], self.bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(exc.HTTPBadRequest, + self._test_create, params) + + def test_create_instance_with_device_name_too_long(self): + self.bdm[0]['device_name'] = 'a' * 256, + params = {'block_device_mapping': self.bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], self.bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(exc.HTTPBadRequest, + self._test_create, params) + + def test_create_instance_with_space_in_device_name(self): + self.bdm[0]['device_name'] = 'vd a', + params = {'block_device_mapping': self.bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertTrue(kwargs['legacy_bdm']) + self.assertEqual(kwargs['block_device_mapping'], self.bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(exc.HTTPBadRequest, + self._test_create, params) + + def test_create_instance_with_invalid_size(self): + bdm = [{'delete_on_termination': 1, + 'device_name': 'vda', + 'volume_size': "hello world", + 'volume_id': '11111111-1111-1111-1111-111111111111'}] + params = {'block_device_mapping': bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['block_device_mapping'], bdm) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(exc.HTTPBadRequest, + self._test_create, params) + + def test_create_instance_with_bdm_delete_on_termination(self): + bdm = [{'device_name': 'foo1', 'volume_id': 'fake_vol', + 'delete_on_termination': 1}, + {'device_name': 'foo2', 'volume_id': 'fake_vol', + 'delete_on_termination': True}, + {'device_name': 'foo3', 'volume_id': 'fake_vol', + 'delete_on_termination': 'invalid'}, + {'device_name': 'foo4', 'volume_id': 'fake_vol', + 'delete_on_termination': 0}, + {'device_name': 'foo5', 'volume_id': 'fake_vol', + 'delete_on_termination': False}] + expected_bdm = [ + {'device_name': 'foo1', 'volume_id': 'fake_vol', + 'delete_on_termination': True}, + {'device_name': 'foo2', 'volume_id': 'fake_vol', + 'delete_on_termination': True}, + {'device_name': 'foo3', 'volume_id': 'fake_vol', + 'delete_on_termination': False}, + {'device_name': 'foo4', 'volume_id': 'fake_vol', + 'delete_on_termination': False}, + {'device_name': 'foo5', 'volume_id': 'fake_vol', + 'delete_on_termination': False}] + params = {'block_device_mapping': bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(expected_bdm, kwargs['block_device_mapping']) + return old_create(*args, **kwargs) + + def _validate_bdm(*args, **kwargs): + pass + + self.stubs.Set(compute_api.API, 'create', create) + self.stubs.Set(compute_api.API, '_validate_bdm', _validate_bdm) + self._test_create(params) + + def test_create_instance_decide_format_legacy(self): + ext_info = plugins.LoadedExtensionInfo() + CONF.set_override('extensions_blacklist', + ['os-block-device-mapping', + 'os-block-device-mapping-v1'], + 'osapi_v3') + controller = servers_v3.ServersController(extension_info=ext_info) + bdm = [{'device_name': 'foo1', + 'volume_id': 'fake_vol', + 'delete_on_termination': 1}] + + expected_legacy_flag = True + + old_create = compute_api.API.create + + def create(*args, **kwargs): + legacy_bdm = kwargs.get('legacy_bdm', True) + self.assertEqual(legacy_bdm, expected_legacy_flag) + return old_create(*args, **kwargs) + + def _validate_bdm(*args, **kwargs): + pass + + self.stubs.Set(compute_api.API, 'create', create) + self.stubs.Set(compute_api.API, '_validate_bdm', + _validate_bdm) + + self._test_create({}, override_controller=controller) + + params = {'block_device_mapping': bdm} + self._test_create(params, override_controller=controller) + + def test_create_instance_both_bdm_formats(self): + bdm = [{'device_name': 'foo'}] + bdm_v2 = [{'source_type': 'volume', + 'uuid': 'fake_vol'}] + params = {'block_device_mapping': bdm, + 'block_device_mapping_v2': bdm_v2} + self.assertRaises(exc.HTTPBadRequest, self._test_create, params) + + +class BlockDeviceMappingTestV2(BlockDeviceMappingTestV21): + + def _setup_controller(self): + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {'os-volumes': 'fake'} + self.controller = servers_v2.Controller(self.ext_mgr) + self.ext_mgr_no_vols = extensions.ExtensionManager() + self.ext_mgr_no_vols.extensions = {} + self.no_volumes_controller = servers_v2.Controller( + self.ext_mgr_no_vols) + + def test_create_instance_with_volumes_disabled(self): + bdm = [{'device_name': 'foo'}] + params = {'block_device_mapping': bdm} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertIsNone(kwargs['block_device_mapping']) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create(params, + override_controller=self.no_volumes_controller) + + def test_create_instance_decide_format_legacy(self): + ext_mgr = extensions.ExtensionManager() + ext_mgr.extensions = {'os-volumes': 'fake', + 'os-block-device-mapping-v2-boot': 'fake'} + controller = servers_v2.Controller(self.ext_mgr) + bdm = [{'device_name': 'foo1', + 'volume_id': 'fake_vol', + 'delete_on_termination': 1}] + + expected_legacy_flag = True + + old_create = compute_api.API.create + + def create(*args, **kwargs): + legacy_bdm = kwargs.get('legacy_bdm', True) + self.assertEqual(legacy_bdm, expected_legacy_flag) + return old_create(*args, **kwargs) + + def _validate_bdm(*args, **kwargs): + pass + + self.stubs.Set(compute_api.API, 'create', create) + self.stubs.Set(compute_api.API, '_validate_bdm', + _validate_bdm) + + self._test_create({}, override_controller=controller) + + params = {'block_device_mapping': bdm} + self._test_create(params, override_controller=controller) + + +class TestServerCreateRequestXMLDeserializer(test.TestCase): + + def setUp(self): + super(TestServerCreateRequestXMLDeserializer, self).setUp() + self.deserializer = servers_v2.CreateDeserializer() + + def test_request_with_block_device_mapping(self): + serial_request = """ + <server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" imageRef="1" flavorRef="1"> + <block_device_mapping> + <mapping volume_id="7329b667-50c7-46a6-b913-cb2a09dfeee0" + device_name="/dev/vda" virtual_name="root" + delete_on_termination="False" /> + <mapping snapshot_id="f31efb24-34d2-43e1-8b44-316052956a39" + device_name="/dev/vdb" virtual_name="ephemeral0" + delete_on_termination="False" /> + <mapping device_name="/dev/vdc" no_device="True" /> + </block_device_mapping> + </server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "block_device_mapping": [ + { + "volume_id": "7329b667-50c7-46a6-b913-cb2a09dfeee0", + "device_name": "/dev/vda", + "virtual_name": "root", + "delete_on_termination": False, + }, + { + "snapshot_id": "f31efb24-34d2-43e1-8b44-316052956a39", + "device_name": "/dev/vdb", + "virtual_name": "ephemeral0", + "delete_on_termination": False, + }, + { + "device_name": "/dev/vdc", + "no_device": True, + }, + ] + }} + self.assertEqual(request['body'], expected) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_cells.py b/nova/tests/unit/api/openstack/compute/contrib/test_cells.py new file mode 100644 index 0000000000..1460d33e3a --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_cells.py @@ -0,0 +1,698 @@ +# Copyright 2011-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 copy + +from lxml import etree +from oslo.utils import timeutils +from webob import exc + +from nova.api.openstack.compute.contrib import cells as cells_ext_v2 +from nova.api.openstack.compute.plugins.v3 import cells as cells_ext_v21 +from nova.api.openstack import extensions +from nova.api.openstack import xmlutil +from nova.cells import rpcapi as cells_rpcapi +from nova import context +from nova import exception +from nova import rpc +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import utils + + +class BaseCellsTest(test.NoDBTestCase): + def setUp(self): + super(BaseCellsTest, self).setUp() + + self.fake_cells = [ + dict(id=1, name='cell1', is_parent=True, + weight_scale=1.0, weight_offset=0.0, + transport_url='rabbit://bob:xxxx@r1.example.org/'), + dict(id=2, name='cell2', is_parent=False, + weight_scale=1.0, weight_offset=0.0, + transport_url='rabbit://alice:qwerty@r2.example.org/')] + + self.fake_capabilities = [ + {'cap1': '0,1', 'cap2': '2,3'}, + {'cap3': '4,5', 'cap4': '5,6'}] + + def fake_cell_get(_self, context, cell_name): + for cell in self.fake_cells: + if cell_name == cell['name']: + return cell + else: + raise exception.CellNotFound(cell_name=cell_name) + + def fake_cell_create(_self, context, values): + cell = dict(id=1) + cell.update(values) + return cell + + def fake_cell_update(_self, context, cell_id, values): + cell = fake_cell_get(_self, context, cell_id) + cell.update(values) + return cell + + def fake_cells_api_get_all_cell_info(*args): + return self._get_all_cell_info(*args) + + self.stubs.Set(cells_rpcapi.CellsAPI, 'cell_get', fake_cell_get) + self.stubs.Set(cells_rpcapi.CellsAPI, 'cell_update', fake_cell_update) + self.stubs.Set(cells_rpcapi.CellsAPI, 'cell_create', fake_cell_create) + self.stubs.Set(cells_rpcapi.CellsAPI, 'get_cell_info_for_neighbors', + fake_cells_api_get_all_cell_info) + + def _get_all_cell_info(self, *args): + def insecure_transport_url(url): + transport_url = rpc.get_transport_url(url) + transport_url.hosts[0].password = None + return str(transport_url) + + cells = copy.deepcopy(self.fake_cells) + cells[0]['transport_url'] = insecure_transport_url( + cells[0]['transport_url']) + cells[1]['transport_url'] = insecure_transport_url( + cells[1]['transport_url']) + for i, cell in enumerate(cells): + cell['capabilities'] = self.fake_capabilities[i] + return cells + + +class CellsTestV21(BaseCellsTest): + cell_extension = 'compute_extension:v3:os-cells' + bad_request = exception.ValidationError + + def _get_cell_controller(self, ext_mgr): + return cells_ext_v21.CellsController() + + def _get_request(self, resource): + return fakes.HTTPRequest.blank('/v2/fake/' + resource) + + def setUp(self): + super(CellsTestV21, self).setUp() + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.controller = self._get_cell_controller(self.ext_mgr) + self.context = context.get_admin_context() + self.flags(enable=True, group='cells') + + def test_index(self): + req = self._get_request("cells") + res_dict = self.controller.index(req) + + self.assertEqual(len(res_dict['cells']), 2) + for i, cell in enumerate(res_dict['cells']): + self.assertEqual(cell['name'], self.fake_cells[i]['name']) + self.assertNotIn('capabilitiles', cell) + self.assertNotIn('password', cell) + + def test_detail(self): + req = self._get_request("cells/detail") + res_dict = self.controller.detail(req) + + self.assertEqual(len(res_dict['cells']), 2) + for i, cell in enumerate(res_dict['cells']): + self.assertEqual(cell['name'], self.fake_cells[i]['name']) + self.assertEqual(cell['capabilities'], self.fake_capabilities[i]) + self.assertNotIn('password', cell) + + def test_show_bogus_cell_raises(self): + req = self._get_request("cells/bogus") + self.assertRaises(exc.HTTPNotFound, self.controller.show, req, 'bogus') + + def test_get_cell_by_name(self): + req = self._get_request("cells/cell1") + res_dict = self.controller.show(req, 'cell1') + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'cell1') + self.assertEqual(cell['rpc_host'], 'r1.example.org') + self.assertNotIn('password', cell) + + def _cell_delete(self): + call_info = {'delete_called': 0} + + def fake_cell_delete(inst, context, cell_name): + self.assertEqual(cell_name, 'cell999') + call_info['delete_called'] += 1 + + self.stubs.Set(cells_rpcapi.CellsAPI, 'cell_delete', fake_cell_delete) + + req = self._get_request("cells/cell999") + req.environ['nova.context'] = self.context + self.controller.delete(req, 'cell999') + self.assertEqual(call_info['delete_called'], 1) + + def test_cell_delete(self): + # Test cell delete with just cell policy + rules = {"default": "is_admin:true", + self.cell_extension: "is_admin:true"} + self.policy.set_rules(rules) + self._cell_delete() + + def test_cell_delete_with_delete_policy(self): + self._cell_delete() + + def test_delete_bogus_cell_raises(self): + def fake_cell_delete(inst, context, cell_name): + return 0 + + self.stubs.Set(cells_rpcapi.CellsAPI, 'cell_delete', fake_cell_delete) + + req = self._get_request("cells/cell999") + req.environ['nova.context'] = self.context + self.assertRaises(exc.HTTPNotFound, self.controller.delete, req, + 'cell999') + + def test_cell_delete_fails_for_invalid_policy(self): + def fake_cell_delete(inst, context, cell_name): + pass + + self.stubs.Set(cells_rpcapi.CellsAPI, 'cell_delete', fake_cell_delete) + + req = self._get_request("cells/cell999") + req.environ['nova.context'] = self.context + req.environ["nova.context"].is_admin = False + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.delete, req, 'cell999') + + def _cell_create_parent(self): + body = {'cell': {'name': 'meow', + 'username': 'fred', + 'password': 'fubar', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + req.environ['nova.context'] = self.context + res_dict = self.controller.create(req, body=body) + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'meow') + self.assertEqual(cell['username'], 'fred') + self.assertEqual(cell['rpc_host'], 'r3.example.org') + self.assertEqual(cell['type'], 'parent') + self.assertNotIn('password', cell) + self.assertNotIn('is_parent', cell) + + def test_cell_create_parent(self): + # Test create with just cells policy + rules = {"default": "is_admin:true", + self.cell_extension: "is_admin:true"} + self.policy.set_rules(rules) + self._cell_create_parent() + + def test_cell_create_parent_with_create_policy(self): + self._cell_create_parent() + + def _cell_create_child(self): + body = {'cell': {'name': 'meow', + 'username': 'fred', + 'password': 'fubar', + 'rpc_host': 'r3.example.org', + 'type': 'child'}} + + req = self._get_request("cells") + req.environ['nova.context'] = self.context + res_dict = self.controller.create(req, body=body) + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'meow') + self.assertEqual(cell['username'], 'fred') + self.assertEqual(cell['rpc_host'], 'r3.example.org') + self.assertEqual(cell['type'], 'child') + self.assertNotIn('password', cell) + self.assertNotIn('is_parent', cell) + + def test_cell_create_child(self): + # Test create with just cells policy + rules = {"default": "is_admin:true", + self.cell_extension: "is_admin:true"} + self.policy.set_rules(rules) + self._cell_create_child() + + def test_cell_create_child_with_create_policy(self): + self._cell_create_child() + + def test_cell_create_no_name_raises(self): + body = {'cell': {'username': 'moocow', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + req.environ['nova.context'] = self.context + self.assertRaises(self.bad_request, + self.controller.create, req, body=body) + + def test_cell_create_name_empty_string_raises(self): + body = {'cell': {'name': '', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + req.environ['nova.context'] = self.context + self.assertRaises(self.bad_request, + self.controller.create, req, body=body) + + def test_cell_create_name_with_bang_raises(self): + body = {'cell': {'name': 'moo!cow', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + req.environ['nova.context'] = self.context + self.assertRaises(self.bad_request, + self.controller.create, req, body=body) + + def test_cell_create_name_with_dot_raises(self): + body = {'cell': {'name': 'moo.cow', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + req.environ['nova.context'] = self.context + res_dict = self.controller.create(req, body=body) + cell = res_dict['cell'] + self.assertEqual(cell['name'], 'moo.cow') + + def test_cell_create_name_with_invalid_type_raises(self): + body = {'cell': {'name': 'moocow', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'invalid'}} + + req = self._get_request("cells") + req.environ['nova.context'] = self.context + self.assertRaises(self.bad_request, + self.controller.create, req, body=body) + + def test_cell_create_fails_for_invalid_policy(self): + body = {'cell': {'name': 'fake'}} + req = self._get_request("cells") + req.environ['nova.context'] = self.context + req.environ['nova.context'].is_admin = False + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.create, req, body=body) + + def _cell_update(self): + body = {'cell': {'username': 'zeb', + 'password': 'sneaky'}} + + req = self._get_request("cells/cell1") + req.environ['nova.context'] = self.context + res_dict = self.controller.update(req, 'cell1', body=body) + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'cell1') + self.assertEqual(cell['rpc_host'], 'r1.example.org') + self.assertEqual(cell['username'], 'zeb') + self.assertNotIn('password', cell) + + def test_cell_update(self): + # Test cell update with just cell policy + rules = {"default": "is_admin:true", + self.cell_extension: "is_admin:true"} + self.policy.set_rules(rules) + self._cell_update() + + def test_cell_update_with_update_policy(self): + self._cell_update() + + def test_cell_update_fails_for_invalid_policy(self): + body = {'cell': {'name': 'got_changed'}} + req = self._get_request("cells/cell1") + req.environ['nova.context'] = self.context + req.environ['nova.context'].is_admin = False + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.create, req, body=body) + + def test_cell_update_empty_name_raises(self): + body = {'cell': {'name': '', + 'username': 'zeb', + 'password': 'sneaky'}} + + req = self._get_request("cells/cell1") + req.environ['nova.context'] = self.context + self.assertRaises(self.bad_request, + self.controller.update, req, 'cell1', body=body) + + def test_cell_update_invalid_type_raises(self): + body = {'cell': {'username': 'zeb', + 'type': 'invalid', + 'password': 'sneaky'}} + + req = self._get_request("cells/cell1") + req.environ['nova.context'] = self.context + self.assertRaises(self.bad_request, + self.controller.update, req, 'cell1', body=body) + + def test_cell_update_without_type_specified(self): + body = {'cell': {'username': 'wingwj'}} + + req = self._get_request("cells/cell1") + req.environ['nova.context'] = self.context + res_dict = self.controller.update(req, 'cell1', body=body) + cell = res_dict['cell'] + + self.assertEqual(cell['name'], 'cell1') + self.assertEqual(cell['rpc_host'], 'r1.example.org') + self.assertEqual(cell['username'], 'wingwj') + self.assertEqual(cell['type'], 'parent') + + def test_cell_update_with_type_specified(self): + body1 = {'cell': {'username': 'wingwj', 'type': 'child'}} + body2 = {'cell': {'username': 'wingwj', 'type': 'parent'}} + + req1 = self._get_request("cells/cell1") + req1.environ['nova.context'] = self.context + res_dict1 = self.controller.update(req1, 'cell1', body=body1) + cell1 = res_dict1['cell'] + + req2 = self._get_request("cells/cell2") + req2.environ['nova.context'] = self.context + res_dict2 = self.controller.update(req2, 'cell2', body=body2) + cell2 = res_dict2['cell'] + + self.assertEqual(cell1['name'], 'cell1') + self.assertEqual(cell1['rpc_host'], 'r1.example.org') + self.assertEqual(cell1['username'], 'wingwj') + self.assertEqual(cell1['type'], 'child') + + self.assertEqual(cell2['name'], 'cell2') + self.assertEqual(cell2['rpc_host'], 'r2.example.org') + self.assertEqual(cell2['username'], 'wingwj') + self.assertEqual(cell2['type'], 'parent') + + def test_cell_info(self): + caps = ['cap1=a;b', 'cap2=c;d'] + self.flags(name='darksecret', capabilities=caps, group='cells') + + req = self._get_request("cells/info") + res_dict = self.controller.info(req) + cell = res_dict['cell'] + cell_caps = cell['capabilities'] + + self.assertEqual(cell['name'], 'darksecret') + self.assertEqual(cell_caps['cap1'], 'a;b') + self.assertEqual(cell_caps['cap2'], 'c;d') + + def test_show_capacities(self): + if (self.cell_extension == 'compute_extension:cells'): + self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True) + self.mox.StubOutWithMock(self.controller.cells_rpcapi, + 'get_capacities') + response = {"ram_free": + {"units_by_mb": {"8192": 0, "512": 13, + "4096": 1, "2048": 3, "16384": 0}, + "total_mb": 7680}, + "disk_free": + {"units_by_mb": {"81920": 11, "20480": 46, + "40960": 23, "163840": 5, "0": 0}, + "total_mb": 1052672} + } + self.controller.cells_rpcapi.\ + get_capacities(self.context, cell_name=None).AndReturn(response) + self.mox.ReplayAll() + req = self._get_request("cells/capacities") + req.environ["nova.context"] = self.context + res_dict = self.controller.capacities(req) + self.assertEqual(response, res_dict['cell']['capacities']) + + def test_show_capacity_fails_with_non_admin_context(self): + if (self.cell_extension == 'compute_extension:cells'): + self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True) + rules = {self.cell_extension: "is_admin:true"} + self.policy.set_rules(rules) + + self.mox.ReplayAll() + req = self._get_request("cells/capacities") + req.environ["nova.context"] = self.context + req.environ["nova.context"].is_admin = False + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.capacities, req) + + def test_show_capacities_for_invalid_cell(self): + if (self.cell_extension == 'compute_extension:cells'): + self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True) + self.mox.StubOutWithMock(self.controller.cells_rpcapi, + 'get_capacities') + self.controller.cells_rpcapi. \ + get_capacities(self.context, cell_name="invalid_cell").AndRaise( + exception.CellNotFound(cell_name="invalid_cell")) + self.mox.ReplayAll() + req = self._get_request("cells/invalid_cell/capacities") + req.environ["nova.context"] = self.context + self.assertRaises(exc.HTTPNotFound, + self.controller.capacities, req, "invalid_cell") + + def test_show_capacities_for_cell(self): + if (self.cell_extension == 'compute_extension:cells'): + self.ext_mgr.is_loaded('os-cell-capacities').AndReturn(True) + self.mox.StubOutWithMock(self.controller.cells_rpcapi, + 'get_capacities') + response = {"ram_free": + {"units_by_mb": {"8192": 0, "512": 13, + "4096": 1, "2048": 3, "16384": 0}, + "total_mb": 7680}, + "disk_free": + {"units_by_mb": {"81920": 11, "20480": 46, + "40960": 23, "163840": 5, "0": 0}, + "total_mb": 1052672} + } + self.controller.cells_rpcapi.\ + get_capacities(self.context, cell_name='cell_name').\ + AndReturn(response) + self.mox.ReplayAll() + req = self._get_request("cells/capacities") + req.environ["nova.context"] = self.context + res_dict = self.controller.capacities(req, 'cell_name') + self.assertEqual(response, res_dict['cell']['capacities']) + + def test_sync_instances(self): + call_info = {} + + def sync_instances(self, context, **kwargs): + call_info['project_id'] = kwargs.get('project_id') + call_info['updated_since'] = kwargs.get('updated_since') + call_info['deleted'] = kwargs.get('deleted') + + self.stubs.Set(cells_rpcapi.CellsAPI, 'sync_instances', sync_instances) + + req = self._get_request("cells/sync_instances") + req.environ['nova.context'] = self.context + body = {} + self.controller.sync_instances(req, body=body) + self.assertIsNone(call_info['project_id']) + self.assertIsNone(call_info['updated_since']) + + body = {'project_id': 'test-project'} + self.controller.sync_instances(req, body=body) + self.assertEqual(call_info['project_id'], 'test-project') + self.assertIsNone(call_info['updated_since']) + + expected = timeutils.utcnow().isoformat() + if not expected.endswith("+00:00"): + expected += "+00:00" + + body = {'updated_since': expected} + self.controller.sync_instances(req, body=body) + self.assertIsNone(call_info['project_id']) + self.assertEqual(call_info['updated_since'], expected) + + body = {'updated_since': 'skjdfkjsdkf'} + self.assertRaises(self.bad_request, + self.controller.sync_instances, req, body=body) + + body = {'deleted': False} + self.controller.sync_instances(req, body=body) + self.assertIsNone(call_info['project_id']) + self.assertIsNone(call_info['updated_since']) + self.assertEqual(call_info['deleted'], False) + + body = {'deleted': 'False'} + self.controller.sync_instances(req, body=body) + self.assertIsNone(call_info['project_id']) + self.assertIsNone(call_info['updated_since']) + self.assertEqual(call_info['deleted'], False) + + body = {'deleted': 'True'} + self.controller.sync_instances(req, body=body) + self.assertIsNone(call_info['project_id']) + self.assertIsNone(call_info['updated_since']) + self.assertEqual(call_info['deleted'], True) + + body = {'deleted': 'foo'} + self.assertRaises(self.bad_request, + self.controller.sync_instances, req, body=body) + + body = {'foo': 'meow'} + self.assertRaises(self.bad_request, + self.controller.sync_instances, req, body=body) + + def test_sync_instances_fails_for_invalid_policy(self): + def sync_instances(self, context, **kwargs): + pass + + self.stubs.Set(cells_rpcapi.CellsAPI, 'sync_instances', sync_instances) + + req = self._get_request("cells/sync_instances") + req.environ['nova.context'] = self.context + req.environ['nova.context'].is_admin = False + + body = {} + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.sync_instances, req, body=body) + + def test_cells_disabled(self): + self.flags(enable=False, group='cells') + + req = self._get_request("cells") + self.assertRaises(exc.HTTPNotImplemented, + self.controller.index, req) + + req = self._get_request("cells/detail") + self.assertRaises(exc.HTTPNotImplemented, + self.controller.detail, req) + + req = self._get_request("cells/cell1") + self.assertRaises(exc.HTTPNotImplemented, + self.controller.show, req) + + self.assertRaises(exc.HTTPNotImplemented, + self.controller.delete, req, 'cell999') + + req = self._get_request("cells/cells") + self.assertRaises(exc.HTTPNotImplemented, + self.controller.create, req, {}) + + req = self._get_request("cells/capacities") + self.assertRaises(exc.HTTPNotImplemented, + self.controller.capacities, req) + + req = self._get_request("cells/sync_instances") + self.assertRaises(exc.HTTPNotImplemented, + self.controller.sync_instances, req, {}) + + +class CellsTestV2(CellsTestV21): + cell_extension = 'compute_extension:cells' + bad_request = exc.HTTPBadRequest + + def _get_cell_controller(self, ext_mgr): + return cells_ext_v2.Controller(ext_mgr) + + def test_cell_create_name_with_dot_raises(self): + body = {'cell': {'name': 'moo.cow', + 'username': 'fred', + 'password': 'secret', + 'rpc_host': 'r3.example.org', + 'type': 'parent'}} + + req = self._get_request("cells") + req.environ['nova.context'] = self.context + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, req, body=body) + + +class TestCellsXMLSerializer(BaseCellsTest): + def test_multiple_cells(self): + fixture = {'cells': self._get_all_cell_info()} + + serializer = cells_ext_v2.CellsTemplate() + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, '{%s}cells' % xmlutil.XMLNS_V10) + self.assertEqual(len(res_tree), 2) + self.assertEqual(res_tree[0].tag, '{%s}cell' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree[1].tag, '{%s}cell' % xmlutil.XMLNS_V10) + + def test_single_cell_with_caps(self): + cell = {'id': 1, + 'name': 'darksecret', + 'username': 'meow', + 'capabilities': {'cap1': 'a;b', + 'cap2': 'c;d'}} + fixture = {'cell': cell} + + serializer = cells_ext_v2.CellTemplate() + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, '{%s}cell' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree.get('name'), 'darksecret') + self.assertEqual(res_tree.get('username'), 'meow') + self.assertIsNone(res_tree.get('password')) + self.assertEqual(len(res_tree), 1) + + child = res_tree[0] + self.assertEqual(child.tag, + '{%s}capabilities' % xmlutil.XMLNS_V10) + for elem in child: + self.assertIn(elem.tag, ('{%s}cap1' % xmlutil.XMLNS_V10, + '{%s}cap2' % xmlutil.XMLNS_V10)) + if elem.tag == '{%s}cap1' % xmlutil.XMLNS_V10: + self.assertEqual(elem.text, 'a;b') + elif elem.tag == '{%s}cap2' % xmlutil.XMLNS_V10: + self.assertEqual(elem.text, 'c;d') + + def test_single_cell_without_caps(self): + cell = {'id': 1, + 'username': 'woof', + 'name': 'darksecret'} + fixture = {'cell': cell} + + serializer = cells_ext_v2.CellTemplate() + output = serializer.serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, '{%s}cell' % xmlutil.XMLNS_V10) + self.assertEqual(res_tree.get('name'), 'darksecret') + self.assertEqual(res_tree.get('username'), 'woof') + self.assertIsNone(res_tree.get('password')) + self.assertEqual(len(res_tree), 0) + + +class TestCellsXMLDeserializer(test.NoDBTestCase): + def test_cell_deserializer(self): + caps_dict = {'cap1': 'a;b', + 'cap2': 'c;d'} + caps_xml = ("<capabilities><cap1>a;b</cap1>" + "<cap2>c;d</cap2></capabilities>") + expected = {'cell': {'name': 'testcell1', + 'type': 'child', + 'rpc_host': 'localhost', + 'capabilities': caps_dict}} + intext = ("<?xml version='1.0' encoding='UTF-8'?>\n" + "<cell><name>testcell1</name><type>child</type>" + "<rpc_host>localhost</rpc_host>" + "%s</cell>") % caps_xml + deserializer = cells_ext_v2.CellDeserializer() + result = deserializer.deserialize(intext) + self.assertEqual(dict(body=expected), result) + + def test_with_corrupt_xml(self): + deserializer = cells_ext_v2.CellDeserializer() + self.assertRaises( + exception.MalformedRequestBody, + deserializer.deserialize, + utils.killer_xml_body()) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_certificates.py b/nova/tests/unit/api/openstack/compute/contrib/test_certificates.py new file mode 100644 index 0000000000..c7066516d8 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_certificates.py @@ -0,0 +1,140 @@ +# Copyright (c) 2012 OpenStack Foundation +# All Rights Reserved. +# Copyright 2013 Red Hat, 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. + +from lxml import etree +import mock +import mox +from webob import exc + +from nova.api.openstack.compute.contrib import certificates as certificates_v2 +from nova.api.openstack.compute.plugins.v3 import certificates \ + as certificates_v21 +from nova.cert import rpcapi +from nova import context +from nova import exception +from nova.openstack.common import policy as common_policy +from nova import policy +from nova import test +from nova.tests.unit.api.openstack import fakes + + +class CertificatesTestV21(test.NoDBTestCase): + certificates = certificates_v21 + url = '/v3/os-certificates' + certificate_show_extension = 'compute_extension:v3:os-certificates:show' + certificate_create_extension = \ + 'compute_extension:v3:os-certificates:create' + + def setUp(self): + super(CertificatesTestV21, self).setUp() + self.context = context.RequestContext('fake', 'fake') + self.controller = self.certificates.CertificatesController() + + def test_translate_certificate_view(self): + pk, cert = 'fakepk', 'fakecert' + view = self.certificates._translate_certificate_view(cert, pk) + self.assertEqual(view['data'], cert) + self.assertEqual(view['private_key'], pk) + + def test_certificates_show_root(self): + self.mox.StubOutWithMock(self.controller.cert_rpcapi, 'fetch_ca') + + self.controller.cert_rpcapi.fetch_ca( + mox.IgnoreArg(), project_id='fake').AndReturn('fakeroot') + + self.mox.ReplayAll() + + req = fakes.HTTPRequest.blank(self.url + '/root') + res_dict = self.controller.show(req, 'root') + + response = {'certificate': {'data': 'fakeroot', 'private_key': None}} + self.assertEqual(res_dict, response) + + def test_certificates_show_policy_failed(self): + rules = { + self.certificate_show_extension: + common_policy.parse_rule("!") + } + policy.set_rules(rules) + req = fakes.HTTPRequest.blank(self.url + '/root') + exc = self.assertRaises(exception.PolicyNotAuthorized, + self.controller.show, req, 'root') + self.assertIn(self.certificate_show_extension, + exc.format_message()) + + def test_certificates_create_certificate(self): + self.mox.StubOutWithMock(self.controller.cert_rpcapi, + 'generate_x509_cert') + + self.controller.cert_rpcapi.generate_x509_cert( + mox.IgnoreArg(), + user_id='fake_user', + project_id='fake').AndReturn(('fakepk', 'fakecert')) + + self.mox.ReplayAll() + + req = fakes.HTTPRequest.blank(self.url) + res_dict = self.controller.create(req) + + response = { + 'certificate': {'data': 'fakecert', + 'private_key': 'fakepk'} + } + self.assertEqual(res_dict, response) + + def test_certificates_create_policy_failed(self): + rules = { + self.certificate_create_extension: + common_policy.parse_rule("!") + } + policy.set_rules(rules) + req = fakes.HTTPRequest.blank(self.url) + exc = self.assertRaises(exception.PolicyNotAuthorized, + self.controller.create, req) + self.assertIn(self.certificate_create_extension, + exc.format_message()) + + @mock.patch.object(rpcapi.CertAPI, 'fetch_ca', + side_effect=exception.CryptoCAFileNotFound(project='fake')) + def test_non_exist_certificates_show(self, mock_fetch_ca): + req = fakes.HTTPRequest.blank(self.url + '/root') + self.assertRaises( + exc.HTTPNotFound, + self.controller.show, + req, 'root') + + +class CertificatesTestV2(CertificatesTestV21): + certificates = certificates_v2 + url = '/v2/fake/os-certificates' + certificate_show_extension = 'compute_extension:certificates' + certificate_create_extension = 'compute_extension:certificates' + + +class CertificatesSerializerTest(test.NoDBTestCase): + def test_index_serializer(self): + serializer = certificates_v2.CertificateTemplate() + text = serializer.serialize(dict( + certificate=dict( + data='fakecert', + private_key='fakepk'), + )) + + tree = etree.fromstring(text) + + self.assertEqual('certificate', tree.tag) + self.assertEqual('fakepk', tree.get('private_key')) + self.assertEqual('fakecert', tree.get('data')) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe.py b/nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe.py new file mode 100644 index 0000000000..ab3b1a58cc --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe.py @@ -0,0 +1,210 @@ +# Copyright 2011 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 uuid as uuid_lib + +from lxml import etree +from oslo.config import cfg +from oslo.utils import timeutils +from webob import exc + +from nova.api.openstack.compute.contrib import cloudpipe as cloudpipe_v2 +from nova.api.openstack.compute.plugins.v3 import cloudpipe as cloudpipe_v21 +from nova.api.openstack import wsgi +from nova.compute import utils as compute_utils +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_network +from nova.tests.unit import matchers +from nova import utils + +CONF = cfg.CONF +CONF.import_opt('vpn_image_id', 'nova.cloudpipe.pipelib') + + +project_id = str(uuid_lib.uuid4().hex) +uuid = str(uuid_lib.uuid4()) + + +def fake_vpn_instance(): + return { + 'id': 7, 'image_ref': CONF.vpn_image_id, 'vm_state': 'active', + 'created_at': timeutils.parse_strtime('1981-10-20T00:00:00.000000'), + 'uuid': uuid, 'project_id': project_id, + } + + +def compute_api_get_all_empty(context, search_opts=None): + return [] + + +def compute_api_get_all(context, search_opts=None): + return [fake_vpn_instance()] + + +def utils_vpn_ping(addr, port, timoeout=0.05, session_id=None): + return True + + +class CloudpipeTestV21(test.NoDBTestCase): + cloudpipe = cloudpipe_v21 + url = '/v2/fake/os-cloudpipe' + + def setUp(self): + super(CloudpipeTestV21, self).setUp() + self.controller = self.cloudpipe.CloudpipeController() + self.stubs.Set(self.controller.compute_api, "get_all", + compute_api_get_all_empty) + self.stubs.Set(utils, 'vpn_ping', utils_vpn_ping) + + def test_cloudpipe_list_no_network(self): + + def fake_get_nw_info_for_instance(instance): + return {} + + self.stubs.Set(compute_utils, "get_nw_info_for_instance", + fake_get_nw_info_for_instance) + self.stubs.Set(self.controller.compute_api, "get_all", + compute_api_get_all) + req = fakes.HTTPRequest.blank(self.url) + res_dict = self.controller.index(req) + response = {'cloudpipes': [{'project_id': project_id, + 'instance_id': uuid, + 'created_at': '1981-10-20T00:00:00Z'}]} + self.assertEqual(res_dict, response) + + def test_cloudpipe_list(self): + + def network_api_get(context, network_id): + self.assertEqual(context.project_id, project_id) + return {'vpn_public_address': '127.0.0.1', + 'vpn_public_port': 22} + + def fake_get_nw_info_for_instance(instance): + return fake_network.fake_get_instance_nw_info(self.stubs) + + self.stubs.Set(compute_utils, "get_nw_info_for_instance", + fake_get_nw_info_for_instance) + self.stubs.Set(self.controller.network_api, "get", + network_api_get) + self.stubs.Set(self.controller.compute_api, "get_all", + compute_api_get_all) + req = fakes.HTTPRequest.blank(self.url) + res_dict = self.controller.index(req) + response = {'cloudpipes': [{'project_id': project_id, + 'internal_ip': '192.168.1.100', + 'public_ip': '127.0.0.1', + 'public_port': 22, + 'state': 'running', + 'instance_id': uuid, + 'created_at': '1981-10-20T00:00:00Z'}]} + self.assertThat(res_dict, matchers.DictMatches(response)) + + def test_cloudpipe_create(self): + def launch_vpn_instance(context): + return ([fake_vpn_instance()], 'fake-reservation') + + self.stubs.Set(self.controller.cloudpipe, 'launch_vpn_instance', + launch_vpn_instance) + body = {'cloudpipe': {'project_id': project_id}} + req = fakes.HTTPRequest.blank(self.url) + res_dict = self.controller.create(req, body=body) + + response = {'instance_id': uuid} + self.assertEqual(res_dict, response) + + def test_cloudpipe_create_no_networks(self): + def launch_vpn_instance(context): + raise exception.NoMoreNetworks + + self.stubs.Set(self.controller.cloudpipe, 'launch_vpn_instance', + launch_vpn_instance) + body = {'cloudpipe': {'project_id': project_id}} + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, req, body=body) + + def test_cloudpipe_create_already_running(self): + def launch_vpn_instance(*args, **kwargs): + self.fail("Method should not have been called") + + self.stubs.Set(self.controller.cloudpipe, 'launch_vpn_instance', + launch_vpn_instance) + self.stubs.Set(self.controller.compute_api, "get_all", + compute_api_get_all) + body = {'cloudpipe': {'project_id': project_id}} + req = fakes.HTTPRequest.blank(self.url) + res_dict = self.controller.create(req, body=body) + response = {'instance_id': uuid} + self.assertEqual(res_dict, response) + + def test_cloudpipe_create_with_bad_project_id_failed(self): + body = {'cloudpipe': {'project_id': 'bad.project.id'}} + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(exception.ValidationError, + self.controller.create, req, body=body) + + +class CloudpipeTestV2(CloudpipeTestV21): + cloudpipe = cloudpipe_v2 + + def test_cloudpipe_create_with_bad_project_id_failed(self): + pass + + +class CloudpipesXMLSerializerTestV2(test.NoDBTestCase): + def test_default_serializer(self): + serializer = cloudpipe_v2.CloudpipeTemplate() + exemplar = dict(cloudpipe=dict(instance_id='1234-1234-1234-1234')) + text = serializer.serialize(exemplar) + tree = etree.fromstring(text) + self.assertEqual('cloudpipe', tree.tag) + for child in tree: + self.assertIn(child.tag, exemplar['cloudpipe']) + self.assertEqual(child.text, exemplar['cloudpipe'][child.tag]) + + def test_index_serializer(self): + serializer = cloudpipe_v2.CloudpipesTemplate() + exemplar = dict(cloudpipes=[ + dict( + project_id='1234', + public_ip='1.2.3.4', + public_port='321', + instance_id='1234-1234-1234-1234', + created_at=timeutils.isotime(), + state='running'), + dict( + project_id='4321', + public_ip='4.3.2.1', + public_port='123', + state='pending')]) + text = serializer.serialize(exemplar) + tree = etree.fromstring(text) + self.assertEqual('cloudpipes', tree.tag) + self.assertEqual(len(exemplar['cloudpipes']), len(tree)) + for idx, cl_pipe in enumerate(tree): + kp_data = exemplar['cloudpipes'][idx] + for child in cl_pipe: + self.assertIn(child.tag, kp_data) + self.assertEqual(child.text, kp_data[child.tag]) + + def test_deserializer(self): + deserializer = wsgi.XMLDeserializer() + exemplar = dict(cloudpipe=dict(project_id='4321')) + intext = ("<?xml version='1.0' encoding='UTF-8'?>\n" + '<cloudpipe><project_id>4321</project_id></cloudpipe>') + result = deserializer.deserialize(intext)['body'] + self.assertEqual(result, exemplar) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe_update.py b/nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe_update.py new file mode 100644 index 0000000000..23faf6275a --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_cloudpipe_update.py @@ -0,0 +1,99 @@ +# Copyright 2012 IBM Corp. +# +# 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 webob + +from nova.api.openstack.compute.contrib import cloudpipe_update as clup_v2 +from nova.api.openstack.compute.plugins.v3 import cloudpipe as clup_v21 +from nova import db +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_network + + +fake_networks = [fake_network.fake_network(1), + fake_network.fake_network(2)] + + +def fake_project_get_networks(context, project_id, associate=True): + return fake_networks + + +def fake_network_update(context, network_id, values): + for network in fake_networks: + if network['id'] == network_id: + for key in values: + network[key] = values[key] + + +class CloudpipeUpdateTestV21(test.NoDBTestCase): + bad_request = exception.ValidationError + + def setUp(self): + super(CloudpipeUpdateTestV21, self).setUp() + self.stubs.Set(db, "project_get_networks", fake_project_get_networks) + self.stubs.Set(db, "network_update", fake_network_update) + self._setup() + + def _setup(self): + self.controller = clup_v21.CloudpipeController() + + def _check_status(self, expected_status, res, controller_methord): + self.assertEqual(expected_status, controller_methord.wsgi_code) + + def test_cloudpipe_configure_project(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/os-cloudpipe/configure-project') + body = {"configure_project": {"vpn_ip": "1.2.3.4", "vpn_port": 222}} + result = self.controller.update(req, 'configure-project', + body=body) + self._check_status(202, result, self.controller.update) + self.assertEqual(fake_networks[0]['vpn_public_address'], "1.2.3.4") + self.assertEqual(fake_networks[0]['vpn_public_port'], 222) + + def test_cloudpipe_configure_project_bad_url(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/os-cloudpipe/configure-projectx') + body = {"configure_project": {"vpn_ip": "1.2.3.4", "vpn_port": 222}} + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, + 'configure-projectx', body=body) + + def test_cloudpipe_configure_project_bad_data(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/os-cloudpipe/configure-project') + body = {"configure_project": {"vpn_ipxx": "1.2.3.4", "vpn_port": 222}} + self.assertRaises(self.bad_request, + self.controller.update, req, + 'configure-project', body=body) + + def test_cloudpipe_configure_project_bad_vpn_port(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/os-cloudpipe/configure-project') + body = {"configure_project": {"vpn_ipxx": "1.2.3.4", + "vpn_port": "foo"}} + self.assertRaises(self.bad_request, + self.controller.update, req, + 'configure-project', body=body) + + +class CloudpipeUpdateTestV2(CloudpipeUpdateTestV21): + bad_request = webob.exc.HTTPBadRequest + + def _setup(self): + self.controller = clup_v2.CloudpipeUpdateController() + + def _check_status(self, expected_status, res, controller_methord): + self.assertEqual(expected_status, res.status_int) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_config_drive.py b/nova/tests/unit/api/openstack/compute/contrib/test_config_drive.py new file mode 100644 index 0000000000..ef94db0d23 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_config_drive.py @@ -0,0 +1,260 @@ +# 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 datetime + +from oslo.config import cfg +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import config_drive as config_drive_v2 +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import config_drive \ + as config_drive_v21 +from nova.api.openstack.compute.plugins.v3 import servers as servers_v21 +from nova.api.openstack.compute import servers as servers_v2 +from nova.api.openstack import extensions +from nova.compute import api as compute_api +from nova.compute import flavors +from nova import db +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance +from nova.tests.unit.image import fake + + +CONF = cfg.CONF + + +class ConfigDriveTestV21(test.TestCase): + base_url = '/v2/fake/servers/' + + def _setup_wsgi(self): + self.app = fakes.wsgi_app_v21(init_only=('servers', 'os-config-drive')) + + def _get_config_drive_controller(self): + return config_drive_v21.ConfigDriveController() + + def setUp(self): + super(ConfigDriveTestV21, self).setUp() + self.Controller = self._get_config_drive_controller() + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fake.stub_out_image_service(self.stubs) + self._setup_wsgi() + + def test_show(self): + self.stubs.Set(db, 'instance_get', + fakes.fake_instance_get()) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get()) + req = webob.Request.blank(self.base_url + '1') + req.headers['Content-Type'] = 'application/json' + response = req.get_response(self.app) + self.assertEqual(response.status_int, 200) + res_dict = jsonutils.loads(response.body) + self.assertIn('config_drive', res_dict['server']) + + def test_detail_servers(self): + self.stubs.Set(db, 'instance_get_all_by_filters', + fakes.fake_instance_get_all_by_filters()) + req = fakes.HTTPRequest.blank(self.base_url + 'detail') + res = req.get_response(self.app) + server_dicts = jsonutils.loads(res.body)['servers'] + self.assertNotEqual(len(server_dicts), 0) + for server_dict in server_dicts: + self.assertIn('config_drive', server_dict) + + +class ConfigDriveTestV2(ConfigDriveTestV21): + + def _get_config_drive_controller(self): + return config_drive_v2.Controller() + + def _setup_wsgi(self): + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Config_drive']) + self.app = fakes.wsgi_app(init_only=('servers',)) + + +class ServersControllerCreateTestV21(test.TestCase): + base_url = '/v2/fake/' + bad_request = exception.ValidationError + + def _set_up_controller(self): + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers_v21.ServersController( + extension_info=ext_info) + CONF.set_override('extensions_blacklist', + 'os-config-drive', + 'osapi_v3') + self.no_config_drive_controller = servers_v21.ServersController( + extension_info=ext_info) + + def _verfiy_config_drive(self, **kwargs): + self.assertNotIn('config_drive', kwargs) + + def _initialize_extension(self): + pass + + def setUp(self): + """Shared implementation for tests below that create instance.""" + super(ServersControllerCreateTestV21, self).setUp() + + self.instance_cache_num = 0 + self._set_up_controller() + + def instance_create(context, inst): + inst_type = flavors.get_flavor_by_flavor_id(3) + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + def_image_ref = 'http://localhost/images/%s' % image_uuid + self.instance_cache_num += 1 + instance = fake_instance.fake_db_instance(**{ + 'id': self.instance_cache_num, + 'display_name': inst['display_name'] or 'test', + 'uuid': fakes.FAKE_UUID, + 'instance_type': inst_type, + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': 'fead::1234', + 'image_ref': inst.get('image_ref', def_image_ref), + 'user_id': 'fake', + 'project_id': 'fake', + 'reservation_id': inst['reservation_id'], + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "progress": 0, + "fixed_ips": [], + "task_state": "", + "vm_state": "", + "root_device_name": inst.get('root_device_name', 'vda'), + }) + + return instance + + fake.stub_out_image_service(self.stubs) + self.stubs.Set(db, 'instance_create', instance_create) + + def _test_create_extra(self, params, override_controller): + image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + server = dict(name='server_test', imageRef=image_uuid, flavorRef=2) + server.update(params) + body = dict(server=server) + req = fakes.HTTPRequest.blank(self.base_url + 'servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + if override_controller is not None: + server = override_controller.create(req, body=body).obj['server'] + else: + server = self.controller.create(req, body=body).obj['server'] + + def test_create_instance_with_config_drive_disabled(self): + params = {'config_drive': "False"} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self._verfiy_config_drive(**kwargs) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params, + override_controller=self.no_config_drive_controller) + + def _create_instance_body_of_config_drive(self, param): + self._initialize_extension() + + def create(*args, **kwargs): + self.assertIn('config_drive', kwargs) + return old_create(*args, **kwargs) + + old_create = compute_api.API.create + self.stubs.Set(compute_api.API, 'create', create) + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = ('http://localhost' + self.base_url + 'flavors/3') + body = { + 'server': { + 'name': 'config_drive_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'config_drive': param, + }, + } + + req = fakes.HTTPRequest.blank(self.base_url + 'servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + return req, body + + def test_create_instance_with_config_drive(self): + param = True + req, body = self._create_instance_body_of_config_drive(param) + res = self.controller.create(req, body=body).obj + server = res['server'] + self.assertEqual(fakes.FAKE_UUID, server['id']) + + def test_create_instance_with_config_drive_as_boolean_string(self): + param = 'false' + req, body = self._create_instance_body_of_config_drive(param) + res = self.controller.create(req, body=body).obj + server = res['server'] + self.assertEqual(fakes.FAKE_UUID, server['id']) + + def test_create_instance_with_bad_config_drive(self): + param = 12345 + req, body = self._create_instance_body_of_config_drive(param) + self.assertRaises(self.bad_request, + self.controller.create, req, body=body) + + def test_create_instance_without_config_drive(self): + param = True + req, body = self._create_instance_body_of_config_drive(param) + del body['server']['config_drive'] + res = self.controller.create(req, body=body).obj + server = res['server'] + self.assertEqual(fakes.FAKE_UUID, server['id']) + + def test_create_instance_with_empty_config_drive(self): + param = '' + req, body = self._create_instance_body_of_config_drive(param) + self.assertRaises(exception.ValidationError, + self.controller.create, req, body=body) + + +class ServersControllerCreateTestV2(ServersControllerCreateTestV21): + bad_request = webob.exc.HTTPBadRequest + + def _set_up_controller(self): + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = servers_v2.Controller(self.ext_mgr) + self.no_config_drive_controller = None + + def _verfiy_config_drive(self, **kwargs): + self.assertIsNone(kwargs['config_drive']) + + def _initialize_extension(self): + self.ext_mgr.extensions = {'os-config-drive': 'fake'} + + def test_create_instance_with_empty_config_drive(self): + param = '' + req, body = self._create_instance_body_of_config_drive(param) + res = self.controller.create(req, body=body).obj + server = res['server'] + self.assertEqual(fakes.FAKE_UUID, server['id']) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_console_auth_tokens.py b/nova/tests/unit/api/openstack/compute/contrib/test_console_auth_tokens.py new file mode 100644 index 0000000000..eef4cd62ea --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_console_auth_tokens.py @@ -0,0 +1,103 @@ +# Copyright 2013 Cloudbase Solutions Srl +# 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. + +from oslo.config import cfg +from oslo.serialization import jsonutils +import webob + +from nova.consoleauth import rpcapi as consoleauth_rpcapi +from nova import context +from nova import test +from nova.tests.unit.api.openstack import fakes + +CONF = cfg.CONF +CONF.import_opt('osapi_compute_ext_list', 'nova.api.openstack.compute.contrib') + +_FAKE_CONNECT_INFO = {'instance_uuid': 'fake_instance_uuid', + 'host': 'fake_host', + 'port': 'fake_port', + 'internal_access_path': 'fake_access_path', + 'console_type': 'rdp-html5'} + + +def _fake_check_token(self, context, token): + return _FAKE_CONNECT_INFO + + +def _fake_check_token_not_found(self, context, token): + return None + + +def _fake_check_token_unauthorized(self, context, token): + connect_info = _FAKE_CONNECT_INFO + connect_info['console_type'] = 'unauthorized_console_type' + return connect_info + + +class ConsoleAuthTokensExtensionTest(test.TestCase): + + _FAKE_URL = '/v2/fake/os-console-auth-tokens/1' + + _EXPECTED_OUTPUT = {'console': {'instance_uuid': 'fake_instance_uuid', + 'host': 'fake_host', + 'port': 'fake_port', + 'internal_access_path': + 'fake_access_path'}} + + def setUp(self): + super(ConsoleAuthTokensExtensionTest, self).setUp() + self.stubs.Set(consoleauth_rpcapi.ConsoleAuthAPI, 'check_token', + _fake_check_token) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Console_auth_tokens']) + + ctxt = self._get_admin_context() + self.app = fakes.wsgi_app(init_only=('os-console-auth-tokens',), + fake_auth_context=ctxt) + + def _get_admin_context(self): + ctxt = context.get_admin_context() + ctxt.user_id = 'fake' + ctxt.project_id = 'fake' + return ctxt + + def _create_request(self): + req = webob.Request.blank(self._FAKE_URL) + req.method = "GET" + req.headers["content-type"] = "application/json" + return req + + def test_get_console_connect_info(self): + req = self._create_request() + res = req.get_response(self.app) + self.assertEqual(200, res.status_int) + output = jsonutils.loads(res.body) + self.assertEqual(self._EXPECTED_OUTPUT, output) + + def test_get_console_connect_info_token_not_found(self): + self.stubs.Set(consoleauth_rpcapi.ConsoleAuthAPI, 'check_token', + _fake_check_token_not_found) + req = self._create_request() + res = req.get_response(self.app) + self.assertEqual(404, res.status_int) + + def test_get_console_connect_info_unauthorized_console_type(self): + self.stubs.Set(consoleauth_rpcapi.ConsoleAuthAPI, 'check_token', + _fake_check_token_unauthorized) + req = self._create_request() + res = req.get_response(self.app) + self.assertEqual(401, res.status_int) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_console_output.py b/nova/tests/unit/api/openstack/compute/contrib/test_console_output.py new file mode 100644 index 0000000000..441899a19b --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_console_output.py @@ -0,0 +1,171 @@ +# Copyright 2011 Eldar Nugaev +# 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 string + +from oslo.serialization import jsonutils + +from nova.compute import api as compute_api +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + + +def fake_get_console_output(self, _context, _instance, tail_length): + fixture = [str(i) for i in range(5)] + + if tail_length is None: + pass + elif tail_length == 0: + fixture = [] + else: + fixture = fixture[-int(tail_length):] + + return '\n'.join(fixture) + + +def fake_get_console_output_not_ready(self, _context, _instance, tail_length): + raise exception.InstanceNotReady(instance_id=_instance["uuid"]) + + +def fake_get_console_output_all_characters(self, _ctx, _instance, _tail_len): + return string.printable + + +def fake_get(self, context, instance_uuid, want_objects=False, + expected_attrs=None): + return fake_instance.fake_instance_obj(context, **{'uuid': instance_uuid}) + + +def fake_get_not_found(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + +class ConsoleOutputExtensionTestV21(test.NoDBTestCase): + application_type = "application/json" + action_url = '/v2/fake/servers/1/action' + + def setUp(self): + super(ConsoleOutputExtensionTestV21, self).setUp() + self.stubs.Set(compute_api.API, 'get_console_output', + fake_get_console_output) + self.stubs.Set(compute_api.API, 'get', fake_get) + self.app = self._get_app() + + def _get_app(self): + return fakes.wsgi_app_v21(init_only=('servers', + 'os-console-output')) + + def _get_response(self, length_dict=None): + length_dict = length_dict or {} + body = {'os-getConsoleOutput': length_dict} + req = fakes.HTTPRequest.blank(self.action_url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = self.application_type + res = req.get_response(self.app) + return res + + def test_get_text_console_instance_action(self): + res = self._get_response() + output = jsonutils.loads(res.body) + self.assertEqual(200, res.status_int) + self.assertEqual({'output': '0\n1\n2\n3\n4'}, output) + + def test_get_console_output_with_tail(self): + res = self._get_response(length_dict={'length': 3}) + output = jsonutils.loads(res.body) + self.assertEqual(200, res.status_int) + self.assertEqual({'output': '2\n3\n4'}, output) + + def test_get_console_output_with_none_length(self): + res = self._get_response(length_dict={'length': None}) + output = jsonutils.loads(res.body) + self.assertEqual(200, res.status_int) + self.assertEqual({'output': '0\n1\n2\n3\n4'}, output) + + def test_get_console_output_with_length_as_str(self): + res = self._get_response(length_dict={'length': '3'}) + output = jsonutils.loads(res.body) + self.assertEqual(200, res.status_int) + self.assertEqual({'output': '2\n3\n4'}, output) + + def test_get_console_output_filtered_characters(self): + self.stubs.Set(compute_api.API, 'get_console_output', + fake_get_console_output_all_characters) + res = self._get_response() + output = jsonutils.loads(res.body) + self.assertEqual(200, res.status_int) + expect = string.digits + string.letters + string.punctuation + ' \t\n' + self.assertEqual({'output': expect}, output) + + def test_get_text_console_no_instance(self): + self.stubs.Set(compute_api.API, 'get', fake_get_not_found) + res = self._get_response() + self.assertEqual(404, res.status_int) + + def test_get_text_console_no_instance_on_get_output(self): + self.stubs.Set(compute_api.API, + 'get_console_output', + fake_get_not_found) + res = self._get_response() + self.assertEqual(404, res.status_int) + + def _get_console_output_bad_request_case(self, body): + req = fakes.HTTPRequest.blank(self.action_url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + res = req.get_response(self.app) + self.assertEqual(400, res.status_int) + + def test_get_console_output_with_non_integer_length(self): + body = {'os-getConsoleOutput': {'length': 'NaN'}} + self._get_console_output_bad_request_case(body) + + def test_get_text_console_bad_body(self): + body = {} + self._get_console_output_bad_request_case(body) + + def test_get_console_output_with_length_as_float(self): + body = {'os-getConsoleOutput': {'length': 2.5}} + self._get_console_output_bad_request_case(body) + + def test_get_console_output_not_ready(self): + self.stubs.Set(compute_api.API, 'get_console_output', + fake_get_console_output_not_ready) + res = self._get_response(length_dict={'length': 3}) + self.assertEqual(409, res.status_int) + + def test_not_implemented(self): + self.stubs.Set(compute_api.API, 'get_console_output', + fakes.fake_not_implemented) + res = self._get_response() + self.assertEqual(501, res.status_int) + + def test_get_console_output_with_boolean_length(self): + res = self._get_response(length_dict={'length': True}) + self.assertEqual(400, res.status_int) + + +class ConsoleOutputExtensionTestV2(ConsoleOutputExtensionTestV21): + need_osapi_compute_extension = True + + def _get_app(self): + self.flags(osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Console_output']) + return fakes.wsgi_app(init_only=('servers',)) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_consoles.py b/nova/tests/unit/api/openstack/compute/contrib/test_consoles.py new file mode 100644 index 0000000000..debd1e7f5f --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_consoles.py @@ -0,0 +1,587 @@ +# 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 mock +from oslo.serialization import jsonutils +import webob + +from nova.compute import api as compute_api +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + + +def fake_get_vnc_console(self, _context, _instance, _console_type): + return {'url': 'http://fake'} + + +def fake_get_spice_console(self, _context, _instance, _console_type): + return {'url': 'http://fake'} + + +def fake_get_rdp_console(self, _context, _instance, _console_type): + return {'url': 'http://fake'} + + +def fake_get_serial_console(self, _context, _instance, _console_type): + return {'url': 'http://fake'} + + +def fake_get_vnc_console_invalid_type(self, _context, + _instance, _console_type): + raise exception.ConsoleTypeInvalid(console_type=_console_type) + + +def fake_get_spice_console_invalid_type(self, _context, + _instance, _console_type): + raise exception.ConsoleTypeInvalid(console_type=_console_type) + + +def fake_get_rdp_console_invalid_type(self, _context, + _instance, _console_type): + raise exception.ConsoleTypeInvalid(console_type=_console_type) + + +def fake_get_vnc_console_type_unavailable(self, _context, + _instance, _console_type): + raise exception.ConsoleTypeUnavailable(console_type=_console_type) + + +def fake_get_spice_console_type_unavailable(self, _context, + _instance, _console_type): + raise exception.ConsoleTypeUnavailable(console_type=_console_type) + + +def fake_get_rdp_console_type_unavailable(self, _context, + _instance, _console_type): + raise exception.ConsoleTypeUnavailable(console_type=_console_type) + + +def fake_get_vnc_console_not_ready(self, _context, instance, _console_type): + raise exception.InstanceNotReady(instance_id=instance["uuid"]) + + +def fake_get_spice_console_not_ready(self, _context, instance, _console_type): + raise exception.InstanceNotReady(instance_id=instance["uuid"]) + + +def fake_get_rdp_console_not_ready(self, _context, instance, _console_type): + raise exception.InstanceNotReady(instance_id=instance["uuid"]) + + +def fake_get_vnc_console_not_found(self, _context, instance, _console_type): + raise exception.InstanceNotFound(instance_id=instance["uuid"]) + + +def fake_get_spice_console_not_found(self, _context, instance, _console_type): + raise exception.InstanceNotFound(instance_id=instance["uuid"]) + + +def fake_get_rdp_console_not_found(self, _context, instance, _console_type): + raise exception.InstanceNotFound(instance_id=instance["uuid"]) + + +def fake_get(self, context, instance_uuid, want_objects=False, + expected_attrs=None): + return {'uuid': instance_uuid} + + +def fake_get_not_found(self, context, instance_uuid, want_objects=False, + expected_attrs=None): + raise exception.InstanceNotFound(instance_id=instance_uuid) + + +class ConsolesExtensionTestV21(test.NoDBTestCase): + url = '/v2/fake/servers/1/action' + + def _setup_wsgi(self): + self.app = fakes.wsgi_app_v21(init_only=('servers', + 'os-remote-consoles')) + + def setUp(self): + super(ConsolesExtensionTestV21, self).setUp() + self.stubs.Set(compute_api.API, 'get_vnc_console', + fake_get_vnc_console) + self.stubs.Set(compute_api.API, 'get_spice_console', + fake_get_spice_console) + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console) + self.stubs.Set(compute_api.API, 'get_serial_console', + fake_get_serial_console) + self.stubs.Set(compute_api.API, 'get', fake_get) + self._setup_wsgi() + + def test_get_vnc_console(self): + body = {'os-getVNCConsole': {'type': 'novnc'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + output = jsonutils.loads(res.body) + self.assertEqual(res.status_int, 200) + self.assertEqual(output, + {u'console': {u'url': u'http://fake', u'type': u'novnc'}}) + + def test_get_vnc_console_not_ready(self): + self.stubs.Set(compute_api.API, 'get_vnc_console', + fake_get_vnc_console_not_ready) + body = {'os-getVNCConsole': {'type': 'novnc'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + jsonutils.loads(res.body) + self.assertEqual(res.status_int, 409) + + def test_get_vnc_console_no_type(self): + self.stubs.Set(compute_api.API, 'get_vnc_console', + fake_get_vnc_console_invalid_type) + body = {'os-getVNCConsole': {}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + + def test_get_vnc_console_no_instance(self): + self.stubs.Set(compute_api.API, 'get', fake_get_not_found) + body = {'os-getVNCConsole': {'type': 'novnc'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_get_vnc_console_no_instance_on_console_get(self): + self.stubs.Set(compute_api.API, 'get_vnc_console', + fake_get_vnc_console_not_found) + body = {'os-getVNCConsole': {'type': 'novnc'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_get_vnc_console_invalid_type(self): + body = {'os-getVNCConsole': {'type': 'invalid'}} + self.stubs.Set(compute_api.API, 'get_vnc_console', + fake_get_vnc_console_invalid_type) + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + + def test_get_vnc_console_type_unavailable(self): + body = {'os-getVNCConsole': {'type': 'unavailable'}} + self.stubs.Set(compute_api.API, 'get_vnc_console', + fake_get_vnc_console_type_unavailable) + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(400, res.status_int) + + def test_get_vnc_console_not_implemented(self): + self.stubs.Set(compute_api.API, 'get_vnc_console', + fakes.fake_not_implemented) + + body = {'os-getVNCConsole': {'type': 'novnc'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 501) + + def test_get_spice_console(self): + body = {'os-getSPICEConsole': {'type': 'spice-html5'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + output = jsonutils.loads(res.body) + self.assertEqual(res.status_int, 200) + self.assertEqual(output, + {u'console': {u'url': u'http://fake', u'type': u'spice-html5'}}) + + def test_get_spice_console_not_ready(self): + self.stubs.Set(compute_api.API, 'get_spice_console', + fake_get_spice_console_not_ready) + body = {'os-getSPICEConsole': {'type': 'spice-html5'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + jsonutils.loads(res.body) + self.assertEqual(res.status_int, 409) + + def test_get_spice_console_no_type(self): + self.stubs.Set(compute_api.API, 'get_spice_console', + fake_get_spice_console_invalid_type) + body = {'os-getSPICEConsole': {}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + + def test_get_spice_console_no_instance(self): + self.stubs.Set(compute_api.API, 'get', fake_get_not_found) + body = {'os-getSPICEConsole': {'type': 'spice-html5'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_get_spice_console_no_instance_on_console_get(self): + self.stubs.Set(compute_api.API, 'get_spice_console', + fake_get_spice_console_not_found) + body = {'os-getSPICEConsole': {'type': 'spice-html5'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_get_spice_console_invalid_type(self): + body = {'os-getSPICEConsole': {'type': 'invalid'}} + self.stubs.Set(compute_api.API, 'get_spice_console', + fake_get_spice_console_invalid_type) + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + + def test_get_spice_console_not_implemented(self): + body = {'os-getSPICEConsole': {'type': 'spice-html5'}} + self.stubs.Set(compute_api.API, 'get_spice_console', + fakes.fake_not_implemented) + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 501) + + def test_get_spice_console_type_unavailable(self): + body = {'os-getSPICEConsole': {'type': 'unavailable'}} + self.stubs.Set(compute_api.API, 'get_spice_console', + fake_get_spice_console_type_unavailable) + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(400, res.status_int) + + def test_get_rdp_console(self): + body = {'os-getRDPConsole': {'type': 'rdp-html5'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + output = jsonutils.loads(res.body) + self.assertEqual(res.status_int, 200) + self.assertEqual(output, + {u'console': {u'url': u'http://fake', u'type': u'rdp-html5'}}) + + def test_get_rdp_console_not_ready(self): + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_not_ready) + body = {'os-getRDPConsole': {'type': 'rdp-html5'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + jsonutils.loads(res.body) + self.assertEqual(res.status_int, 409) + + def test_get_rdp_console_no_type(self): + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_invalid_type) + body = {'os-getRDPConsole': {}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + + def test_get_rdp_console_no_instance(self): + self.stubs.Set(compute_api.API, 'get', fake_get_not_found) + body = {'os-getRDPConsole': {'type': 'rdp-html5'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_get_rdp_console_no_instance_on_console_get(self): + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_not_found) + body = {'os-getRDPConsole': {'type': 'rdp-html5'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_get_rdp_console_invalid_type(self): + body = {'os-getRDPConsole': {'type': 'invalid'}} + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_invalid_type) + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + + def test_get_rdp_console_type_unavailable(self): + body = {'os-getRDPConsole': {'type': 'unavailable'}} + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_type_unavailable) + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(400, res.status_int) + + def test_get_vnc_console_with_undefined_param(self): + body = {'os-getVNCConsole': {'type': 'novnc', 'undefined': 'foo'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + res = req.get_response(self.app) + self.assertEqual(400, res.status_int) + + def test_get_spice_console_with_undefined_param(self): + body = {'os-getSPICEConsole': {'type': 'spice-html5', + 'undefined': 'foo'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + res = req.get_response(self.app) + self.assertEqual(400, res.status_int) + + def test_get_rdp_console_with_undefined_param(self): + body = {'os-getRDPConsole': {'type': 'rdp-html5', 'undefined': 'foo'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + res = req.get_response(self.app) + self.assertEqual(400, res.status_int) + + +class ConsolesExtensionTestV2(ConsolesExtensionTestV21): + + def _setup_wsgi(self): + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Consoles']) + self.app = fakes.wsgi_app(init_only=('servers',)) + + def test_get_vnc_console_with_undefined_param(self): + pass + + def test_get_spice_console_with_undefined_param(self): + pass + + def test_get_rdp_console_with_undefined_param(self): + pass + + def test_get_serial_console(self): + body = {'os-getSerialConsole': {'type': 'serial'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + output = jsonutils.loads(res.body) + self.assertEqual(200, res.status_int) + self.assertEqual({u'console': {u'url': u'http://fake', + u'type': u'serial'}}, + output) + + @mock.patch.object(compute_api.API, 'get_serial_console') + def test_get_serial_console_not_enable(self, get_serial_console): + get_serial_console.side_effect = exception.ConsoleTypeUnavailable( + console_type="serial") + + body = {'os-getSerialConsole': {'type': 'serial'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + self.assertTrue(get_serial_console.called) + + @mock.patch.object(compute_api.API, 'get_serial_console') + def test_get_serial_console_invalid_type(self, get_serial_console): + get_serial_console.side_effect = ( + exception.ConsoleTypeInvalid(console_type='invalid')) + + body = {'os-getSerialConsole': {'type': 'invalid'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + self.assertTrue(get_serial_console.called) + + @mock.patch.object(compute_api.API, 'get_serial_console') + def test_get_serial_console_no_type(self, get_serial_console): + get_serial_console.side_effect = ( + exception.ConsoleTypeInvalid(console_type='')) + + body = {'os-getSerialConsole': {}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + self.assertTrue(get_serial_console.called) + + @mock.patch.object(compute_api.API, 'get_serial_console') + def test_get_serial_console_no_instance(self, get_serial_console): + get_serial_console.side_effect = ( + exception.InstanceNotFound(instance_id='xxx')) + + body = {'os-getSerialConsole': {'type': 'serial'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + self.assertTrue(get_serial_console.called) + + @mock.patch.object(compute_api.API, 'get_serial_console') + def test_get_serial_console_instance_not_ready(self, get_serial_console): + get_serial_console.side_effect = ( + exception.InstanceNotReady(instance_id='xxx')) + + body = {'os-getSerialConsole': {'type': 'serial'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 409) + self.assertTrue(get_serial_console.called) + + @mock.patch.object(compute_api.API, 'get_serial_console') + def test_get_serial_console_socket_exhausted(self, get_serial_console): + get_serial_console.side_effect = ( + exception.SocketPortRangeExhaustedException( + host='127.0.0.1')) + + body = {'os-getSerialConsole': {'type': 'serial'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 500) + self.assertTrue(get_serial_console.called) + + @mock.patch.object(compute_api.API, 'get_serial_console') + def test_get_serial_console_image_nport_invalid(self, get_serial_console): + get_serial_console.side_effect = ( + exception.ImageSerialPortNumberInvalid( + num_ports='x', property="hw_serial_port_count")) + + body = {'os-getSerialConsole': {'type': 'serial'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + self.assertTrue(get_serial_console.called) + + @mock.patch.object(compute_api.API, 'get_serial_console') + def test_get_serial_console_image_nport_exceed(self, get_serial_console): + get_serial_console.side_effect = ( + exception.ImageSerialPortNumberExceedFlavorValue()) + + body = {'os-getSerialConsole': {'type': 'serial'}} + req = webob.Request.blank(self.url) + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + self.assertTrue(get_serial_console.called) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_createserverext.py b/nova/tests/unit/api/openstack/compute/contrib/test_createserverext.py new file mode 100644 index 0000000000..eca3aa3953 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_createserverext.py @@ -0,0 +1,387 @@ +# Copyright 2010-2011 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 base64 +from xml.dom import minidom + +from oslo.serialization import jsonutils +import webob + +from nova.compute import api as compute_api +from nova import db +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + +FAKE_UUID = fakes.FAKE_UUID + +FAKE_NETWORKS = [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12'), + ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '10.0.2.12')] + +DUPLICATE_NETWORKS = [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12'), + ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12')] + +INVALID_NETWORKS = [('invalid', 'invalid-ip-address')] + + +def return_security_group_non_existing(context, project_id, group_name): + raise exception.SecurityGroupNotFoundForProject(project_id=project_id, + security_group_id=group_name) + + +def return_security_group_get_by_name(context, project_id, group_name): + return {'id': 1, 'name': group_name} + + +def return_security_group_get(context, security_group_id, session): + return {'id': security_group_id} + + +def return_instance_add_security_group(context, instance_id, + security_group_id): + pass + + +class CreateserverextTest(test.TestCase): + def setUp(self): + super(CreateserverextTest, self).setUp() + + self.security_group = None + self.injected_files = None + self.networks = None + self.user_data = None + + def create(*args, **kwargs): + if 'security_group' in kwargs: + self.security_group = kwargs['security_group'] + else: + self.security_group = None + if 'injected_files' in kwargs: + self.injected_files = kwargs['injected_files'] + else: + self.injected_files = None + + if 'requested_networks' in kwargs: + self.networks = kwargs['requested_networks'] + else: + self.networks = None + + if 'user_data' in kwargs: + self.user_data = kwargs['user_data'] + + resv_id = None + + return ([{'id': '1234', 'display_name': 'fakeinstance', + 'uuid': FAKE_UUID, + 'user_id': 'fake', + 'project_id': 'fake', + 'created_at': "", + 'updated_at': "", + 'fixed_ips': [], + 'progress': 0}], resv_id) + + self.stubs.Set(compute_api.API, 'create', create) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Createserverext', 'User_data', + 'Security_groups', 'Os_networks']) + + def _make_stub_method(self, canned_return): + def stub_method(*args, **kwargs): + return canned_return + return stub_method + + def _create_security_group_request_dict(self, security_groups): + server = {} + server['name'] = 'new-server-test' + server['imageRef'] = 'cedef40a-ed67-4d10-800e-17455edce175' + server['flavorRef'] = 1 + if security_groups is not None: + sg_list = [] + for name in security_groups: + sg_list.append({'name': name}) + server['security_groups'] = sg_list + return {'server': server} + + def _create_networks_request_dict(self, networks): + server = {} + server['name'] = 'new-server-test' + server['imageRef'] = 'cedef40a-ed67-4d10-800e-17455edce175' + server['flavorRef'] = 1 + if networks is not None: + network_list = [] + for uuid, fixed_ip in networks: + network_list.append({'uuid': uuid, 'fixed_ip': fixed_ip}) + server['networks'] = network_list + return {'server': server} + + def _create_user_data_request_dict(self, user_data): + server = {} + server['name'] = 'new-server-test' + server['imageRef'] = 'cedef40a-ed67-4d10-800e-17455edce175' + server['flavorRef'] = 1 + server['user_data'] = user_data + return {'server': server} + + def _get_create_request_json(self, body_dict): + req = webob.Request.blank('/v2/fake/os-create-server-ext') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = jsonutils.dumps(body_dict) + return req + + def _format_xml_request_body(self, body_dict): + server = body_dict['server'] + body_parts = [] + body_parts.extend([ + '<?xml version="1.0" encoding="UTF-8"?>', + '<server xmlns="http://docs.rackspacecloud.com/servers/api/v1.1"', + ' name="%s" imageRef="%s" flavorRef="%s">' % ( + server['name'], server['imageRef'], server['flavorRef'])]) + if 'metadata' in server: + metadata = server['metadata'] + body_parts.append('<metadata>') + for item in metadata.iteritems(): + body_parts.append('<meta key="%s">%s</meta>' % item) + body_parts.append('</metadata>') + if 'personality' in server: + personalities = server['personality'] + body_parts.append('<personality>') + for file in personalities: + item = (file['path'], file['contents']) + body_parts.append('<file path="%s">%s</file>' % item) + body_parts.append('</personality>') + if 'networks' in server: + networks = server['networks'] + body_parts.append('<networks>') + for network in networks: + item = (network['uuid'], network['fixed_ip']) + body_parts.append('<network uuid="%s" fixed_ip="%s"></network>' + % item) + body_parts.append('</networks>') + body_parts.append('</server>') + return ''.join(body_parts) + + def _get_create_request_xml(self, body_dict): + req = webob.Request.blank('/v2/fake/os-create-server-ext') + req.content_type = 'application/xml' + req.accept = 'application/xml' + req.method = 'POST' + req.body = self._format_xml_request_body(body_dict) + return req + + def _create_instance_with_networks_json(self, networks): + body_dict = self._create_networks_request_dict(networks) + request = self._get_create_request_json(body_dict) + response = request.get_response(fakes.wsgi_app( + init_only=('servers', 'os-create-server-ext'))) + return request, response, self.networks + + def _create_instance_with_user_data_json(self, networks): + body_dict = self._create_user_data_request_dict(networks) + request = self._get_create_request_json(body_dict) + response = request.get_response(fakes.wsgi_app( + init_only=('servers', 'os-create-server-ext'))) + return request, response, self.user_data + + def _create_instance_with_networks_xml(self, networks): + body_dict = self._create_networks_request_dict(networks) + request = self._get_create_request_xml(body_dict) + response = request.get_response(fakes.wsgi_app( + init_only=('servers', 'os-create-server-ext'))) + return request, response, self.networks + + def test_create_instance_with_no_networks(self): + _create_inst = self._create_instance_with_networks_json + request, response, networks = _create_inst(networks=None) + self.assertEqual(response.status_int, 202) + self.assertIsNone(networks) + + def test_create_instance_with_no_networks_xml(self): + _create_inst = self._create_instance_with_networks_xml + request, response, networks = _create_inst(networks=None) + self.assertEqual(response.status_int, 202) + self.assertIsNone(networks) + + def test_create_instance_with_one_network(self): + _create_inst = self._create_instance_with_networks_json + request, response, networks = _create_inst([FAKE_NETWORKS[0]]) + self.assertEqual(response.status_int, 202) + self.assertEqual([FAKE_NETWORKS[0]], networks.as_tuples()) + + def test_create_instance_with_one_network_xml(self): + _create_inst = self._create_instance_with_networks_xml + request, response, networks = _create_inst([FAKE_NETWORKS[0]]) + self.assertEqual(response.status_int, 202) + self.assertEqual([FAKE_NETWORKS[0]], networks.as_tuples()) + + def test_create_instance_with_two_networks(self): + _create_inst = self._create_instance_with_networks_json + request, response, networks = _create_inst(FAKE_NETWORKS) + self.assertEqual(response.status_int, 202) + self.assertEqual(FAKE_NETWORKS, networks.as_tuples()) + + def test_create_instance_with_two_networks_xml(self): + _create_inst = self._create_instance_with_networks_xml + request, response, networks = _create_inst(FAKE_NETWORKS) + self.assertEqual(response.status_int, 202) + self.assertEqual(FAKE_NETWORKS, networks.as_tuples()) + + def test_create_instance_with_duplicate_networks(self): + _create_inst = self._create_instance_with_networks_json + request, response, networks = _create_inst(DUPLICATE_NETWORKS) + self.assertEqual(response.status_int, 400) + self.assertIsNone(networks) + + def test_create_instance_with_duplicate_networks_xml(self): + _create_inst = self._create_instance_with_networks_xml + request, response, networks = _create_inst(DUPLICATE_NETWORKS) + self.assertEqual(response.status_int, 400) + self.assertIsNone(networks) + + def test_create_instance_with_network_no_id(self): + body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]]) + del body_dict['server']['networks'][0]['uuid'] + request = self._get_create_request_json(body_dict) + response = request.get_response(fakes.wsgi_app( + init_only=('servers', 'os-create-server-ext'))) + self.assertEqual(response.status_int, 400) + self.assertIsNone(self.networks) + + def test_create_instance_with_network_no_id_xml(self): + body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]]) + request = self._get_create_request_xml(body_dict) + uuid = ' uuid="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"' + request.body = request.body.replace(uuid, '') + response = request.get_response(fakes.wsgi_app( + init_only=('servers', 'os-create-server-ext'))) + self.assertEqual(response.status_int, 400) + self.assertIsNone(self.networks) + + def test_create_instance_with_network_invalid_id(self): + _create_inst = self._create_instance_with_networks_json + request, response, networks = _create_inst(INVALID_NETWORKS) + self.assertEqual(response.status_int, 400) + self.assertIsNone(networks) + + def test_create_instance_with_network_invalid_id_xml(self): + _create_inst = self._create_instance_with_networks_xml + request, response, networks = _create_inst(INVALID_NETWORKS) + self.assertEqual(response.status_int, 400) + self.assertIsNone(networks) + + def test_create_instance_with_network_empty_fixed_ip(self): + networks = [('1', '')] + _create_inst = self._create_instance_with_networks_json + request, response, networks = _create_inst(networks) + self.assertEqual(response.status_int, 400) + self.assertIsNone(networks) + + def test_create_instance_with_network_non_string_fixed_ip(self): + networks = [('1', 12345)] + _create_inst = self._create_instance_with_networks_json + request, response, networks = _create_inst(networks) + self.assertEqual(response.status_int, 400) + self.assertIsNone(networks) + + def test_create_instance_with_network_empty_fixed_ip_xml(self): + networks = [('1', '')] + _create_inst = self._create_instance_with_networks_xml + request, response, networks = _create_inst(networks) + self.assertEqual(response.status_int, 400) + self.assertIsNone(networks) + + def test_create_instance_with_network_no_fixed_ip(self): + body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]]) + del body_dict['server']['networks'][0]['fixed_ip'] + request = self._get_create_request_json(body_dict) + response = request.get_response(fakes.wsgi_app( + init_only=('servers', 'os-create-server-ext'))) + self.assertEqual(response.status_int, 202) + self.assertEqual([('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)], + self.networks.as_tuples()) + + def test_create_instance_with_network_no_fixed_ip_xml(self): + body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]]) + request = self._get_create_request_xml(body_dict) + request.body = request.body.replace(' fixed_ip="10.0.1.12"', '') + response = request.get_response(fakes.wsgi_app( + init_only=('servers', 'os-create-server-ext'))) + self.assertEqual(response.status_int, 202) + self.assertEqual([('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)], + self.networks.as_tuples()) + + def test_create_instance_with_userdata(self): + user_data_contents = '#!/bin/bash\necho "Oh no!"\n' + user_data_contents = base64.b64encode(user_data_contents) + _create_inst = self._create_instance_with_user_data_json + request, response, user_data = _create_inst(user_data_contents) + self.assertEqual(response.status_int, 202) + self.assertEqual(user_data, user_data_contents) + + def test_create_instance_with_userdata_none(self): + user_data_contents = None + _create_inst = self._create_instance_with_user_data_json + request, response, user_data = _create_inst(user_data_contents) + self.assertEqual(response.status_int, 202) + self.assertEqual(user_data, user_data_contents) + + def test_create_instance_with_userdata_with_non_b64_content(self): + user_data_contents = '#!/bin/bash\necho "Oh no!"\n' + _create_inst = self._create_instance_with_user_data_json + request, response, user_data = _create_inst(user_data_contents) + self.assertEqual(response.status_int, 400) + self.assertIsNone(user_data) + + def test_create_instance_with_security_group_json(self): + security_groups = ['test', 'test1'] + self.stubs.Set(db, 'security_group_get_by_name', + return_security_group_get_by_name) + self.stubs.Set(db, 'instance_add_security_group', + return_instance_add_security_group) + body_dict = self._create_security_group_request_dict(security_groups) + request = self._get_create_request_json(body_dict) + response = request.get_response(fakes.wsgi_app( + init_only=('servers', 'os-create-server-ext'))) + self.assertEqual(response.status_int, 202) + self.assertEqual(self.security_group, security_groups) + + def test_get_server_by_id_verify_security_groups_json(self): + self.stubs.Set(db, 'instance_get', fakes.fake_instance_get()) + self.stubs.Set(db, 'instance_get_by_uuid', fakes.fake_instance_get()) + req = webob.Request.blank('/v2/fake/os-create-server-ext/1') + req.headers['Content-Type'] = 'application/json' + response = req.get_response(fakes.wsgi_app( + init_only=('os-create-server-ext', 'servers'))) + self.assertEqual(response.status_int, 200) + res_dict = jsonutils.loads(response.body) + expected_security_group = [{"name": "test"}] + self.assertEqual(res_dict['server'].get('security_groups'), + expected_security_group) + + def test_get_server_by_id_verify_security_groups_xml(self): + self.stubs.Set(db, 'instance_get', fakes.fake_instance_get()) + self.stubs.Set(db, 'instance_get_by_uuid', fakes.fake_instance_get()) + req = webob.Request.blank('/v2/fake/os-create-server-ext/1') + req.headers['Accept'] = 'application/xml' + response = req.get_response(fakes.wsgi_app( + init_only=('os-create-server-ext', 'servers'))) + self.assertEqual(response.status_int, 200) + dom = minidom.parseString(response.body) + server = dom.childNodes[0] + sec_groups = server.getElementsByTagName('security_groups')[0] + sec_group = sec_groups.getElementsByTagName('security_group')[0] + self.assertEqual('test', sec_group.getAttribute("name")) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_deferred_delete.py b/nova/tests/unit/api/openstack/compute/contrib/test_deferred_delete.py new file mode 100644 index 0000000000..0dfd0e5339 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_deferred_delete.py @@ -0,0 +1,147 @@ +# Copyright 2011 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 mock +import webob + +from nova.api.openstack.compute.contrib import deferred_delete +from nova.api.openstack.compute.plugins.v3 import deferred_delete as dd_v21 +from nova.compute import api as compute_api +from nova import context +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + + +class FakeRequest(object): + def __init__(self, context): + self.environ = {'nova.context': context} + + +class DeferredDeleteExtensionTestV21(test.NoDBTestCase): + ext_ver = dd_v21.DeferredDeleteController + + def setUp(self): + super(DeferredDeleteExtensionTestV21, self).setUp() + self.fake_input_dict = {} + self.fake_uuid = 'fake_uuid' + self.fake_context = context.RequestContext('fake', 'fake') + self.fake_req = FakeRequest(self.fake_context) + self.extension = self.ext_ver() + + def test_force_delete(self): + self.mox.StubOutWithMock(compute_api.API, 'get') + self.mox.StubOutWithMock(compute_api.API, 'force_delete') + + fake_instance = 'fake_instance' + + compute_api.API.get(self.fake_context, self.fake_uuid, + expected_attrs=None, + want_objects=True).AndReturn(fake_instance) + compute_api.API.force_delete(self.fake_context, fake_instance) + + self.mox.ReplayAll() + res = self.extension._force_delete(self.fake_req, self.fake_uuid, + self.fake_input_dict) + # NOTE: on v2.1, http status code is set as wsgi_code of API + # method instead of status_int in a response object. + if isinstance(self.extension, dd_v21.DeferredDeleteController): + status_int = self.extension._force_delete.wsgi_code + else: + status_int = res.status_int + self.assertEqual(202, status_int) + + def test_force_delete_instance_not_found(self): + self.mox.StubOutWithMock(compute_api.API, 'get') + + compute_api.API.get(self.fake_context, self.fake_uuid, + expected_attrs=None, + want_objects=True).AndRaise( + exception.InstanceNotFound(instance_id='instance-0000')) + + self.mox.ReplayAll() + self.assertRaises(webob.exc.HTTPNotFound, + self.extension._force_delete, + self.fake_req, + self.fake_uuid, + self.fake_input_dict) + + @mock.patch.object(compute_api.API, 'get') + @mock.patch.object(compute_api.API, 'force_delete', + side_effect=exception.InstanceIsLocked( + instance_uuid='fake_uuid')) + def test_force_delete_instance_locked(self, mock_force_delete, mock_get): + req = fakes.HTTPRequest.blank('/v2/fake/servers/fake_uuid/action') + ex = self.assertRaises(webob.exc.HTTPConflict, + self.extension._force_delete, + req, 'fake_uuid', '') + self.assertIn('Instance fake_uuid is locked', ex.explanation) + + def test_restore(self): + self.mox.StubOutWithMock(compute_api.API, 'get') + self.mox.StubOutWithMock(compute_api.API, 'restore') + + fake_instance = 'fake_instance' + + compute_api.API.get(self.fake_context, self.fake_uuid, + expected_attrs=None, + want_objects=True).AndReturn(fake_instance) + compute_api.API.restore(self.fake_context, fake_instance) + + self.mox.ReplayAll() + res = self.extension._restore(self.fake_req, self.fake_uuid, + self.fake_input_dict) + # NOTE: on v2.1, http status code is set as wsgi_code of API + # method instead of status_int in a response object. + if isinstance(self.extension, dd_v21.DeferredDeleteController): + status_int = self.extension._restore.wsgi_code + else: + status_int = res.status_int + self.assertEqual(202, status_int) + + def test_restore_instance_not_found(self): + self.mox.StubOutWithMock(compute_api.API, 'get') + + compute_api.API.get(self.fake_context, self.fake_uuid, + expected_attrs=None, want_objects=True).AndRaise( + exception.InstanceNotFound(instance_id='instance-0000')) + + self.mox.ReplayAll() + self.assertRaises(webob.exc.HTTPNotFound, self.extension._restore, + self.fake_req, self.fake_uuid, + self.fake_input_dict) + + def test_restore_raises_conflict_on_invalid_state(self): + self.mox.StubOutWithMock(compute_api.API, 'get') + self.mox.StubOutWithMock(compute_api.API, 'restore') + + fake_instance = 'fake_instance' + exc = exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + + compute_api.API.get(self.fake_context, self.fake_uuid, + expected_attrs=None, + want_objects=True).AndReturn(fake_instance) + compute_api.API.restore(self.fake_context, fake_instance).AndRaise( + exc) + + self.mox.ReplayAll() + self.assertRaises(webob.exc.HTTPConflict, self.extension._restore, + self.fake_req, self.fake_uuid, self.fake_input_dict) + + +class DeferredDeleteExtensionTestV2(DeferredDeleteExtensionTestV21): + ext_ver = deferred_delete.DeferredDeleteController diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_disk_config.py b/nova/tests/unit/api/openstack/compute/contrib/test_disk_config.py new file mode 100644 index 0000000000..b9a514a451 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_disk_config.py @@ -0,0 +1,449 @@ +# Copyright 2011 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 datetime + +from oslo.serialization import jsonutils + +from nova.api.openstack import compute +from nova.compute import api as compute_api +from nova import db +from nova import objects +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance +import nova.tests.unit.image.fake + + +MANUAL_INSTANCE_UUID = fakes.FAKE_UUID +AUTO_INSTANCE_UUID = fakes.FAKE_UUID.replace('a', 'b') + +stub_instance = fakes.stub_instance + +API_DISK_CONFIG = 'OS-DCF:diskConfig' + + +def instance_addresses(context, instance_id): + return None + + +class DiskConfigTestCaseV21(test.TestCase): + + def setUp(self): + super(DiskConfigTestCaseV21, self).setUp() + self._set_up_app() + self._setup_fake_image_service() + + fakes.stub_out_nw_api(self.stubs) + + FAKE_INSTANCES = [ + fakes.stub_instance(1, + uuid=MANUAL_INSTANCE_UUID, + auto_disk_config=False), + fakes.stub_instance(2, + uuid=AUTO_INSTANCE_UUID, + auto_disk_config=True) + ] + + def fake_instance_get(context, id_): + for instance in FAKE_INSTANCES: + if id_ == instance['id']: + return instance + + self.stubs.Set(db, 'instance_get', fake_instance_get) + + def fake_instance_get_by_uuid(context, uuid, + columns_to_join=None, use_slave=False): + for instance in FAKE_INSTANCES: + if uuid == instance['uuid']: + return instance + + self.stubs.Set(db, 'instance_get_by_uuid', + fake_instance_get_by_uuid) + + def fake_instance_get_all(context, *args, **kwargs): + return FAKE_INSTANCES + + self.stubs.Set(db, 'instance_get_all', fake_instance_get_all) + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_instance_get_all) + + self.stubs.Set(objects.Instance, 'save', + lambda *args, **kwargs: None) + + def fake_rebuild(*args, **kwargs): + pass + + self.stubs.Set(compute_api.API, 'rebuild', fake_rebuild) + + def fake_instance_create(context, inst_, session=None): + inst = fake_instance.fake_db_instance(**{ + 'id': 1, + 'uuid': AUTO_INSTANCE_UUID, + 'created_at': datetime.datetime(2010, 10, 10, 12, 0, 0), + 'updated_at': datetime.datetime(2010, 10, 10, 12, 0, 0), + 'progress': 0, + 'name': 'instance-1', # this is a property + 'task_state': '', + 'vm_state': '', + 'auto_disk_config': inst_['auto_disk_config'], + 'security_groups': inst_['security_groups'], + }) + + def fake_instance_get_for_create(context, id_, *args, **kwargs): + return (inst, inst) + + self.stubs.Set(db, 'instance_update_and_get_original', + fake_instance_get_for_create) + + def fake_instance_get_all_for_create(context, *args, **kwargs): + return [inst] + self.stubs.Set(db, 'instance_get_all', + fake_instance_get_all_for_create) + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_instance_get_all_for_create) + + def fake_instance_add_security_group(context, instance_id, + security_group_id): + pass + + self.stubs.Set(db, + 'instance_add_security_group', + fake_instance_add_security_group) + + return inst + + self.stubs.Set(db, 'instance_create', fake_instance_create) + + def _set_up_app(self): + self.app = compute.APIRouterV21(init_only=('servers', 'images', + 'os-disk-config')) + + def _get_expected_msg_for_invalid_disk_config(self): + return ('{{"badRequest": {{"message": "Invalid input for' + ' field/attribute {0}. Value: {1}. u\'{1}\' is' + ' not one of [\'AUTO\', \'MANUAL\']", "code": 400}}}}') + + def _setup_fake_image_service(self): + self.image_service = nova.tests.unit.image.fake.stub_out_image_service( + self.stubs) + timestamp = datetime.datetime(2011, 1, 1, 1, 2, 3) + image = {'id': '88580842-f50a-11e2-8d3a-f23c91aec05e', + 'name': 'fakeimage7', + 'created_at': timestamp, + 'updated_at': timestamp, + 'deleted_at': None, + 'deleted': False, + 'status': 'active', + 'is_public': False, + 'container_format': 'ova', + 'disk_format': 'vhd', + 'size': '74185822', + 'properties': {'auto_disk_config': 'Disabled'}} + self.image_service.create(None, image) + + def tearDown(self): + super(DiskConfigTestCaseV21, self).tearDown() + nova.tests.unit.image.fake.FakeImageService_reset() + + def assertDiskConfig(self, dict_, value): + self.assertIn(API_DISK_CONFIG, dict_) + self.assertEqual(dict_[API_DISK_CONFIG], value) + + def test_show_server(self): + req = fakes.HTTPRequest.blank( + '/fake/servers/%s' % MANUAL_INSTANCE_UUID) + res = req.get_response(self.app) + server_dict = jsonutils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, 'MANUAL') + + req = fakes.HTTPRequest.blank( + '/fake/servers/%s' % AUTO_INSTANCE_UUID) + res = req.get_response(self.app) + server_dict = jsonutils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, 'AUTO') + + def test_detail_servers(self): + req = fakes.HTTPRequest.blank('/fake/servers/detail') + res = req.get_response(self.app) + server_dicts = jsonutils.loads(res.body)['servers'] + + expectations = ['MANUAL', 'AUTO'] + for server_dict, expected in zip(server_dicts, expectations): + self.assertDiskConfig(server_dict, expected) + + def test_show_image(self): + req = fakes.HTTPRequest.blank( + '/fake/images/a440c04b-79fa-479c-bed1-0b816eaec379') + res = req.get_response(self.app) + image_dict = jsonutils.loads(res.body)['image'] + self.assertDiskConfig(image_dict, 'MANUAL') + + req = fakes.HTTPRequest.blank( + '/fake/images/70a599e0-31e7-49b7-b260-868f441e862b') + res = req.get_response(self.app) + image_dict = jsonutils.loads(res.body)['image'] + self.assertDiskConfig(image_dict, 'AUTO') + + def test_detail_image(self): + req = fakes.HTTPRequest.blank('/fake/images/detail') + res = req.get_response(self.app) + image_dicts = jsonutils.loads(res.body)['images'] + + expectations = ['MANUAL', 'AUTO'] + for image_dict, expected in zip(image_dicts, expectations): + # NOTE(sirp): image fixtures 6 and 7 are setup for + # auto_disk_config testing + if image_dict['id'] in (6, 7): + self.assertDiskConfig(image_dict, expected) + + def test_create_server_override_auto(self): + req = fakes.HTTPRequest.blank('/fake/servers') + req.method = 'POST' + req.content_type = 'application/json' + body = {'server': { + 'name': 'server_test', + 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175', + 'flavorRef': '1', + API_DISK_CONFIG: 'AUTO' + }} + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + server_dict = jsonutils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, 'AUTO') + + def test_create_server_override_manual(self): + req = fakes.HTTPRequest.blank('/fake/servers') + req.method = 'POST' + req.content_type = 'application/json' + body = {'server': { + 'name': 'server_test', + 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175', + 'flavorRef': '1', + API_DISK_CONFIG: 'MANUAL' + }} + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + server_dict = jsonutils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, 'MANUAL') + + def test_create_server_detect_from_image(self): + """If user doesn't pass in diskConfig for server, use image metadata + to specify AUTO or MANUAL. + """ + req = fakes.HTTPRequest.blank('/fake/servers') + req.method = 'POST' + req.content_type = 'application/json' + body = {'server': { + 'name': 'server_test', + 'imageRef': 'a440c04b-79fa-479c-bed1-0b816eaec379', + 'flavorRef': '1', + }} + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + server_dict = jsonutils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, 'MANUAL') + + req = fakes.HTTPRequest.blank('/fake/servers') + req.method = 'POST' + req.content_type = 'application/json' + body = {'server': { + 'name': 'server_test', + 'imageRef': '70a599e0-31e7-49b7-b260-868f441e862b', + 'flavorRef': '1', + }} + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + server_dict = jsonutils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, 'AUTO') + + def test_create_server_detect_from_image_disabled_goes_to_manual(self): + req = fakes.HTTPRequest.blank('/fake/servers') + req.method = 'POST' + req.content_type = 'application/json' + body = {'server': { + 'name': 'server_test', + 'imageRef': '88580842-f50a-11e2-8d3a-f23c91aec05e', + 'flavorRef': '1', + }} + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + server_dict = jsonutils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, 'MANUAL') + + def test_create_server_errors_when_disabled_and_auto(self): + req = fakes.HTTPRequest.blank('/fake/servers') + req.method = 'POST' + req.content_type = 'application/json' + body = {'server': { + 'name': 'server_test', + 'imageRef': '88580842-f50a-11e2-8d3a-f23c91aec05e', + 'flavorRef': '1', + API_DISK_CONFIG: 'AUTO' + }} + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + + def test_create_server_when_disabled_and_manual(self): + req = fakes.HTTPRequest.blank('/fake/servers') + req.method = 'POST' + req.content_type = 'application/json' + body = {'server': { + 'name': 'server_test', + 'imageRef': '88580842-f50a-11e2-8d3a-f23c91aec05e', + 'flavorRef': '1', + API_DISK_CONFIG: 'MANUAL' + }} + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + server_dict = jsonutils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, 'MANUAL') + + def _test_update_server_disk_config(self, uuid, disk_config): + req = fakes.HTTPRequest.blank( + '/fake/servers/%s' % uuid) + req.method = 'PUT' + req.content_type = 'application/json' + body = {'server': {API_DISK_CONFIG: disk_config}} + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + server_dict = jsonutils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, disk_config) + + def test_update_server_override_auto(self): + self._test_update_server_disk_config(AUTO_INSTANCE_UUID, 'AUTO') + + def test_update_server_override_manual(self): + self._test_update_server_disk_config(MANUAL_INSTANCE_UUID, 'MANUAL') + + def test_update_server_invalid_disk_config(self): + # Return BadRequest if user passes an invalid diskConfig value. + req = fakes.HTTPRequest.blank( + '/fake/servers/%s' % MANUAL_INSTANCE_UUID) + req.method = 'PUT' + req.content_type = 'application/json' + body = {'server': {API_DISK_CONFIG: 'server_test'}} + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + expected_msg = self._get_expected_msg_for_invalid_disk_config() + self.assertEqual(expected_msg.format(API_DISK_CONFIG, 'server_test'), + res.body) + + def _test_rebuild_server_disk_config(self, uuid, disk_config): + req = fakes.HTTPRequest.blank( + '/fake/servers/%s/action' % uuid) + req.method = 'POST' + req.content_type = 'application/json' + body = {"rebuild": { + 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175', + API_DISK_CONFIG: disk_config + }} + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + server_dict = jsonutils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, disk_config) + + def test_rebuild_server_override_auto(self): + self._test_rebuild_server_disk_config(AUTO_INSTANCE_UUID, 'AUTO') + + def test_rebuild_server_override_manual(self): + self._test_rebuild_server_disk_config(MANUAL_INSTANCE_UUID, 'MANUAL') + + def test_create_server_with_auto_disk_config(self): + req = fakes.HTTPRequest.blank('/fake/servers') + req.method = 'POST' + req.content_type = 'application/json' + body = {'server': { + 'name': 'server_test', + 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175', + 'flavorRef': '1', + API_DISK_CONFIG: 'AUTO' + }} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertIn('auto_disk_config', kwargs) + self.assertEqual(True, kwargs['auto_disk_config']) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + server_dict = jsonutils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, 'AUTO') + + def test_rebuild_server_with_auto_disk_config(self): + req = fakes.HTTPRequest.blank( + '/fake/servers/%s/action' % AUTO_INSTANCE_UUID) + req.method = 'POST' + req.content_type = 'application/json' + body = {"rebuild": { + 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175', + API_DISK_CONFIG: 'AUTO' + }} + + def rebuild(*args, **kwargs): + self.assertIn('auto_disk_config', kwargs) + self.assertEqual(True, kwargs['auto_disk_config']) + + self.stubs.Set(compute_api.API, 'rebuild', rebuild) + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + server_dict = jsonutils.loads(res.body)['server'] + self.assertDiskConfig(server_dict, 'AUTO') + + def test_resize_server_with_auto_disk_config(self): + req = fakes.HTTPRequest.blank( + '/fake/servers/%s/action' % AUTO_INSTANCE_UUID) + req.method = 'POST' + req.content_type = 'application/json' + body = {"resize": { + "flavorRef": "3", + API_DISK_CONFIG: 'AUTO' + }} + + def resize(*args, **kwargs): + self.assertIn('auto_disk_config', kwargs) + self.assertEqual(True, kwargs['auto_disk_config']) + + self.stubs.Set(compute_api.API, 'resize', resize) + + req.body = jsonutils.dumps(body) + req.get_response(self.app) + + +class DiskConfigTestCaseV2(DiskConfigTestCaseV21): + def _set_up_app(self): + self.flags(verbose=True, + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Disk_config']) + + self.app = compute.APIRouter(init_only=('servers', 'images')) + + def _get_expected_msg_for_invalid_disk_config(self): + return ('{{"badRequest": {{"message": "{0} must be either' + ' \'MANUAL\' or \'AUTO\'.", "code": 400}}}}') diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_evacuate.py b/nova/tests/unit/api/openstack/compute/contrib/test_evacuate.py new file mode 100644 index 0000000000..3f5b662db5 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_evacuate.py @@ -0,0 +1,268 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 uuid + +from oslo.config import cfg +from oslo.serialization import jsonutils +import webob + +from nova.compute import api as compute_api +from nova.compute import vm_states +from nova import context +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + + +CONF = cfg.CONF +CONF.import_opt('password_length', 'nova.utils') + + +def fake_compute_api(*args, **kwargs): + return True + + +def fake_compute_api_get(self, context, instance_id, want_objects=False, + **kwargs): + # BAD_UUID is something that does not exist + if instance_id == 'BAD_UUID': + raise exception.InstanceNotFound(instance_id=instance_id) + else: + return fake_instance.fake_instance_obj(context, id=1, uuid=instance_id, + task_state=None, host='host1', + vm_state=vm_states.ACTIVE) + + +def fake_service_get_by_compute_host(self, context, host): + if host == 'bad-host': + raise exception.ComputeHostNotFound(host=host) + else: + return { + 'host_name': host, + 'service': 'compute', + 'zone': 'nova' + } + + +class EvacuateTestV21(test.NoDBTestCase): + + _methods = ('resize', 'evacuate') + fake_url = '/v2/fake' + + def setUp(self): + super(EvacuateTestV21, self).setUp() + self.stubs.Set(compute_api.API, 'get', fake_compute_api_get) + self.stubs.Set(compute_api.HostAPI, 'service_get_by_compute_host', + fake_service_get_by_compute_host) + self.UUID = uuid.uuid4() + for _method in self._methods: + self.stubs.Set(compute_api.API, _method, fake_compute_api) + + def _fake_wsgi_app(self, ctxt): + return fakes.wsgi_app_v21(fake_auth_context=ctxt) + + def _gen_resource_with_app(self, json_load, is_admin=True, uuid=None): + ctxt = context.get_admin_context() + ctxt.user_id = 'fake' + ctxt.project_id = 'fake' + ctxt.is_admin = is_admin + app = self._fake_wsgi_app(ctxt) + req = webob.Request.blank('%s/servers/%s/action' % (self.fake_url, + uuid or self.UUID)) + req.method = 'POST' + base_json_load = {'evacuate': json_load} + req.body = jsonutils.dumps(base_json_load) + req.content_type = 'application/json' + + return req.get_response(app) + + def _fake_update(self, inst, context, instance, task_state, + expected_task_state): + return None + + def test_evacuate_with_valid_instance(self): + res = self._gen_resource_with_app({'host': 'my-host', + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + + self.assertEqual(res.status_int, 200) + + def test_evacuate_with_invalid_instance(self): + res = self._gen_resource_with_app({'host': 'my-host', + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}, + uuid='BAD_UUID') + + self.assertEqual(res.status_int, 404) + + def test_evacuate_with_active_service(self): + def fake_evacuate(*args, **kwargs): + raise exception.ComputeServiceInUse("Service still in use") + + self.stubs.Set(compute_api.API, 'evacuate', fake_evacuate) + res = self._gen_resource_with_app({'host': 'my-host', + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(res.status_int, 400) + + def test_evacuate_instance_with_no_target(self): + res = self._gen_resource_with_app({'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(200, res.status_int) + + def test_evacuate_instance_without_on_shared_storage(self): + res = self._gen_resource_with_app({'host': 'my-host', + 'adminPass': 'MyNewPass'}) + self.assertEqual(res.status_int, 400) + + def test_evacuate_instance_with_invalid_characters_host(self): + host = 'abc!#' + res = self._gen_resource_with_app({'host': host, + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(400, res.status_int) + + def test_evacuate_instance_with_too_long_host(self): + host = 'a' * 256 + res = self._gen_resource_with_app({'host': host, + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(400, res.status_int) + + def test_evacuate_instance_with_invalid_on_shared_storage(self): + res = self._gen_resource_with_app({'host': 'my-host', + 'onSharedStorage': 'foo', + 'adminPass': 'MyNewPass'}) + self.assertEqual(400, res.status_int) + + def test_evacuate_instance_with_bad_target(self): + res = self._gen_resource_with_app({'host': 'bad-host', + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(res.status_int, 404) + + def test_evacuate_instance_with_target(self): + self.stubs.Set(compute_api.API, 'update', self._fake_update) + + res = self._gen_resource_with_app({'host': 'my-host', + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(res.status_int, 200) + resp_json = jsonutils.loads(res.body) + self.assertEqual("MyNewPass", resp_json['adminPass']) + + def test_evacuate_shared_and_pass(self): + self.stubs.Set(compute_api.API, 'update', self._fake_update) + res = self._gen_resource_with_app({'host': 'my-host', + 'onSharedStorage': 'True', + 'adminPass': 'MyNewPass'}) + self.assertEqual(res.status_int, 400) + + def test_evacuate_not_shared_pass_generated(self): + self.stubs.Set(compute_api.API, 'update', self._fake_update) + res = self._gen_resource_with_app({'host': 'my-host', + 'onSharedStorage': 'False'}) + self.assertEqual(res.status_int, 200) + resp_json = jsonutils.loads(res.body) + self.assertEqual(CONF.password_length, len(resp_json['adminPass'])) + + def test_evacuate_shared(self): + self.stubs.Set(compute_api.API, 'update', self._fake_update) + res = self._gen_resource_with_app({'host': 'my-host', + 'onSharedStorage': 'True'}) + self.assertEqual(res.status_int, 200) + + def test_not_admin(self): + res = self._gen_resource_with_app({'host': 'my-host', + 'onSharedStorage': 'True'}, + is_admin=False) + self.assertEqual(res.status_int, 403) + + def test_evacuate_to_same_host(self): + res = self._gen_resource_with_app({'host': 'host1', + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(res.status_int, 400) + + def test_evacuate_instance_with_empty_host(self): + res = self._gen_resource_with_app({'host': '', + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(400, res.status_int) + + def test_evacuate_instance_with_underscore_in_hostname(self): + # NOTE: The hostname grammar in RFC952 does not allow for + # underscores in hostnames. However, we should test that it + # is supported because it sometimes occurs in real systems. + self.stubs.Set(compute_api.API, 'update', self._fake_update) + res = self._gen_resource_with_app({'host': 'underscore_hostname', + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(200, res.status_int) + resp_json = jsonutils.loads(res.body) + self.assertEqual("MyNewPass", resp_json['adminPass']) + + def test_evacuate_disable_password_return(self): + self._test_evacuate_enable_instance_password_conf(False) + + def test_evacuate_enable_password_return(self): + self._test_evacuate_enable_instance_password_conf(True) + + def _test_evacuate_enable_instance_password_conf(self, enable_pass): + self.flags(enable_instance_password=enable_pass) + self.stubs.Set(compute_api.API, 'update', self._fake_update) + + res = self._gen_resource_with_app({'host': 'my_host', + 'onSharedStorage': 'False'}) + self.assertEqual(res.status_int, 200) + resp_json = jsonutils.loads(res.body) + if enable_pass: + self.assertIn('adminPass', resp_json) + else: + self.assertIsNone(resp_json.get('adminPass')) + + +class EvacuateTestV2(EvacuateTestV21): + + def setUp(self): + super(EvacuateTestV2, self).setUp() + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Evacuate']) + + def _fake_wsgi_app(self, ctxt): + return fakes.wsgi_app(fake_auth_context=ctxt) + + def test_evacuate_instance_with_no_target(self): + res = self._gen_resource_with_app({'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(400, res.status_int) + + def test_evacuate_instance_with_too_long_host(self): + pass + + def test_evacuate_instance_with_invalid_characters_host(self): + pass + + def test_evacuate_instance_with_invalid_on_shared_storage(self): + pass + + def test_evacuate_disable_password_return(self): + pass + + def test_evacuate_enable_password_return(self): + pass diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_availability_zone.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_availability_zone.py new file mode 100644 index 0000000000..a3e6dd4a78 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_availability_zone.py @@ -0,0 +1,184 @@ +# Copyright 2011 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import extended_availability_zone +from nova import availability_zones +from nova import compute +from nova.compute import vm_states +from nova import db +from nova import exception +from nova import objects +from nova.objects import instance as instance_obj +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + +UUID1 = '00000000-0000-0000-0000-000000000001' +UUID2 = '00000000-0000-0000-0000-000000000002' +UUID3 = '00000000-0000-0000-0000-000000000003' + + +def fake_compute_get_az(*args, **kwargs): + inst = fakes.stub_instance(1, uuid=UUID3, host="get-host", + vm_state=vm_states.ACTIVE, + availability_zone='fakeaz') + return fake_instance.fake_instance_obj(args[1], **inst) + + +def fake_compute_get_empty(*args, **kwargs): + inst = fakes.stub_instance(1, uuid=UUID3, host="", + vm_state=vm_states.ACTIVE, + availability_zone='fakeaz') + return fake_instance.fake_instance_obj(args[1], **inst) + + +def fake_compute_get(*args, **kwargs): + inst = fakes.stub_instance(1, uuid=UUID3, host="get-host", + vm_state=vm_states.ACTIVE) + return fake_instance.fake_instance_obj(args[1], **inst) + + +def fake_compute_get_all(*args, **kwargs): + inst1 = fakes.stub_instance(1, uuid=UUID1, host="all-host", + vm_state=vm_states.ACTIVE) + inst2 = fakes.stub_instance(2, uuid=UUID2, host="all-host", + vm_state=vm_states.ACTIVE) + db_list = [inst1, inst2] + fields = instance_obj.INSTANCE_DEFAULT_FIELDS + return instance_obj._make_instance_list(args[1], + objects.InstanceList(), + db_list, fields) + + +def fake_get_host_availability_zone(context, host): + return host + + +def fake_get_no_host_availability_zone(context, host): + return None + + +class ExtendedAvailabilityZoneTestV21(test.TestCase): + content_type = 'application/json' + prefix = 'OS-EXT-AZ:' + base_url = '/v2/fake/servers/' + + def setUp(self): + super(ExtendedAvailabilityZoneTestV21, self).setUp() + availability_zones.reset_cache() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all) + self.stubs.Set(availability_zones, 'get_host_availability_zone', + fake_get_host_availability_zone) + return_server = fakes.fake_instance_get() + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app_v21(init_only=None)) + return res + + def _get_server(self, body): + return jsonutils.loads(body).get('server') + + def _get_servers(self, body): + return jsonutils.loads(body).get('servers') + + def assertAvailabilityZone(self, server, az): + self.assertEqual(server.get('%savailability_zone' % self.prefix), + az) + + def test_show_no_host_az(self): + self.stubs.Set(compute.api.API, 'get', fake_compute_get_az) + self.stubs.Set(availability_zones, 'get_host_availability_zone', + fake_get_no_host_availability_zone) + + url = self.base_url + UUID3 + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + self.assertAvailabilityZone(self._get_server(res.body), 'fakeaz') + + def test_show_empty_host_az(self): + self.stubs.Set(compute.api.API, 'get', fake_compute_get_empty) + self.stubs.Set(availability_zones, 'get_host_availability_zone', + fake_get_no_host_availability_zone) + + url = self.base_url + UUID3 + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + self.assertAvailabilityZone(self._get_server(res.body), 'fakeaz') + + def test_show(self): + url = self.base_url + UUID3 + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + self.assertAvailabilityZone(self._get_server(res.body), 'get-host') + + def test_detail(self): + url = self.base_url + 'detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + for i, server in enumerate(self._get_servers(res.body)): + self.assertAvailabilityZone(server, 'all-host') + + def test_no_instance_passthrough_404(self): + + def fake_compute_get(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + url = self.base_url + '70f6db34-de8d-4fbd-aafb-4065bdfa6115' + res = self._make_request(url) + + self.assertEqual(res.status_int, 404) + + +class ExtendedAvailabilityZoneTestV2(ExtendedAvailabilityZoneTestV21): + + def setUp(self): + super(ExtendedAvailabilityZoneTestV2, self).setUp() + + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Extended_availability_zone']) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app(init_only=('servers',))) + return res + + +class ExtendedAvailabilityZoneXmlTestV2(ExtendedAvailabilityZoneTestV2): + content_type = 'application/xml' + prefix = '{%s}' % extended_availability_zone.\ + Extended_availability_zone.namespace + + def _get_server(self, body): + return etree.XML(body) + + def _get_servers(self, body): + return etree.XML(body).getchildren() diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_evacuate_find_host.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_evacuate_find_host.py new file mode 100644 index 0000000000..1aaee6837a --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_evacuate_find_host.py @@ -0,0 +1,114 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 uuid + +import mock +from oslo.serialization import jsonutils +import webob + +from nova.compute import vm_states +from nova import context +from nova.objects import instance as instance_obj +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + + +class ExtendedEvacuateFindHostTest(test.NoDBTestCase): + + def setUp(self): + super(ExtendedEvacuateFindHostTest, self).setUp() + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Extended_evacuate_find_host', + 'Evacuate']) + self.UUID = uuid.uuid4() + + def _get_admin_context(self, user_id='fake', project_id='fake'): + ctxt = context.get_admin_context() + ctxt.user_id = user_id + ctxt.project_id = project_id + return ctxt + + def _fake_compute_api(*args, **kwargs): + return True + + def _fake_compute_api_get(self, context, instance_id, **kwargs): + instance = fake_instance.fake_db_instance(id=1, uuid=uuid, + task_state=None, + host='host1', + vm_state=vm_states.ACTIVE) + instance = instance_obj.Instance._from_db_object(context, + instance_obj.Instance(), + instance) + return instance + + def _fake_service_get_by_compute_host(self, context, host): + return {'host_name': host, + 'service': 'compute', + 'zone': 'nova' + } + + @mock.patch('nova.compute.api.HostAPI.service_get_by_compute_host') + @mock.patch('nova.compute.api.API.get') + @mock.patch('nova.compute.api.API.evacuate') + def test_evacuate_instance_with_no_target(self, evacuate_mock, + api_get_mock, + service_get_mock): + service_get_mock.side_effects = self._fake_service_get_by_compute_host + api_get_mock.side_effects = self._fake_compute_api_get + evacuate_mock.side_effects = self._fake_compute_api + + ctxt = self._get_admin_context() + app = fakes.wsgi_app(fake_auth_context=ctxt) + req = webob.Request.blank('/v2/fake/servers/%s/action' % self.UUID) + req.method = 'POST' + req.body = jsonutils.dumps({ + 'evacuate': { + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass' + } + }) + req.content_type = 'application/json' + res = req.get_response(app) + self.assertEqual(200, res.status_int) + evacuate_mock.assert_called_once_with(mock.ANY, mock.ANY, None, + mock.ANY, mock.ANY) + + @mock.patch('nova.compute.api.HostAPI.service_get_by_compute_host') + @mock.patch('nova.compute.api.API.get') + def test_no_target_fails_if_extension_not_loaded(self, api_get_mock, + service_get_mock): + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Evacuate']) + service_get_mock.side_effects = self._fake_service_get_by_compute_host + api_get_mock.side_effects = self._fake_compute_api_get + + ctxt = self._get_admin_context() + app = fakes.wsgi_app(fake_auth_context=ctxt) + req = webob.Request.blank('/v2/fake/servers/%s/action' % self.UUID) + req.method = 'POST' + req.body = jsonutils.dumps({ + 'evacuate': { + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass' + } + }) + req.content_type = 'application/json' + res = req.get_response(app) + self.assertEqual(400, res.status_int) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_hypervisors.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_hypervisors.py new file mode 100644 index 0000000000..df5e0d787a --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_hypervisors.py @@ -0,0 +1,101 @@ +# Copyright 2014 IBM Corp. +# +# 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 copy + +import mock + +from nova.api.openstack.compute.contrib import hypervisors as hypervisors_v2 +from nova.api.openstack.compute.plugins.v3 import hypervisors \ + as hypervisors_v21 +from nova.api.openstack import extensions +from nova import db +from nova import exception +from nova import test +from nova.tests.unit.api.openstack.compute.contrib import test_hypervisors +from nova.tests.unit.api.openstack import fakes + + +def fake_compute_node_get(context, compute_id): + for hyper in test_hypervisors.TEST_HYPERS: + if hyper['id'] == compute_id: + return hyper + raise exception.ComputeHostNotFound(host=compute_id) + + +def fake_compute_node_get_all(context): + return test_hypervisors.TEST_HYPERS + + +class ExtendedHypervisorsTestV21(test.NoDBTestCase): + DETAIL_HYPERS_DICTS = copy.deepcopy(test_hypervisors.TEST_HYPERS) + del DETAIL_HYPERS_DICTS[0]['service_id'] + del DETAIL_HYPERS_DICTS[1]['service_id'] + DETAIL_HYPERS_DICTS[0].update({'state': 'up', + 'status': 'enabled', + 'service': dict(id=1, host='compute1', + disabled_reason=None)}) + DETAIL_HYPERS_DICTS[1].update({'state': 'up', + 'status': 'enabled', + 'service': dict(id=2, host='compute2', + disabled_reason=None)}) + + def _set_up_controller(self): + self.controller = hypervisors_v21.HypervisorsController() + self.controller.servicegroup_api.service_is_up = mock.MagicMock( + return_value=True) + + def _get_request(self): + return fakes.HTTPRequest.blank('/v2/fake/os-hypervisors/detail', + use_admin_context=True) + + def setUp(self): + super(ExtendedHypervisorsTestV21, self).setUp() + self._set_up_controller() + + self.stubs.Set(db, 'compute_node_get_all', fake_compute_node_get_all) + self.stubs.Set(db, 'compute_node_get', + fake_compute_node_get) + + def test_view_hypervisor_detail_noservers(self): + result = self.controller._view_hypervisor( + test_hypervisors.TEST_HYPERS[0], True) + + self.assertEqual(result, self.DETAIL_HYPERS_DICTS[0]) + + def test_detail(self): + req = self._get_request() + result = self.controller.detail(req) + + self.assertEqual(result, dict(hypervisors=self.DETAIL_HYPERS_DICTS)) + + def test_show_withid(self): + req = self._get_request() + result = self.controller.show(req, '1') + + self.assertEqual(result, dict(hypervisor=self.DETAIL_HYPERS_DICTS[0])) + + +class ExtendedHypervisorsTestV2(ExtendedHypervisorsTestV21): + DETAIL_HYPERS_DICTS = copy.deepcopy(test_hypervisors.TEST_HYPERS) + del DETAIL_HYPERS_DICTS[0]['service_id'] + del DETAIL_HYPERS_DICTS[1]['service_id'] + DETAIL_HYPERS_DICTS[0].update({'service': dict(id=1, host='compute1')}) + DETAIL_HYPERS_DICTS[1].update({'service': dict(id=2, host='compute2')}) + + def _set_up_controller(self): + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.ext_mgr.extensions['os-extended-hypervisors'] = True + self.controller = hypervisors_v2.HypervisorsController(self.ext_mgr) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_ips.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_ips.py new file mode 100644 index 0000000000..770814116c --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_ips.py @@ -0,0 +1,189 @@ +# Copyright 2013 Nebula, Inc. +# 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import extended_ips +from nova.api.openstack import xmlutil +from nova import compute +from nova import objects +from nova.objects import instance as instance_obj +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + +UUID1 = '00000000-0000-0000-0000-000000000001' +UUID2 = '00000000-0000-0000-0000-000000000002' +UUID3 = '00000000-0000-0000-0000-000000000003' +NW_CACHE = [ + { + 'address': 'aa:aa:aa:aa:aa:aa', + 'id': 1, + 'network': { + 'bridge': 'br0', + 'id': 1, + 'label': 'private', + 'subnets': [ + { + 'cidr': '192.168.1.0/24', + 'ips': [ + { + 'address': '192.168.1.100', + 'type': 'fixed', + 'floating_ips': [ + {'address': '5.0.0.1', 'type': 'floating'}, + ], + }, + ], + }, + ] + } + }, + { + 'address': 'bb:bb:bb:bb:bb:bb', + 'id': 2, + 'network': { + 'bridge': 'br1', + 'id': 2, + 'label': 'public', + 'subnets': [ + { + 'cidr': '10.0.0.0/24', + 'ips': [ + { + 'address': '10.0.0.100', + 'type': 'fixed', + 'floating_ips': [ + {'address': '5.0.0.2', 'type': 'floating'}, + ], + } + ], + }, + ] + } + } +] +ALL_IPS = [] +for cache in NW_CACHE: + for subnet in cache['network']['subnets']: + for fixed in subnet['ips']: + sanitized = dict(fixed) + sanitized.pop('floating_ips') + ALL_IPS.append(sanitized) + for floating in fixed['floating_ips']: + ALL_IPS.append(floating) +ALL_IPS.sort() + + +def fake_compute_get(*args, **kwargs): + inst = fakes.stub_instance(1, uuid=UUID3, nw_cache=NW_CACHE) + return fake_instance.fake_instance_obj(args[1], + expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS, **inst) + + +def fake_compute_get_all(*args, **kwargs): + db_list = [ + fakes.stub_instance(1, uuid=UUID1, nw_cache=NW_CACHE), + fakes.stub_instance(2, uuid=UUID2, nw_cache=NW_CACHE), + ] + fields = instance_obj.INSTANCE_DEFAULT_FIELDS + return instance_obj._make_instance_list(args[1], + objects.InstanceList(), + db_list, fields) + + +class ExtendedIpsTestV21(test.TestCase): + content_type = 'application/json' + prefix = 'OS-EXT-IPS:' + + def setUp(self): + super(ExtendedIpsTestV21, self).setUp() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app_v21(init_only=('servers',))) + return res + + def _get_server(self, body): + return jsonutils.loads(body).get('server') + + def _get_servers(self, body): + return jsonutils.loads(body).get('servers') + + def _get_ips(self, server): + for network in server['addresses'].itervalues(): + for ip in network: + yield ip + + def assertServerStates(self, server): + results = [] + for ip in self._get_ips(server): + results.append({'address': ip.get('addr'), + 'type': ip.get('%stype' % self.prefix)}) + + self.assertEqual(ALL_IPS, sorted(results)) + + def test_show(self): + url = '/v2/fake/servers/%s' % UUID3 + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + self.assertServerStates(self._get_server(res.body)) + + def test_detail(self): + url = '/v2/fake/servers/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + for i, server in enumerate(self._get_servers(res.body)): + self.assertServerStates(server) + + +class ExtendedIpsTestV2(ExtendedIpsTestV21): + + def setUp(self): + super(ExtendedIpsTestV2, self).setUp() + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Extended_ips']) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app(init_only=('servers',))) + return res + + +class ExtendedIpsXmlTest(ExtendedIpsTestV2): + content_type = 'application/xml' + prefix = '{%s}' % extended_ips.Extended_ips.namespace + + def _get_server(self, body): + return etree.XML(body) + + def _get_servers(self, body): + return etree.XML(body).getchildren() + + def _get_ips(self, server): + for network in server.find('{%s}addresses' % xmlutil.XMLNS_V11): + for ip in network: + yield ip diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_ips_mac.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_ips_mac.py new file mode 100644 index 0000000000..c3e94600aa --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_ips_mac.py @@ -0,0 +1,196 @@ +# Copyright 2013 IBM Corp. +# 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import extended_ips_mac +from nova.api.openstack import xmlutil +from nova import compute +from nova import objects +from nova.objects import instance as instance_obj +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + +UUID1 = '00000000-0000-0000-0000-000000000001' +UUID2 = '00000000-0000-0000-0000-000000000002' +UUID3 = '00000000-0000-0000-0000-000000000003' +NW_CACHE = [ + { + 'address': 'aa:aa:aa:aa:aa:aa', + 'id': 1, + 'network': { + 'bridge': 'br0', + 'id': 1, + 'label': 'private', + 'subnets': [ + { + 'cidr': '192.168.1.0/24', + 'ips': [ + { + 'address': '192.168.1.100', + 'type': 'fixed', + 'floating_ips': [ + {'address': '5.0.0.1', 'type': 'floating'}, + ], + }, + ], + }, + ] + } + }, + { + 'address': 'bb:bb:bb:bb:bb:bb', + 'id': 2, + 'network': { + 'bridge': 'br1', + 'id': 2, + 'label': 'public', + 'subnets': [ + { + 'cidr': '10.0.0.0/24', + 'ips': [ + { + 'address': '10.0.0.100', + 'type': 'fixed', + 'floating_ips': [ + {'address': '5.0.0.2', 'type': 'floating'}, + ], + } + ], + }, + ] + } + } +] +ALL_IPS = [] +for cache in NW_CACHE: + for subnet in cache['network']['subnets']: + for fixed in subnet['ips']: + sanitized = dict(fixed) + sanitized['mac_address'] = cache['address'] + sanitized.pop('floating_ips') + sanitized.pop('type') + ALL_IPS.append(sanitized) + for floating in fixed['floating_ips']: + sanitized = dict(floating) + sanitized['mac_address'] = cache['address'] + sanitized.pop('type') + ALL_IPS.append(sanitized) +ALL_IPS.sort() + + +def fake_compute_get(*args, **kwargs): + inst = fakes.stub_instance(1, uuid=UUID3, nw_cache=NW_CACHE) + return fake_instance.fake_instance_obj(args[1], + expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS, **inst) + + +def fake_compute_get_all(*args, **kwargs): + db_list = [ + fakes.stub_instance(1, uuid=UUID1, nw_cache=NW_CACHE), + fakes.stub_instance(2, uuid=UUID2, nw_cache=NW_CACHE), + ] + fields = instance_obj.INSTANCE_DEFAULT_FIELDS + return instance_obj._make_instance_list(args[1], + objects.InstanceList(), + db_list, fields) + + +class ExtendedIpsMacTestV21(test.TestCase): + content_type = 'application/json' + prefix = '%s:' % extended_ips_mac.Extended_ips_mac.alias + + def setUp(self): + super(ExtendedIpsMacTestV21, self).setUp() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app_v21(init_only=('servers',))) + return res + + def _get_server(self, body): + return jsonutils.loads(body).get('server') + + def _get_servers(self, body): + return jsonutils.loads(body).get('servers') + + def _get_ips(self, server): + for network in server['addresses'].itervalues(): + for ip in network: + yield ip + + def assertServerStates(self, server): + results = [] + for ip in self._get_ips(server): + results.append({'address': ip.get('addr'), + 'mac_address': ip.get('%smac_addr' % self.prefix)}) + + self.assertEqual(ALL_IPS, sorted(results)) + + def test_show(self): + url = '/v2/fake/servers/%s' % UUID3 + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + self.assertServerStates(self._get_server(res.body)) + + def test_detail(self): + url = '/v2/fake/servers/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + for _i, server in enumerate(self._get_servers(res.body)): + self.assertServerStates(server) + + +class ExtendedIpsMacTestV2(ExtendedIpsMacTestV21): + content_type = 'application/json' + prefix = '%s:' % extended_ips_mac.Extended_ips_mac.alias + + def setUp(self): + super(ExtendedIpsMacTestV2, self).setUp() + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Extended_ips_mac']) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app(init_only=('servers',))) + return res + + +class ExtendedIpsMacXmlTest(ExtendedIpsMacTestV2): + content_type = 'application/xml' + prefix = '{%s}' % extended_ips_mac.Extended_ips_mac.namespace + + def _get_server(self, body): + return etree.XML(body) + + def _get_servers(self, body): + return etree.XML(body).getchildren() + + def _get_ips(self, server): + for network in server.find('{%s}addresses' % xmlutil.XMLNS_V11): + for ip in network: + yield ip diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_rescue_with_image.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_rescue_with_image.py new file mode 100644 index 0000000000..42a8382595 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_rescue_with_image.py @@ -0,0 +1,62 @@ +# Copyright 2014 OpenStack Foundation +# +# 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 nova.api.openstack import common +from nova.api.openstack.compute.contrib import rescue +from nova.api.openstack import extensions +from nova import compute +import nova.context as context +from nova import test + +CONF = cfg.CONF +CONF.import_opt('password_length', 'nova.utils') + + +class FakeRequest(object): + def __init__(self, context): + self.environ = {"nova.context": context} + + +class ExtendedRescueWithImageTest(test.NoDBTestCase): + def setUp(self): + super(ExtendedRescueWithImageTest, self).setUp() + ext_mgr = extensions.ExtensionManager() + ext_mgr.extensions = {'os-extended-rescue-with-image': 'fake'} + self.controller = rescue.RescueController(ext_mgr) + + @mock.patch.object(common, 'get_instance', + return_value="instance") + @mock.patch.object(compute.api.API, "rescue") + def _make_rescue_request_with_image_ref(self, body, mock_rescue, + mock_get_instance): + instance = "instance" + self.controller._get_instance = mock.Mock(return_value=instance) + fake_context = context.RequestContext('fake', 'fake') + req = FakeRequest(fake_context) + + self.controller._rescue(req, "id", body) + rescue_image_ref = body["rescue"].get("rescue_image_ref") + mock_rescue.assert_called_with(mock.ANY, mock.ANY, + rescue_password=mock.ANY, rescue_image_ref=rescue_image_ref) + + def test_rescue_with_image_specified(self): + body = dict(rescue={"rescue_image_ref": "image-ref"}) + self._make_rescue_request_with_image_ref(body) + + def test_rescue_without_image_specified(self): + body = dict(rescue={}) + self._make_rescue_request_with_image_ref(body) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_server_attributes.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_server_attributes.py new file mode 100644 index 0000000000..f944289efe --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_server_attributes.py @@ -0,0 +1,148 @@ +# Copyright 2011 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import extended_server_attributes +from nova import compute +from nova import db +from nova import exception +from nova import objects +from nova.objects import instance as instance_obj +from nova import test +from nova.tests.unit.api.openstack import fakes + +from oslo.config import cfg + + +NAME_FMT = cfg.CONF.instance_name_template +UUID1 = '00000000-0000-0000-0000-000000000001' +UUID2 = '00000000-0000-0000-0000-000000000002' +UUID3 = '00000000-0000-0000-0000-000000000003' + + +def fake_compute_get(*args, **kwargs): + fields = instance_obj.INSTANCE_DEFAULT_FIELDS + return objects.Instance._from_db_object( + args[1], objects.Instance(), + fakes.stub_instance(1, uuid=UUID3, host="host-fake", + node="node-fake"), fields) + + +def fake_compute_get_all(*args, **kwargs): + db_list = [ + fakes.stub_instance(1, uuid=UUID1, host="host-1", node="node-1"), + fakes.stub_instance(2, uuid=UUID2, host="host-2", node="node-2") + ] + fields = instance_obj.INSTANCE_DEFAULT_FIELDS + return instance_obj._make_instance_list(args[1], + objects.InstanceList(), + db_list, fields) + + +class ExtendedServerAttributesTestV21(test.TestCase): + content_type = 'application/json' + prefix = 'OS-EXT-SRV-ATTR:' + fake_url = '/v2/fake' + + def setUp(self): + super(ExtendedServerAttributesTestV21, self).setUp() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all) + self.stubs.Set(db, 'instance_get_by_uuid', fake_compute_get) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response( + fakes.wsgi_app_v21(init_only=('servers', + 'os-extended-server-attributes'))) + return res + + def _get_server(self, body): + return jsonutils.loads(body).get('server') + + def _get_servers(self, body): + return jsonutils.loads(body).get('servers') + + def assertServerAttributes(self, server, host, node, instance_name): + self.assertEqual(server.get('%shost' % self.prefix), host) + self.assertEqual(server.get('%sinstance_name' % self.prefix), + instance_name) + self.assertEqual(server.get('%shypervisor_hostname' % self.prefix), + node) + + def test_show(self): + url = self.fake_url + '/servers/%s' % UUID3 + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + self.assertServerAttributes(self._get_server(res.body), + host='host-fake', + node='node-fake', + instance_name=NAME_FMT % 1) + + def test_detail(self): + url = self.fake_url + '/servers/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + for i, server in enumerate(self._get_servers(res.body)): + self.assertServerAttributes(server, + host='host-%s' % (i + 1), + node='node-%s' % (i + 1), + instance_name=NAME_FMT % (i + 1)) + + def test_no_instance_passthrough_404(self): + + def fake_compute_get(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + url = self.fake_url + '/servers/70f6db34-de8d-4fbd-aafb-4065bdfa6115' + res = self._make_request(url) + + self.assertEqual(res.status_int, 404) + + +class ExtendedServerAttributesTestV2(ExtendedServerAttributesTestV21): + + def setUp(self): + super(ExtendedServerAttributesTestV2, self).setUp() + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Extended_server_attributes']) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app(init_only=('servers',))) + return res + + +class ExtendedServerAttributesXmlTest(ExtendedServerAttributesTestV2): + content_type = 'application/xml' + ext = extended_server_attributes + prefix = '{%s}' % ext.Extended_server_attributes.namespace + + def _get_server(self, body): + return etree.XML(body) + + def _get_servers(self, body): + return etree.XML(body).getchildren() diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_status.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_status.py new file mode 100644 index 0000000000..b47562f7a7 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_status.py @@ -0,0 +1,148 @@ +# Copyright 2011 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import extended_status +from nova import compute +from nova import db +from nova import exception +from nova import objects +from nova.objects import instance as instance_obj +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + +UUID1 = '00000000-0000-0000-0000-000000000001' +UUID2 = '00000000-0000-0000-0000-000000000002' +UUID3 = '00000000-0000-0000-0000-000000000003' + + +def fake_compute_get(*args, **kwargs): + inst = fakes.stub_instance(1, uuid=UUID3, task_state="kayaking", + vm_state="slightly crunchy", power_state=1) + return fake_instance.fake_instance_obj(args[1], **inst) + + +def fake_compute_get_all(*args, **kwargs): + db_list = [ + fakes.stub_instance(1, uuid=UUID1, task_state="task-1", + vm_state="vm-1", power_state=1), + fakes.stub_instance(2, uuid=UUID2, task_state="task-2", + vm_state="vm-2", power_state=2), + ] + + fields = instance_obj.INSTANCE_DEFAULT_FIELDS + return instance_obj._make_instance_list(args[1], + objects.InstanceList(), + db_list, fields) + + +class ExtendedStatusTestV21(test.TestCase): + content_type = 'application/json' + prefix = 'OS-EXT-STS:' + fake_url = '/v2/fake' + + def _set_flags(self): + pass + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app_v21( + init_only=('servers', + 'os-extended-status'))) + return res + + def setUp(self): + super(ExtendedStatusTestV21, self).setUp() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all) + self._set_flags() + return_server = fakes.fake_instance_get() + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + def _get_server(self, body): + return jsonutils.loads(body).get('server') + + def _get_servers(self, body): + return jsonutils.loads(body).get('servers') + + def assertServerStates(self, server, vm_state, power_state, task_state): + self.assertEqual(server.get('%svm_state' % self.prefix), vm_state) + self.assertEqual(int(server.get('%spower_state' % self.prefix)), + power_state) + self.assertEqual(server.get('%stask_state' % self.prefix), task_state) + + def test_show(self): + url = self.fake_url + '/servers/%s' % UUID3 + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + self.assertServerStates(self._get_server(res.body), + vm_state='slightly crunchy', + power_state=1, + task_state='kayaking') + + def test_detail(self): + url = self.fake_url + '/servers/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + for i, server in enumerate(self._get_servers(res.body)): + self.assertServerStates(server, + vm_state='vm-%s' % (i + 1), + power_state=(i + 1), + task_state='task-%s' % (i + 1)) + + def test_no_instance_passthrough_404(self): + + def fake_compute_get(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + url = self.fake_url + '/servers/70f6db34-de8d-4fbd-aafb-4065bdfa6115' + res = self._make_request(url) + + self.assertEqual(res.status_int, 404) + + +class ExtendedStatusTestV2(ExtendedStatusTestV21): + + def _set_flags(self): + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Extended_status']) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app(init_only=('servers',))) + return res + + +class ExtendedStatusXmlTest(ExtendedStatusTestV2): + content_type = 'application/xml' + prefix = '{%s}' % extended_status.Extended_status.namespace + + def _get_server(self, body): + return etree.XML(body) + + def _get_servers(self, body): + return etree.XML(body).getchildren() diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_virtual_interfaces_net.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_virtual_interfaces_net.py new file mode 100644 index 0000000000..851848d7a5 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_virtual_interfaces_net.py @@ -0,0 +1,123 @@ +# Copyright 2013 IBM Corp. +# 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import extended_virtual_interfaces_net +from nova.api.openstack import wsgi +from nova import compute +from nova import network +from nova import test +from nova.tests.unit.api.openstack import fakes + + +FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + + +FAKE_VIFS = [{'uuid': '00000000-0000-0000-0000-00000000000000000', + 'address': '00-00-00-00-00-00', + 'net_uuid': '00000000-0000-0000-0000-00000000000000001'}, + {'uuid': '11111111-1111-1111-1111-11111111111111111', + 'address': '11-11-11-11-11-11', + 'net_uuid': '11111111-1111-1111-1111-11111111111111112'}] + +EXPECTED_NET_UUIDS = ['00000000-0000-0000-0000-00000000000000001', + '11111111-1111-1111-1111-11111111111111112'] + + +def compute_api_get(self, context, instance_id, expected_attrs=None, + want_objects=False): + return dict(uuid=FAKE_UUID, id=instance_id, instance_type_id=1, host='bob') + + +def get_vifs_by_instance(self, context, instance_id): + return FAKE_VIFS + + +def get_vif_by_mac_address(self, context, mac_address): + if mac_address == "00-00-00-00-00-00": + return {'net_uuid': '00000000-0000-0000-0000-00000000000000001'} + else: + return {'net_uuid': '11111111-1111-1111-1111-11111111111111112'} + + +class ExtendedServerVIFNetTest(test.NoDBTestCase): + content_type = 'application/json' + prefix = "%s:" % extended_virtual_interfaces_net. \ + Extended_virtual_interfaces_net.alias + + def setUp(self): + super(ExtendedServerVIFNetTest, self).setUp() + self.stubs.Set(compute.api.API, "get", + compute_api_get) + self.stubs.Set(network.api.API, "get_vifs_by_instance", + get_vifs_by_instance) + self.stubs.Set(network.api.API, "get_vif_by_mac_address", + get_vif_by_mac_address) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Virtual_interfaces', + 'Extended_virtual_interfaces_net']) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app(init_only=( + 'os-virtual-interfaces', 'OS-EXT-VIF-NET'))) + return res + + def _get_vifs(self, body): + return jsonutils.loads(body).get('virtual_interfaces') + + def _get_net_id(self, vifs): + for vif in vifs: + yield vif['%snet_id' % self.prefix] + + def assertVIFs(self, vifs): + result = [] + for net_id in self._get_net_id(vifs): + result.append(net_id) + sorted(result) + + for i, net_uuid in enumerate(result): + self.assertEqual(net_uuid, EXPECTED_NET_UUIDS[i]) + + def test_get_extend_virtual_interfaces_list(self): + res = self._make_request('/v2/fake/servers/abcd/os-virtual-interfaces') + + self.assertEqual(res.status_int, 200) + self.assertVIFs(self._get_vifs(res.body)) + + +class ExtendedServerVIFNetSerializerTest(ExtendedServerVIFNetTest): + content_type = 'application/xml' + prefix = "{%s}" % extended_virtual_interfaces_net. \ + Extended_virtual_interfaces_net.namespace + + def setUp(self): + super(ExtendedServerVIFNetSerializerTest, self).setUp() + self.namespace = wsgi.XMLNS_V11 + self.serializer = extended_virtual_interfaces_net. \ + ExtendedVirtualInterfaceNetTemplate() + + def _get_vifs(self, body): + return etree.XML(body).getchildren() + + def _get_net_id(self, vifs): + for vif in vifs: + yield vif.attrib['%snet_id' % self.prefix] diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_extended_volumes.py b/nova/tests/unit/api/openstack/compute/contrib/test_extended_volumes.py new file mode 100644 index 0000000000..d441013e8d --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_extended_volumes.py @@ -0,0 +1,124 @@ +# Copyright 2013 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import extended_volumes +from nova import compute +from nova import db +from nova import objects +from nova.objects import instance as instance_obj +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_block_device +from nova.tests.unit import fake_instance + +UUID1 = '00000000-0000-0000-0000-000000000001' +UUID2 = '00000000-0000-0000-0000-000000000002' +UUID3 = '00000000-0000-0000-0000-000000000003' + + +def fake_compute_get(*args, **kwargs): + inst = fakes.stub_instance(1, uuid=UUID1) + return fake_instance.fake_instance_obj(args[1], **inst) + + +def fake_compute_get_all(*args, **kwargs): + db_list = [fakes.stub_instance(1), fakes.stub_instance(2)] + fields = instance_obj.INSTANCE_DEFAULT_FIELDS + return instance_obj._make_instance_list(args[1], + objects.InstanceList(), + db_list, fields) + + +def fake_bdms_get_all_by_instance(*args, **kwargs): + return [fake_block_device.FakeDbBlockDeviceDict( + {'volume_id': UUID1, 'source_type': 'volume', + 'destination_type': 'volume', 'id': 1}), + fake_block_device.FakeDbBlockDeviceDict( + {'volume_id': UUID2, 'source_type': 'volume', + 'destination_type': 'volume', 'id': 2})] + + +class ExtendedVolumesTest(test.TestCase): + content_type = 'application/json' + prefix = 'os-extended-volumes:' + + def setUp(self): + super(ExtendedVolumesTest, self).setUp() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all) + self.stubs.Set(db, 'block_device_mapping_get_all_by_instance', + fake_bdms_get_all_by_instance) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Extended_volumes']) + return_server = fakes.fake_instance_get() + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app(init_only=('servers',))) + return res + + def _get_server(self, body): + return jsonutils.loads(body).get('server') + + def _get_servers(self, body): + return jsonutils.loads(body).get('servers') + + def test_show(self): + url = '/v2/fake/servers/%s' % UUID1 + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + server = self._get_server(res.body) + exp_volumes = [{'id': UUID1}, {'id': UUID2}] + if self.content_type == 'application/json': + actual = server.get('%svolumes_attached' % self.prefix) + elif self.content_type == 'application/xml': + actual = [dict(elem.items()) for elem in + server.findall('%svolume_attached' % self.prefix)] + self.assertEqual(exp_volumes, actual) + + def test_detail(self): + url = '/v2/fake/servers/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + exp_volumes = [{'id': UUID1}, {'id': UUID2}] + for i, server in enumerate(self._get_servers(res.body)): + if self.content_type == 'application/json': + actual = server.get('%svolumes_attached' % self.prefix) + elif self.content_type == 'application/xml': + actual = [dict(elem.items()) for elem in + server.findall('%svolume_attached' % self.prefix)] + self.assertEqual(exp_volumes, actual) + + +class ExtendedVolumesXmlTest(ExtendedVolumesTest): + content_type = 'application/xml' + prefix = '{%s}' % extended_volumes.Extended_volumes.namespace + + def _get_server(self, body): + return etree.XML(body) + + def _get_servers(self, body): + return etree.XML(body).getchildren() diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_fixed_ips.py b/nova/tests/unit/api/openstack/compute/contrib/test_fixed_ips.py new file mode 100644 index 0000000000..f331da80fe --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_fixed_ips.py @@ -0,0 +1,256 @@ +# Copyright 2012 IBM Corp. +# +# 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 webob + +from nova.api.openstack.compute.contrib import fixed_ips as fixed_ips_v2 +from nova.api.openstack.compute.plugins.v3 import fixed_ips as fixed_ips_v21 +from nova import context +from nova import db +from nova import exception +from nova.i18n import _ +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit.objects import test_network + + +fake_fixed_ips = [{'id': 1, + 'address': '192.168.1.1', + 'network_id': 1, + 'virtual_interface_id': 1, + 'instance_uuid': '1', + 'allocated': False, + 'leased': False, + 'reserved': False, + 'host': None, + 'instance': None, + 'network': test_network.fake_network, + 'created_at': None, + 'updated_at': None, + 'deleted_at': None, + 'deleted': False}, + {'id': 2, + 'address': '192.168.1.2', + 'network_id': 1, + 'virtual_interface_id': 2, + 'instance_uuid': '2', + 'allocated': False, + 'leased': False, + 'reserved': False, + 'host': None, + 'instance': None, + 'network': test_network.fake_network, + 'created_at': None, + 'updated_at': None, + 'deleted_at': None, + 'deleted': False}, + {'id': 3, + 'address': '10.0.0.2', + 'network_id': 1, + 'virtual_interface_id': 3, + 'instance_uuid': '3', + 'allocated': False, + 'leased': False, + 'reserved': False, + 'host': None, + 'instance': None, + 'network': test_network.fake_network, + 'created_at': None, + 'updated_at': None, + 'deleted_at': None, + 'deleted': True}, + ] + + +def fake_fixed_ip_get_by_address(context, address, columns_to_join=None): + if address == 'inv.ali.d.ip': + msg = _("Invalid fixed IP Address %s in request") % address + raise exception.FixedIpInvalid(msg) + for fixed_ip in fake_fixed_ips: + if fixed_ip['address'] == address and not fixed_ip['deleted']: + return fixed_ip + raise exception.FixedIpNotFoundForAddress(address=address) + + +def fake_fixed_ip_get_by_address_detailed(context, address): + network = {'id': 1, + 'cidr': "192.168.1.0/24"} + for fixed_ip in fake_fixed_ips: + if fixed_ip['address'] == address and not fixed_ip['deleted']: + return (fixed_ip, FakeModel(network), None) + raise exception.FixedIpNotFoundForAddress(address=address) + + +def fake_fixed_ip_update(context, address, values): + fixed_ip = fake_fixed_ip_get_by_address(context, address) + if fixed_ip is None: + raise exception.FixedIpNotFoundForAddress(address=address) + else: + for key in values: + fixed_ip[key] = values[key] + + +class FakeModel(object): + """Stubs out for model.""" + def __init__(self, values): + self.values = values + + def __getattr__(self, name): + return self.values[name] + + def __getitem__(self, key): + if key in self.values: + return self.values[key] + else: + raise NotImplementedError() + + def __repr__(self): + return '<FakeModel: %s>' % self.values + + +def fake_network_get_all(context): + network = {'id': 1, + 'cidr': "192.168.1.0/24"} + return [FakeModel(network)] + + +class FixedIpTestV21(test.NoDBTestCase): + + fixed_ips = fixed_ips_v21 + url = '/v2/fake/os-fixed-ips' + + def setUp(self): + super(FixedIpTestV21, self).setUp() + + self.stubs.Set(db, "fixed_ip_get_by_address", + fake_fixed_ip_get_by_address) + self.stubs.Set(db, "fixed_ip_get_by_address_detailed", + fake_fixed_ip_get_by_address_detailed) + self.stubs.Set(db, "fixed_ip_update", fake_fixed_ip_update) + + self.context = context.get_admin_context() + self.controller = self.fixed_ips.FixedIPController() + + def _assert_equal(self, ret, exp): + self.assertEqual(ret.wsgi_code, exp) + + def _get_reserve_action(self): + return self.controller.reserve + + def _get_unreserve_action(self): + return self.controller.unreserve + + def test_fixed_ips_get(self): + req = fakes.HTTPRequest.blank('%s/192.168.1.1' % self.url) + res_dict = self.controller.show(req, '192.168.1.1') + response = {'fixed_ip': {'cidr': '192.168.1.0/24', + 'hostname': None, + 'host': None, + 'address': '192.168.1.1'}} + self.assertEqual(response, res_dict) + + def test_fixed_ips_get_bad_ip_fail(self): + req = fakes.HTTPRequest.blank('%s/10.0.0.1' % self.url) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, req, + '10.0.0.1') + + def test_fixed_ips_get_invalid_ip_address(self): + req = fakes.HTTPRequest.blank('%s/inv.ali.d.ip' % self.url) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.show, req, + 'inv.ali.d.ip') + + def test_fixed_ips_get_deleted_ip_fail(self): + req = fakes.HTTPRequest.blank('%s/10.0.0.2' % self.url) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, req, + '10.0.0.2') + + def test_fixed_ip_reserve(self): + fake_fixed_ips[0]['reserved'] = False + body = {'reserve': None} + req = fakes.HTTPRequest.blank('%s/192.168.1.1/action' % self.url) + action = self._get_reserve_action() + result = action(req, "192.168.1.1", body) + + self._assert_equal(result or action, 202) + self.assertEqual(fake_fixed_ips[0]['reserved'], True) + + def test_fixed_ip_reserve_bad_ip(self): + body = {'reserve': None} + req = fakes.HTTPRequest.blank('%s/10.0.0.1/action' % self.url) + action = self._get_reserve_action() + + self.assertRaises(webob.exc.HTTPNotFound, action, req, + '10.0.0.1', body) + + def test_fixed_ip_reserve_invalid_ip_address(self): + body = {'reserve': None} + req = fakes.HTTPRequest.blank('%s/inv.ali.d.ip/action' % self.url) + action = self._get_reserve_action() + + self.assertRaises(webob.exc.HTTPBadRequest, + action, req, 'inv.ali.d.ip', body) + + def test_fixed_ip_reserve_deleted_ip(self): + body = {'reserve': None} + action = self._get_reserve_action() + + req = fakes.HTTPRequest.blank('%s/10.0.0.2/action' % self.url) + self.assertRaises(webob.exc.HTTPNotFound, action, req, + '10.0.0.2', body) + + def test_fixed_ip_unreserve(self): + fake_fixed_ips[0]['reserved'] = True + body = {'unreserve': None} + req = fakes.HTTPRequest.blank('%s/192.168.1.1/action' % self.url) + action = self._get_unreserve_action() + result = action(req, "192.168.1.1", body) + + self._assert_equal(result or action, 202) + self.assertEqual(fake_fixed_ips[0]['reserved'], False) + + def test_fixed_ip_unreserve_bad_ip(self): + body = {'unreserve': None} + req = fakes.HTTPRequest.blank('%s/10.0.0.1/action' % self.url) + action = self._get_unreserve_action() + + self.assertRaises(webob.exc.HTTPNotFound, action, req, + '10.0.0.1', body) + + def test_fixed_ip_unreserve_invalid_ip_address(self): + body = {'unreserve': None} + req = fakes.HTTPRequest.blank('%s/inv.ali.d.ip/action' % self.url) + action = self._get_unreserve_action() + self.assertRaises(webob.exc.HTTPBadRequest, + action, req, 'inv.ali.d.ip', body) + + def test_fixed_ip_unreserve_deleted_ip(self): + body = {'unreserve': None} + req = fakes.HTTPRequest.blank('%s/10.0.0.2/action' % self.url) + action = self._get_unreserve_action() + self.assertRaises(webob.exc.HTTPNotFound, action, req, + '10.0.0.2', body) + + +class FixedIpTestV2(FixedIpTestV21): + + fixed_ips = fixed_ips_v2 + + def _assert_equal(self, ret, exp): + self.assertEqual(ret.status, '202 Accepted') + + def _get_reserve_action(self): + return self.controller.action + + def _get_unreserve_action(self): + return self.controller.action diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavor_access.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_access.py new file mode 100644 index 0000000000..5718a826e4 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_access.py @@ -0,0 +1,402 @@ +# 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 datetime + +from lxml import etree +from webob import exc + +from nova.api.openstack.compute.contrib import flavor_access \ + as flavor_access_v2 +from nova.api.openstack.compute import flavors as flavors_api +from nova.api.openstack.compute.plugins.v3 import flavor_access \ + as flavor_access_v3 +from nova import context +from nova import db +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + + +def generate_flavor(flavorid, ispublic): + return { + 'id': flavorid, + 'flavorid': str(flavorid), + 'root_gb': 1, + 'ephemeral_gb': 1, + 'name': u'test', + 'deleted': False, + 'created_at': datetime.datetime(2012, 1, 1, 1, 1, 1, 1), + 'updated_at': None, + 'memory_mb': 512, + 'vcpus': 1, + 'swap': 512, + 'rxtx_factor': 1.0, + 'disabled': False, + 'extra_specs': {}, + 'deleted_at': None, + 'vcpu_weight': None, + 'is_public': bool(ispublic) + } + + +INSTANCE_TYPES = { + '0': generate_flavor(0, True), + '1': generate_flavor(1, True), + '2': generate_flavor(2, False), + '3': generate_flavor(3, False)} + + +ACCESS_LIST = [{'flavor_id': '2', 'project_id': 'proj2'}, + {'flavor_id': '2', 'project_id': 'proj3'}, + {'flavor_id': '3', 'project_id': 'proj3'}] + + +def fake_get_flavor_access_by_flavor_id(context, flavorid): + res = [] + for access in ACCESS_LIST: + if access['flavor_id'] == flavorid: + res.append(access) + return res + + +def fake_get_flavor_by_flavor_id(context, flavorid, read_deleted=None): + return INSTANCE_TYPES[flavorid] + + +def _has_flavor_access(flavorid, projectid): + for access in ACCESS_LIST: + if access['flavor_id'] == flavorid and \ + access['project_id'] == projectid: + return True + return False + + +def fake_get_all_flavors_sorted_list(context, inactive=False, + filters=None, sort_key='flavorid', + sort_dir='asc', limit=None, marker=None): + if filters is None or filters['is_public'] is None: + return sorted(INSTANCE_TYPES.values(), key=lambda item: item[sort_key]) + + res = {} + for k, v in INSTANCE_TYPES.iteritems(): + if filters['is_public'] and _has_flavor_access(k, context.project_id): + res.update({k: v}) + continue + if v['is_public'] == filters['is_public']: + res.update({k: v}) + + res = sorted(res.values(), key=lambda item: item[sort_key]) + return res + + +class FakeRequest(object): + environ = {"nova.context": context.get_admin_context()} + + def get_db_flavor(self, flavor_id): + return INSTANCE_TYPES[flavor_id] + + +class FakeResponse(object): + obj = {'flavor': {'id': '0'}, + 'flavors': [ + {'id': '0'}, + {'id': '2'}] + } + + def attach(self, **kwargs): + pass + + +class FlavorAccessTestV21(test.NoDBTestCase): + api_version = "2.1" + FlavorAccessController = flavor_access_v3.FlavorAccessController + FlavorActionController = flavor_access_v3.FlavorActionController + _prefix = "/v3" + validation_ex = exception.ValidationError + + def setUp(self): + super(FlavorAccessTestV21, self).setUp() + self.flavor_controller = flavors_api.Controller() + self.req = FakeRequest() + self.context = self.req.environ['nova.context'] + self.stubs.Set(db, 'flavor_get_by_flavor_id', + fake_get_flavor_by_flavor_id) + self.stubs.Set(db, 'flavor_get_all', + fake_get_all_flavors_sorted_list) + self.stubs.Set(db, 'flavor_access_get_by_flavor_id', + fake_get_flavor_access_by_flavor_id) + + self.flavor_access_controller = self.FlavorAccessController() + self.flavor_action_controller = self.FlavorActionController() + + def _verify_flavor_list(self, result, expected): + # result already sorted by flavor_id + self.assertEqual(len(result), len(expected)) + + for d1, d2 in zip(result, expected): + self.assertEqual(d1['id'], d2['id']) + + def test_list_flavor_access_public(self): + # query os-flavor-access on public flavor should return 404 + self.assertRaises(exc.HTTPNotFound, + self.flavor_access_controller.index, + self.req, '1') + + def test_list_flavor_access_private(self): + expected = {'flavor_access': [ + {'flavor_id': '2', 'tenant_id': 'proj2'}, + {'flavor_id': '2', 'tenant_id': 'proj3'}]} + result = self.flavor_access_controller.index(self.req, '2') + self.assertEqual(result, expected) + + def test_list_with_no_context(self): + req = fakes.HTTPRequest.blank(self._prefix + '/flavors/fake/flavors') + + def fake_authorize(context, target=None, action=None): + raise exception.PolicyNotAuthorized(action='index') + + if self.api_version == "2.1": + self.stubs.Set(flavor_access_v3, + 'authorize', + fake_authorize) + else: + self.stubs.Set(flavor_access_v2, + 'authorize', + fake_authorize) + + self.assertRaises(exception.PolicyNotAuthorized, + self.flavor_access_controller.index, + req, 'fake') + + def test_list_flavor_with_admin_default_proj1(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}]} + req = fakes.HTTPRequest.blank(self._prefix + '/fake/flavors', + use_admin_context=True) + req.environ['nova.context'].project_id = 'proj1' + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_admin_default_proj2(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}, {'id': '2'}]} + req = fakes.HTTPRequest.blank(self._prefix + '/flavors', + use_admin_context=True) + req.environ['nova.context'].project_id = 'proj2' + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_admin_ispublic_true(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}]} + url = self._prefix + '/flavors?is_public=true' + req = fakes.HTTPRequest.blank(url, + use_admin_context=True) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_admin_ispublic_false(self): + expected = {'flavors': [{'id': '2'}, {'id': '3'}]} + url = self._prefix + '/flavors?is_public=false' + req = fakes.HTTPRequest.blank(url, + use_admin_context=True) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_admin_ispublic_false_proj2(self): + expected = {'flavors': [{'id': '2'}, {'id': '3'}]} + url = self._prefix + '/flavors?is_public=false' + req = fakes.HTTPRequest.blank(url, + use_admin_context=True) + req.environ['nova.context'].project_id = 'proj2' + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_admin_ispublic_none(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}, {'id': '2'}, + {'id': '3'}]} + url = self._prefix + '/flavors?is_public=none' + req = fakes.HTTPRequest.blank(url, + use_admin_context=True) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_no_admin_default(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}]} + req = fakes.HTTPRequest.blank(self._prefix + '/flavors', + use_admin_context=False) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_no_admin_ispublic_true(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}]} + url = self._prefix + '/flavors?is_public=true' + req = fakes.HTTPRequest.blank(url, + use_admin_context=False) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_no_admin_ispublic_false(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}]} + url = self._prefix + '/flavors?is_public=false' + req = fakes.HTTPRequest.blank(url, + use_admin_context=False) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_no_admin_ispublic_none(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}]} + url = self._prefix + '/flavors?is_public=none' + req = fakes.HTTPRequest.blank(url, + use_admin_context=False) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_show(self): + resp = FakeResponse() + self.flavor_action_controller.show(self.req, resp, '0') + self.assertEqual({'id': '0', 'os-flavor-access:is_public': True}, + resp.obj['flavor']) + self.flavor_action_controller.show(self.req, resp, '2') + self.assertEqual({'id': '0', 'os-flavor-access:is_public': False}, + resp.obj['flavor']) + + def test_detail(self): + resp = FakeResponse() + self.flavor_action_controller.detail(self.req, resp) + self.assertEqual([{'id': '0', 'os-flavor-access:is_public': True}, + {'id': '2', 'os-flavor-access:is_public': False}], + resp.obj['flavors']) + + def test_create(self): + resp = FakeResponse() + self.flavor_action_controller.create(self.req, {}, resp) + self.assertEqual({'id': '0', 'os-flavor-access:is_public': True}, + resp.obj['flavor']) + + def _get_add_access(self): + if self.api_version == "2.1": + return self.flavor_action_controller._add_tenant_access + else: + return self.flavor_action_controller._addTenantAccess + + def _get_remove_access(self): + if self.api_version == "2.1": + return self.flavor_action_controller._remove_tenant_access + else: + return self.flavor_action_controller._removeTenantAccess + + def test_add_tenant_access(self): + def stub_add_flavor_access(context, flavorid, projectid): + self.assertEqual('3', flavorid, "flavorid") + self.assertEqual("proj2", projectid, "projectid") + self.stubs.Set(db, 'flavor_access_add', + stub_add_flavor_access) + expected = {'flavor_access': + [{'flavor_id': '3', 'tenant_id': 'proj3'}]} + body = {'addTenantAccess': {'tenant': 'proj2'}} + req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', + use_admin_context=True) + + add_access = self._get_add_access() + result = add_access(req, '3', body=body) + self.assertEqual(result, expected) + + def test_add_tenant_access_with_no_admin_user(self): + req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', + use_admin_context=False) + body = {'addTenantAccess': {'tenant': 'proj2'}} + add_access = self._get_add_access() + self.assertRaises(exception.PolicyNotAuthorized, + add_access, req, '2', body=body) + + def test_add_tenant_access_with_no_tenant(self): + req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', + use_admin_context=True) + body = {'addTenantAccess': {'foo': 'proj2'}} + add_access = self._get_add_access() + self.assertRaises(self.validation_ex, + add_access, req, '2', body=body) + body = {'addTenantAccess': {'tenant': ''}} + self.assertRaises(self.validation_ex, + add_access, req, '2', body=body) + + def test_add_tenant_access_with_already_added_access(self): + def stub_add_flavor_access(context, flavorid, projectid): + raise exception.FlavorAccessExists(flavor_id=flavorid, + project_id=projectid) + self.stubs.Set(db, 'flavor_access_add', + stub_add_flavor_access) + body = {'addTenantAccess': {'tenant': 'proj2'}} + add_access = self._get_add_access() + self.assertRaises(exc.HTTPConflict, + add_access, self.req, '3', body=body) + + def test_remove_tenant_access_with_bad_access(self): + def stub_remove_flavor_access(context, flavorid, projectid): + raise exception.FlavorAccessNotFound(flavor_id=flavorid, + project_id=projectid) + self.stubs.Set(db, 'flavor_access_remove', + stub_remove_flavor_access) + body = {'removeTenantAccess': {'tenant': 'proj2'}} + remove_access = self._get_remove_access() + self.assertRaises(exc.HTTPNotFound, + remove_access, self.req, '3', body=body) + + def test_delete_tenant_access_with_no_tenant(self): + req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', + use_admin_context=True) + remove_access = self._get_remove_access() + body = {'removeTenantAccess': {'foo': 'proj2'}} + self.assertRaises(self.validation_ex, + remove_access, req, '2', body=body) + body = {'removeTenantAccess': {'tenant': ''}} + self.assertRaises(self.validation_ex, + remove_access, req, '2', body=body) + + def test_remove_tenant_access_with_no_admin_user(self): + req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', + use_admin_context=False) + body = {'removeTenantAccess': {'tenant': 'proj2'}} + remove_access = self._get_remove_access() + self.assertRaises(exception.PolicyNotAuthorized, + remove_access, req, '2', body=body) + + +class FlavorAccessTestV20(FlavorAccessTestV21): + api_version = "2.0" + FlavorAccessController = flavor_access_v2.FlavorAccessController + FlavorActionController = flavor_access_v2.FlavorActionController + _prefix = "/v2/fake" + validation_ex = exc.HTTPBadRequest + + +class FlavorAccessSerializerTest(test.NoDBTestCase): + def test_serializer_empty(self): + serializer = flavor_access_v2.FlavorAccessTemplate() + text = serializer.serialize(dict(flavor_access=[])) + tree = etree.fromstring(text) + self.assertEqual(len(tree), 0) + + def test_serializer(self): + expected = ("<?xml version='1.0' encoding='UTF-8'?>\n" + '<flavor_access>' + '<access tenant_id="proj2" flavor_id="2"/>' + '<access tenant_id="proj3" flavor_id="2"/>' + '</flavor_access>') + access_list = [{'flavor_id': '2', 'tenant_id': 'proj2'}, + {'flavor_id': '2', 'tenant_id': 'proj3'}] + + serializer = flavor_access_v2.FlavorAccessTemplate() + text = serializer.serialize(dict(flavor_access=access_list)) + self.assertEqual(text, expected) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavor_disabled.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_disabled.py new file mode 100644 index 0000000000..a646f43fd1 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_disabled.py @@ -0,0 +1,127 @@ +# Copyright 2012 Nebula, 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import flavor_disabled +from nova.compute import flavors +from nova import test +from nova.tests.unit.api.openstack import fakes + +FAKE_FLAVORS = { + 'flavor 1': { + "flavorid": '1', + "name": 'flavor 1', + "memory_mb": '256', + "root_gb": '10', + "swap": 512, + "vcpus": 1, + "ephemeral_gb": 1, + "disabled": False, + }, + 'flavor 2': { + "flavorid": '2', + "name": 'flavor 2', + "memory_mb": '512', + "root_gb": '20', + "swap": None, + "vcpus": 1, + "ephemeral_gb": 1, + "disabled": True, + }, +} + + +def fake_flavor_get_by_flavor_id(flavorid, ctxt=None): + return FAKE_FLAVORS['flavor %s' % flavorid] + + +def fake_get_all_flavors_sorted_list(context=None, inactive=False, + filters=None, sort_key='flavorid', + sort_dir='asc', limit=None, marker=None): + return [ + fake_flavor_get_by_flavor_id(1), + fake_flavor_get_by_flavor_id(2) + ] + + +class FlavorDisabledTestV21(test.NoDBTestCase): + base_url = '/v2/fake/flavors' + content_type = 'application/json' + prefix = "OS-FLV-DISABLED:" + + def setUp(self): + super(FlavorDisabledTestV21, self).setUp() + ext = ('nova.api.openstack.compute.contrib' + '.flavor_disabled.Flavor_disabled') + self.flags(osapi_compute_extension=[ext]) + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(flavors, "get_all_flavors_sorted_list", + fake_get_all_flavors_sorted_list) + self.stubs.Set(flavors, + "get_flavor_by_flavor_id", + fake_flavor_get_by_flavor_id) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app_v21(init_only=('flavors'))) + return res + + def _get_flavor(self, body): + return jsonutils.loads(body).get('flavor') + + def _get_flavors(self, body): + return jsonutils.loads(body).get('flavors') + + def assertFlavorDisabled(self, flavor, disabled): + self.assertEqual(str(flavor.get('%sdisabled' % self.prefix)), disabled) + + def test_show(self): + url = self.base_url + '/1' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + self.assertFlavorDisabled(self._get_flavor(res.body), 'False') + + def test_detail(self): + url = self.base_url + '/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + flavors = self._get_flavors(res.body) + self.assertFlavorDisabled(flavors[0], 'False') + self.assertFlavorDisabled(flavors[1], 'True') + + +class FlavorDisabledTestV2(FlavorDisabledTestV21): + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app()) + return res + + +class FlavorDisabledXmlTest(FlavorDisabledTestV2): + content_type = 'application/xml' + prefix = '{%s}' % flavor_disabled.Flavor_disabled.namespace + + def _get_flavor(self, body): + return etree.XML(body) + + def _get_flavors(self, body): + return etree.XML(body).getchildren() diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavor_manage.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_manage.py new file mode 100644 index 0000000000..3d44e4970b --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_manage.py @@ -0,0 +1,465 @@ +# Copyright 2011 Andrew Bogott for the Wikimedia 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 datetime + +import mock +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import flavor_access +from nova.api.openstack.compute.contrib import flavormanage as flavormanage_v2 +from nova.api.openstack.compute.plugins.v3 import flavor_manage as \ + flavormanage_v21 +from nova.compute import flavors +from nova import context +from nova import db +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + + +def fake_db_flavor(**updates): + db_flavor = { + 'root_gb': 1, + 'ephemeral_gb': 1, + 'name': u'frob', + 'deleted': False, + 'created_at': datetime.datetime(2012, 1, 19, 18, 49, 30, 877329), + 'updated_at': None, + 'memory_mb': 256, + 'vcpus': 1, + 'flavorid': 1, + 'swap': 0, + 'rxtx_factor': 1.0, + 'extra_specs': {}, + 'deleted_at': None, + 'vcpu_weight': None, + 'id': 7, + 'is_public': True, + 'disabled': False, + } + if updates: + db_flavor.update(updates) + return db_flavor + + +def fake_get_flavor_by_flavor_id(flavorid, ctxt=None, read_deleted='yes'): + if flavorid == 'failtest': + raise exception.FlavorNotFound(flavor_id=flavorid) + elif not str(flavorid) == '1234': + raise Exception("This test expects flavorid 1234, not %s" % flavorid) + if read_deleted != 'no': + raise test.TestingException("Should not be reading deleted") + return fake_db_flavor(flavorid=flavorid) + + +def fake_destroy(flavorname): + pass + + +def fake_create(context, kwargs, projects=None): + newflavor = fake_db_flavor() + + flavorid = kwargs.get('flavorid') + if flavorid is None: + flavorid = 1234 + + newflavor['flavorid'] = flavorid + newflavor["name"] = kwargs.get('name') + newflavor["memory_mb"] = int(kwargs.get('memory_mb')) + newflavor["vcpus"] = int(kwargs.get('vcpus')) + newflavor["root_gb"] = int(kwargs.get('root_gb')) + newflavor["ephemeral_gb"] = int(kwargs.get('ephemeral_gb')) + newflavor["swap"] = kwargs.get('swap') + newflavor["rxtx_factor"] = float(kwargs.get('rxtx_factor')) + newflavor["is_public"] = bool(kwargs.get('is_public')) + newflavor["disabled"] = bool(kwargs.get('disabled')) + + return newflavor + + +class FlavorManageTestV21(test.NoDBTestCase): + controller = flavormanage_v21.FlavorManageController() + validation_error = exception.ValidationError + base_url = '/v2/fake/flavors' + + def setUp(self): + super(FlavorManageTestV21, self).setUp() + self.stubs.Set(flavors, + "get_flavor_by_flavor_id", + fake_get_flavor_by_flavor_id) + self.stubs.Set(flavors, "destroy", fake_destroy) + self.stubs.Set(db, "flavor_create", fake_create) + self.ctxt = context.RequestContext('fake', 'fake', + is_admin=True, auth_token=True) + self.app = self._setup_app() + + self.request_body = { + "flavor": { + "name": "test", + "ram": 512, + "vcpus": 2, + "disk": 1, + "OS-FLV-EXT-DATA:ephemeral": 1, + "id": unicode('1234'), + "swap": 512, + "rxtx_factor": 1, + "os-flavor-access:is_public": True, + } + } + self.expected_flavor = self.request_body + + def _setup_app(self): + return fakes.wsgi_app_v21(init_only=('flavor-manage', 'os-flavor-rxtx', + 'os-flavor-access', 'flavors', + 'os-flavor-extra-data')) + + def test_delete(self): + req = fakes.HTTPRequest.blank(self.base_url + '/1234') + res = self.controller._delete(req, 1234) + + # NOTE: on v2.1, http status code is set as wsgi_code of API + # method instead of status_int in a response object. + if isinstance(self.controller, + flavormanage_v21.FlavorManageController): + status_int = self.controller._delete.wsgi_code + else: + status_int = res.status_int + self.assertEqual(202, status_int) + + # subsequent delete should fail + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._delete, req, "failtest") + + def _test_create_missing_parameter(self, parameter): + body = { + "flavor": { + "name": "azAZ09. -_", + "ram": 512, + "vcpus": 2, + "disk": 1, + "OS-FLV-EXT-DATA:ephemeral": 1, + "id": unicode('1234'), + "swap": 512, + "rxtx_factor": 1, + "os-flavor-access:is_public": True, + } + } + + del body['flavor'][parameter] + + req = fakes.HTTPRequest.blank(self.base_url) + self.assertRaises(self.validation_error, self.controller._create, + req, body=body) + + def test_create_missing_name(self): + self._test_create_missing_parameter('name') + + def test_create_missing_ram(self): + self._test_create_missing_parameter('ram') + + def test_create_missing_vcpus(self): + self._test_create_missing_parameter('vcpus') + + def test_create_missing_disk(self): + self._test_create_missing_parameter('disk') + + def _create_flavor_success_case(self, body): + req = webob.Request.blank(self.base_url) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(200, res.status_code) + return jsonutils.loads(res.body) + + def test_create(self): + body = self._create_flavor_success_case(self.request_body) + for key in self.expected_flavor["flavor"]: + self.assertEqual(body["flavor"][key], + self.expected_flavor["flavor"][key]) + + def test_create_public_default(self): + del self.request_body['flavor']['os-flavor-access:is_public'] + body = self._create_flavor_success_case(self.request_body) + for key in self.expected_flavor["flavor"]: + self.assertEqual(body["flavor"][key], + self.expected_flavor["flavor"][key]) + + def test_create_without_flavorid(self): + del self.request_body['flavor']['id'] + body = self._create_flavor_success_case(self.request_body) + for key in self.expected_flavor["flavor"]: + self.assertEqual(body["flavor"][key], + self.expected_flavor["flavor"][key]) + + def _create_flavor_bad_request_case(self, body): + self.stubs.UnsetAll() + + req = webob.Request.blank(self.base_url) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(res.status_code, 400) + + def test_create_invalid_name(self): + self.request_body['flavor']['name'] = 'bad !@#!$% name' + self._create_flavor_bad_request_case(self.request_body) + + def test_create_flavor_name_is_whitespace(self): + self.request_body['flavor']['name'] = ' ' + self._create_flavor_bad_request_case(self.request_body) + + def test_create_with_name_too_long(self): + self.request_body['flavor']['name'] = 'a' * 256 + self._create_flavor_bad_request_case(self.request_body) + + def test_create_without_flavorname(self): + del self.request_body['flavor']['name'] + self._create_flavor_bad_request_case(self.request_body) + + def test_create_empty_body(self): + body = { + "flavor": {} + } + self._create_flavor_bad_request_case(body) + + def test_create_no_body(self): + body = {} + self._create_flavor_bad_request_case(body) + + def test_create_invalid_format_body(self): + body = { + "flavor": [] + } + self._create_flavor_bad_request_case(body) + + def test_create_invalid_flavorid(self): + self.request_body['flavor']['id'] = "!@#!$#!$^#&^$&" + self._create_flavor_bad_request_case(self.request_body) + + def test_create_check_flavor_id_length(self): + MAX_LENGTH = 255 + self.request_body['flavor']['id'] = "a" * (MAX_LENGTH + 1) + self._create_flavor_bad_request_case(self.request_body) + + def test_create_with_leading_trailing_whitespaces_in_flavor_id(self): + self.request_body['flavor']['id'] = " bad_id " + self._create_flavor_bad_request_case(self.request_body) + + def test_create_without_ram(self): + del self.request_body['flavor']['ram'] + self._create_flavor_bad_request_case(self.request_body) + + def test_create_with_0_ram(self): + self.request_body['flavor']['ram'] = 0 + self._create_flavor_bad_request_case(self.request_body) + + def test_create_without_vcpus(self): + del self.request_body['flavor']['vcpus'] + self._create_flavor_bad_request_case(self.request_body) + + def test_create_with_0_vcpus(self): + self.request_body['flavor']['vcpus'] = 0 + self._create_flavor_bad_request_case(self.request_body) + + def test_create_without_disk(self): + del self.request_body['flavor']['disk'] + self._create_flavor_bad_request_case(self.request_body) + + def test_create_with_minus_disk(self): + self.request_body['flavor']['disk'] = -1 + self._create_flavor_bad_request_case(self.request_body) + + def test_create_with_minus_ephemeral(self): + self.request_body['flavor']['OS-FLV-EXT-DATA:ephemeral'] = -1 + self._create_flavor_bad_request_case(self.request_body) + + def test_create_with_minus_swap(self): + self.request_body['flavor']['swap'] = -1 + self._create_flavor_bad_request_case(self.request_body) + + def test_create_with_minus_rxtx_factor(self): + self.request_body['flavor']['rxtx_factor'] = -1 + self._create_flavor_bad_request_case(self.request_body) + + def test_create_with_non_boolean_is_public(self): + self.request_body['flavor']['os-flavor-access:is_public'] = 123 + self._create_flavor_bad_request_case(self.request_body) + + def test_flavor_exists_exception_returns_409(self): + expected = { + "flavor": { + "name": "test", + "ram": 512, + "vcpus": 2, + "disk": 1, + "OS-FLV-EXT-DATA:ephemeral": 1, + "id": 1235, + "swap": 512, + "rxtx_factor": 1, + "os-flavor-access:is_public": True, + } + } + + def fake_create(name, memory_mb, vcpus, root_gb, ephemeral_gb, + flavorid, swap, rxtx_factor, is_public): + raise exception.FlavorExists(name=name) + + self.stubs.Set(flavors, "create", fake_create) + req = webob.Request.blank(self.base_url) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = jsonutils.dumps(expected) + res = req.get_response(self.app) + self.assertEqual(res.status_int, 409) + + @mock.patch('nova.compute.flavors.create', + side_effect=exception.FlavorCreateFailed) + def test_flavor_create_db_failed(self, mock_create): + request_dict = { + "flavor": { + "name": "test", + 'id': "12345", + "ram": 512, + "vcpus": 2, + "disk": 1, + "OS-FLV-EXT-DATA:ephemeral": 1, + "swap": 512, + "rxtx_factor": 1, + "os-flavor-access:is_public": True, + } + } + req = webob.Request.blank(self.base_url) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = jsonutils.dumps(request_dict) + res = req.get_response(self.app) + self.assertEqual(res.status_int, 500) + self.assertIn('Unable to create flavor', res.body) + + def test_invalid_memory_mb(self): + """Check negative and decimal number can't be accepted.""" + + self.stubs.UnsetAll() + self.assertRaises(exception.InvalidInput, flavors.create, "abc", + -512, 2, 1, 1, 1234, 512, 1, True) + self.assertRaises(exception.InvalidInput, flavors.create, "abcd", + 512.2, 2, 1, 1, 1234, 512, 1, True) + self.assertRaises(exception.InvalidInput, flavors.create, "abcde", + None, 2, 1, 1, 1234, 512, 1, True) + self.assertRaises(exception.InvalidInput, flavors.create, "abcdef", + 512, 2, None, 1, 1234, 512, 1, True) + self.assertRaises(exception.InvalidInput, flavors.create, "abcdef", + "test_memory_mb", 2, None, 1, 1234, 512, 1, True) + + +class FakeRequest(object): + environ = {"nova.context": context.get_admin_context()} + + +class PrivateFlavorManageTestV21(test.TestCase): + controller = flavormanage_v21.FlavorManageController() + base_url = '/v2/fake/flavors' + + def setUp(self): + super(PrivateFlavorManageTestV21, self).setUp() + self.flavor_access_controller = flavor_access.FlavorAccessController() + self.ctxt = context.RequestContext('fake', 'fake', + is_admin=True, auth_token=True) + self.app = self._setup_app() + self.expected = { + "flavor": { + "name": "test", + "ram": 512, + "vcpus": 2, + "disk": 1, + "OS-FLV-EXT-DATA:ephemeral": 1, + "swap": 512, + "rxtx_factor": 1 + } + } + + def _setup_app(self): + return fakes.wsgi_app_v21(init_only=('flavor-manage', + 'os-flavor-access', + 'os-flavor-rxtx', 'flavors', + 'os-flavor-extra-data'), + fake_auth_context=self.ctxt) + + def _get_response(self): + req = webob.Request.blank(self.base_url) + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = jsonutils.dumps(self.expected) + res = req.get_response(self.app) + return jsonutils.loads(res.body) + + def test_create_private_flavor_should_not_grant_flavor_access(self): + self.expected["flavor"]["os-flavor-access:is_public"] = False + body = self._get_response() + for key in self.expected["flavor"]: + self.assertEqual(body["flavor"][key], self.expected["flavor"][key]) + flavor_access_body = self.flavor_access_controller.index( + FakeRequest(), body["flavor"]["id"]) + expected_flavor_access_body = { + "tenant_id": "%s" % self.ctxt.project_id, + "flavor_id": "%s" % body["flavor"]["id"] + } + self.assertNotIn(expected_flavor_access_body, + flavor_access_body["flavor_access"]) + + def test_create_public_flavor_should_not_create_flavor_access(self): + self.expected["flavor"]["os-flavor-access:is_public"] = True + self.mox.StubOutWithMock(flavors, "add_flavor_access") + self.mox.ReplayAll() + body = self._get_response() + for key in self.expected["flavor"]: + self.assertEqual(body["flavor"][key], self.expected["flavor"][key]) + + +class FlavorManageTestV2(FlavorManageTestV21): + controller = flavormanage_v2.FlavorManageController() + validation_error = webob.exc.HTTPBadRequest + + def setUp(self): + super(FlavorManageTestV2, self).setUp() + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Flavormanage', 'Flavorextradata', + 'Flavor_access', 'Flavor_rxtx', 'Flavor_swap']) + + def _setup_app(self): + return fakes.wsgi_app(init_only=('flavors',), + fake_auth_context=self.ctxt) + + +class PrivateFlavorManageTestV2(PrivateFlavorManageTestV21): + controller = flavormanage_v2.FlavorManageController() + + def setUp(self): + super(PrivateFlavorManageTestV2, self).setUp() + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Flavormanage', 'Flavorextradata', + 'Flavor_access', 'Flavor_rxtx', 'Flavor_swap']) + + def _setup_app(self): + return fakes.wsgi_app(init_only=('flavors',), + fake_auth_context=self.ctxt) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavor_rxtx.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_rxtx.py new file mode 100644 index 0000000000..a8f31653c1 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_rxtx.py @@ -0,0 +1,127 @@ +# Copyright 2012 Nebula, 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.compute import flavors +from nova import test +from nova.tests.unit.api.openstack import fakes + +FAKE_FLAVORS = { + 'flavor 1': { + "flavorid": '1', + "name": 'flavor 1', + "memory_mb": '256', + "root_gb": '10', + "swap": '5', + "disabled": False, + "ephemeral_gb": '20', + "rxtx_factor": '1.0', + "vcpus": 1, + }, + 'flavor 2': { + "flavorid": '2', + "name": 'flavor 2', + "memory_mb": '512', + "root_gb": '10', + "swap": '10', + "ephemeral_gb": '25', + "rxtx_factor": None, + "disabled": False, + "vcpus": 1, + }, +} + + +def fake_flavor_get_by_flavor_id(flavorid, ctxt=None): + return FAKE_FLAVORS['flavor %s' % flavorid] + + +def fake_get_all_flavors_sorted_list(context=None, inactive=False, + filters=None, sort_key='flavorid', + sort_dir='asc', limit=None, marker=None): + return [ + fake_flavor_get_by_flavor_id(1), + fake_flavor_get_by_flavor_id(2) + ] + + +class FlavorRxtxTestV21(test.NoDBTestCase): + content_type = 'application/json' + _prefix = "/v2/fake" + + def setUp(self): + super(FlavorRxtxTestV21, self).setUp() + ext = ('nova.api.openstack.compute.contrib' + '.flavor_rxtx.Flavor_rxtx') + self.flags(osapi_compute_extension=[ext]) + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(flavors, "get_all_flavors_sorted_list", + fake_get_all_flavors_sorted_list) + self.stubs.Set(flavors, + "get_flavor_by_flavor_id", + fake_flavor_get_by_flavor_id) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(self._get_app()) + return res + + def _get_app(self): + return fakes.wsgi_app_v21(init_only=('servers', + 'flavors', 'os-flavor-rxtx')) + + def _get_flavor(self, body): + return jsonutils.loads(body).get('flavor') + + def _get_flavors(self, body): + return jsonutils.loads(body).get('flavors') + + def assertFlavorRxtx(self, flavor, rxtx): + self.assertEqual(str(flavor.get('rxtx_factor')), rxtx) + + def test_show(self): + url = self._prefix + '/flavors/1' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + self.assertFlavorRxtx(self._get_flavor(res.body), '1.0') + + def test_detail(self): + url = self._prefix + '/flavors/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + flavors = self._get_flavors(res.body) + self.assertFlavorRxtx(flavors[0], '1.0') + self.assertFlavorRxtx(flavors[1], '') + + +class FlavorRxtxTestV20(FlavorRxtxTestV21): + + def _get_app(self): + return fakes.wsgi_app() + + +class FlavorRxtxXmlTest(FlavorRxtxTestV20): + content_type = 'application/xml' + + def _get_flavor(self, body): + return etree.XML(body) + + def _get_flavors(self, body): + return etree.XML(body).getchildren() diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavor_swap.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_swap.py new file mode 100644 index 0000000000..f168db060a --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavor_swap.py @@ -0,0 +1,126 @@ +# Copyright 2012 Nebula, 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.compute import flavors +from nova import test +from nova.tests.unit.api.openstack import fakes + +FAKE_FLAVORS = { + 'flavor 1': { + "flavorid": '1', + "name": 'flavor 1', + "memory_mb": '256', + "root_gb": '10', + "swap": 512, + "vcpus": 1, + "ephemeral_gb": 1, + "disabled": False, + }, + 'flavor 2': { + "flavorid": '2', + "name": 'flavor 2', + "memory_mb": '512', + "root_gb": '10', + "swap": None, + "vcpus": 1, + "ephemeral_gb": 1, + "disabled": False, + }, +} + + +# TODO(jogo) dedup these across nova.api.openstack.contrib.test_flavor* +def fake_flavor_get_by_flavor_id(flavorid, ctxt=None): + return FAKE_FLAVORS['flavor %s' % flavorid] + + +def fake_get_all_flavors_sorted_list(context=None, inactive=False, + filters=None, sort_key='flavorid', + sort_dir='asc', limit=None, marker=None): + return [ + fake_flavor_get_by_flavor_id(1), + fake_flavor_get_by_flavor_id(2) + ] + + +class FlavorSwapTestV21(test.NoDBTestCase): + base_url = '/v2/fake/flavors' + content_type = 'application/json' + prefix = '' + + def setUp(self): + super(FlavorSwapTestV21, self).setUp() + ext = ('nova.api.openstack.compute.contrib' + '.flavor_swap.Flavor_swap') + self.flags(osapi_compute_extension=[ext]) + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(flavors, "get_all_flavors_sorted_list", + fake_get_all_flavors_sorted_list) + self.stubs.Set(flavors, + "get_flavor_by_flavor_id", + fake_flavor_get_by_flavor_id) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app_v21(init_only=('flavors'))) + return res + + def _get_flavor(self, body): + return jsonutils.loads(body).get('flavor') + + def _get_flavors(self, body): + return jsonutils.loads(body).get('flavors') + + def assertFlavorSwap(self, flavor, swap): + self.assertEqual(str(flavor.get('%sswap' % self.prefix)), swap) + + def test_show(self): + url = self.base_url + '/1' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + self.assertFlavorSwap(self._get_flavor(res.body), '512') + + def test_detail(self): + url = self.base_url + '/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + flavors = self._get_flavors(res.body) + self.assertFlavorSwap(flavors[0], '512') + self.assertFlavorSwap(flavors[1], '') + + +class FlavorSwapTestV2(FlavorSwapTestV21): + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app()) + return res + + +class FlavorSwapXmlTest(FlavorSwapTestV2): + content_type = 'application/xml' + + def _get_flavor(self, body): + return etree.XML(body) + + def _get_flavors(self, body): + return etree.XML(body).getchildren() diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavorextradata.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavorextradata.py new file mode 100644 index 0000000000..1299b6c88d --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavorextradata.py @@ -0,0 +1,127 @@ +# 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 datetime + +from oslo.serialization import jsonutils +import webob + +from nova.compute import flavors +from nova import test +from nova.tests.unit.api.openstack import fakes + + +def fake_get_flavor_by_flavor_id(flavorid, ctxt=None): + return { + 'id': flavorid, + 'flavorid': str(flavorid), + 'root_gb': 1, + 'ephemeral_gb': 1, + 'name': u'test', + 'deleted': False, + 'created_at': datetime.datetime(2012, 1, 1, 1, 1, 1, 1), + 'updated_at': None, + 'memory_mb': 512, + 'vcpus': 1, + 'extra_specs': {}, + 'deleted_at': None, + 'vcpu_weight': None, + 'swap': 0, + 'disabled': False, + } + + +def fake_get_all_flavors_sorted_list(context=None, inactive=False, + filters=None, sort_key='flavorid', + sort_dir='asc', limit=None, marker=None): + return [ + fake_get_flavor_by_flavor_id(1), + fake_get_flavor_by_flavor_id(2) + ] + + +class FlavorExtraDataTestV21(test.NoDBTestCase): + base_url = '/v2/fake/flavors' + + def setUp(self): + super(FlavorExtraDataTestV21, self).setUp() + ext = ('nova.api.openstack.compute.contrib' + '.flavorextradata.Flavorextradata') + self.flags(osapi_compute_extension=[ext]) + self.stubs.Set(flavors, 'get_flavor_by_flavor_id', + fake_get_flavor_by_flavor_id) + self.stubs.Set(flavors, 'get_all_flavors_sorted_list', + fake_get_all_flavors_sorted_list) + self._setup_app() + + def _setup_app(self): + self.app = fakes.wsgi_app_v21(init_only=('flavors')) + + def _verify_flavor_response(self, flavor, expected): + for key in expected: + self.assertEqual(flavor[key], expected[key]) + + def test_show(self): + expected = { + 'flavor': { + 'id': '1', + 'name': 'test', + 'ram': 512, + 'vcpus': 1, + 'disk': 1, + 'OS-FLV-EXT-DATA:ephemeral': 1, + } + } + + url = self.base_url + '/1' + req = webob.Request.blank(url) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + body = jsonutils.loads(res.body) + self._verify_flavor_response(body['flavor'], expected['flavor']) + + def test_detail(self): + expected = [ + { + 'id': '1', + 'name': 'test', + 'ram': 512, + 'vcpus': 1, + 'disk': 1, + 'OS-FLV-EXT-DATA:ephemeral': 1, + }, + { + 'id': '2', + 'name': 'test', + 'ram': 512, + 'vcpus': 1, + 'disk': 1, + 'OS-FLV-EXT-DATA:ephemeral': 1, + }, + ] + + url = self.base_url + '/detail' + req = webob.Request.blank(url) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + body = jsonutils.loads(res.body) + for i, flavor in enumerate(body['flavors']): + self._verify_flavor_response(flavor, expected[i]) + + +class FlavorExtraDataTestV2(FlavorExtraDataTestV21): + + def _setup_app(self): + self.app = fakes.wsgi_app(init_only=('flavors',)) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_flavors_extra_specs.py b/nova/tests/unit/api/openstack/compute/contrib/test_flavors_extra_specs.py new file mode 100644 index 0000000000..8a6f4814a8 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_flavors_extra_specs.py @@ -0,0 +1,403 @@ +# Copyright 2011 University of Southern California +# 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 mock +import webob + +from nova.api.openstack.compute.contrib import flavorextraspecs \ + as flavorextraspecs_v2 +from nova.api.openstack.compute.plugins.v3 import flavors_extraspecs \ + as flavorextraspecs_v21 +import nova.db +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit.objects import test_flavor + + +def return_create_flavor_extra_specs(context, flavor_id, extra_specs): + return stub_flavor_extra_specs() + + +def return_flavor_extra_specs(context, flavor_id): + return stub_flavor_extra_specs() + + +def return_flavor_extra_specs_item(context, flavor_id, key): + return {key: stub_flavor_extra_specs()[key]} + + +def return_empty_flavor_extra_specs(context, flavor_id): + return {} + + +def delete_flavor_extra_specs(context, flavor_id, key): + pass + + +def stub_flavor_extra_specs(): + specs = { + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5"} + return specs + + +class FlavorsExtraSpecsTestV21(test.TestCase): + bad_request = exception.ValidationError + flavorextraspecs = flavorextraspecs_v21 + + def _get_request(self, url, use_admin_context=False): + req_url = '/v2/fake/flavors/' + url + return fakes.HTTPRequest.blank(req_url, + use_admin_context=use_admin_context) + + def setUp(self): + super(FlavorsExtraSpecsTestV21, self).setUp() + fakes.stub_out_key_pair_funcs(self.stubs) + self.controller = self.flavorextraspecs.FlavorExtraSpecsController() + + def test_index(self): + flavor = dict(test_flavor.fake_flavor, + extra_specs={'key1': 'value1'}) + + req = self._get_request('1/os-extra_specs') + with mock.patch('nova.db.flavor_get_by_flavor_id') as mock_get: + mock_get.return_value = flavor + res_dict = self.controller.index(req, 1) + + self.assertEqual('value1', res_dict['extra_specs']['key1']) + + def test_index_no_data(self): + self.stubs.Set(nova.db, 'flavor_extra_specs_get', + return_empty_flavor_extra_specs) + + req = self._get_request('1/os-extra_specs') + res_dict = self.controller.index(req, 1) + + self.assertEqual(0, len(res_dict['extra_specs'])) + + def test_show(self): + flavor = dict(test_flavor.fake_flavor, + extra_specs={'key5': 'value5'}) + req = self._get_request('1/os-extra_specs/key5') + with mock.patch('nova.db.flavor_get_by_flavor_id') as mock_get: + mock_get.return_value = flavor + res_dict = self.controller.show(req, 1, 'key5') + + self.assertEqual('value5', res_dict['key5']) + + def test_show_spec_not_found(self): + self.stubs.Set(nova.db, 'flavor_extra_specs_get', + return_empty_flavor_extra_specs) + + req = self._get_request('1/os-extra_specs/key6') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, 1, 'key6') + + def test_not_found_because_flavor(self): + req = self._get_request('1/os-extra_specs/key5', + use_admin_context=True) + with mock.patch('nova.db.flavor_get_by_flavor_id') as mock_get: + mock_get.side_effect = exception.FlavorNotFound(flavor_id='1') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, 1, 'key5') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.update, + req, 1, 'key5', body={'key5': 'value5'}) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, 1, 'key5') + + req = self._get_request('1/os-extra_specs', use_admin_context=True) + with mock.patch('nova.db.flavor_get_by_flavor_id') as mock_get: + mock_get.side_effect = exception.FlavorNotFound(flavor_id='1') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.create, + req, 1, body={'extra_specs': {'key5': 'value5'}}) + + def test_delete(self): + flavor = dict(test_flavor.fake_flavor, + extra_specs={'key5': 'value5'}) + self.stubs.Set(nova.db, 'flavor_extra_specs_delete', + delete_flavor_extra_specs) + + req = self._get_request('1/os-extra_specs/key5', + use_admin_context=True) + with mock.patch('nova.db.flavor_get_by_flavor_id') as mock_get: + mock_get.return_value = flavor + self.controller.delete(req, 1, 'key5') + + def test_delete_no_admin(self): + self.stubs.Set(nova.db, 'flavor_extra_specs_delete', + delete_flavor_extra_specs) + + req = self._get_request('1/os-extra_specs/key5') + self.assertRaises(exception.Forbidden, self.controller.delete, + req, 1, 'key 5') + + def test_delete_spec_not_found(self): + req = self._get_request('1/os-extra_specs/key6', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, 1, 'key6') + + def test_create(self): + self.stubs.Set(nova.db, + 'flavor_extra_specs_update_or_create', + return_create_flavor_extra_specs) + body = {"extra_specs": {"key1": "value1", "key2": 0.5, "key3": 5}} + + req = self._get_request('1/os-extra_specs', use_admin_context=True) + res_dict = self.controller.create(req, 1, body=body) + + self.assertEqual('value1', res_dict['extra_specs']['key1']) + self.assertEqual(0.5, res_dict['extra_specs']['key2']) + self.assertEqual(5, res_dict['extra_specs']['key3']) + + def test_create_no_admin(self): + self.stubs.Set(nova.db, + 'flavor_extra_specs_update_or_create', + return_create_flavor_extra_specs) + body = {"extra_specs": {"key1": "value1"}} + + req = self._get_request('1/os-extra_specs') + self.assertRaises(exception.Forbidden, self.controller.create, + req, 1, body=body) + + def test_create_flavor_not_found(self): + def fake_instance_type_extra_specs_update_or_create(*args, **kwargs): + raise exception.FlavorNotFound(flavor_id='') + + self.stubs.Set(nova.db, + 'flavor_extra_specs_update_or_create', + fake_instance_type_extra_specs_update_or_create) + body = {"extra_specs": {"key1": "value1"}} + req = self._get_request('1/os-extra_specs', use_admin_context=True) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.create, + req, 1, body=body) + + def test_create_flavor_db_duplicate(self): + def fake_instance_type_extra_specs_update_or_create(*args, **kwargs): + raise exception.FlavorExtraSpecUpdateCreateFailed(id=1, retries=5) + + self.stubs.Set(nova.db, + 'flavor_extra_specs_update_or_create', + fake_instance_type_extra_specs_update_or_create) + body = {"extra_specs": {"key1": "value1"}} + req = self._get_request('1/os-extra_specs', use_admin_context=True) + self.assertRaises(webob.exc.HTTPConflict, self.controller.create, + req, 1, body=body) + + def _test_create_bad_request(self, body): + self.stubs.Set(nova.db, + 'flavor_extra_specs_update_or_create', + return_create_flavor_extra_specs) + + req = self._get_request('1/os-extra_specs', use_admin_context=True) + self.assertRaises(self.bad_request, self.controller.create, + req, 1, body=body) + + def test_create_empty_body(self): + self._test_create_bad_request('') + + def test_create_non_dict_extra_specs(self): + self._test_create_bad_request({"extra_specs": "non_dict"}) + + def test_create_non_string_key(self): + self._test_create_bad_request({"extra_specs": {None: "value1"}}) + + def test_create_non_string_value(self): + self._test_create_bad_request({"extra_specs": {"key1": None}}) + + def test_create_zero_length_key(self): + self._test_create_bad_request({"extra_specs": {"": "value1"}}) + + def test_create_long_key(self): + key = "a" * 256 + self._test_create_bad_request({"extra_specs": {key: "value1"}}) + + def test_create_long_value(self): + value = "a" * 256 + self._test_create_bad_request({"extra_specs": {"key1": value}}) + + @mock.patch('nova.db.flavor_extra_specs_update_or_create') + def test_create_really_long_integer_value(self, mock_flavor_extra_specs): + value = 10 ** 1000 + mock_flavor_extra_specs.side_effects = return_create_flavor_extra_specs + + req = self._get_request('1/os-extra_specs', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, 1, body={"extra_specs": {"key1": value}}) + + @mock.patch('nova.db.flavor_extra_specs_update_or_create') + def test_create_invalid_specs_key(self, mock_flavor_extra_specs): + invalid_keys = ("key1/", "<key>", "$$akey$", "!akey", "") + mock_flavor_extra_specs.side_effects = return_create_flavor_extra_specs + + for key in invalid_keys: + body = {"extra_specs": {key: "value1"}} + req = self._get_request('1/os-extra_specs', use_admin_context=True) + self.assertRaises(self.bad_request, self.controller.create, + req, 1, body=body) + + @mock.patch('nova.db.flavor_extra_specs_update_or_create') + def test_create_valid_specs_key(self, mock_flavor_extra_specs): + valid_keys = ("key1", "month.price", "I_am-a Key", "finance:g2") + mock_flavor_extra_specs.side_effects = return_create_flavor_extra_specs + + for key in valid_keys: + body = {"extra_specs": {key: "value1"}} + req = self._get_request('1/os-extra_specs', use_admin_context=True) + res_dict = self.controller.create(req, 1, body=body) + self.assertEqual('value1', res_dict['extra_specs'][key]) + + def test_update_item(self): + self.stubs.Set(nova.db, + 'flavor_extra_specs_update_or_create', + return_create_flavor_extra_specs) + body = {"key1": "value1"} + + req = self._get_request('1/os-extra_specs/key1', + use_admin_context=True) + res_dict = self.controller.update(req, 1, 'key1', body=body) + + self.assertEqual('value1', res_dict['key1']) + + def test_update_item_no_admin(self): + self.stubs.Set(nova.db, + 'flavor_extra_specs_update_or_create', + return_create_flavor_extra_specs) + body = {"key1": "value1"} + + req = self._get_request('1/os-extra_specs/key1') + self.assertRaises(exception.Forbidden, self.controller.update, + req, 1, 'key1', body=body) + + def _test_update_item_bad_request(self, body): + self.stubs.Set(nova.db, + 'flavor_extra_specs_update_or_create', + return_create_flavor_extra_specs) + + req = self._get_request('1/os-extra_specs/key1', + use_admin_context=True) + self.assertRaises(self.bad_request, self.controller.update, + req, 1, 'key1', body=body) + + def test_update_item_empty_body(self): + self._test_update_item_bad_request('') + + def test_update_item_too_many_keys(self): + body = {"key1": "value1", "key2": "value2"} + self._test_update_item_bad_request(body) + + def test_update_item_non_dict_extra_specs(self): + self._test_update_item_bad_request("non_dict") + + def test_update_item_non_string_key(self): + self._test_update_item_bad_request({None: "value1"}) + + def test_update_item_non_string_value(self): + self._test_update_item_bad_request({"key1": None}) + + def test_update_item_zero_length_key(self): + self._test_update_item_bad_request({"": "value1"}) + + def test_update_item_long_key(self): + key = "a" * 256 + self._test_update_item_bad_request({key: "value1"}) + + def test_update_item_long_value(self): + value = "a" * 256 + self._test_update_item_bad_request({"key1": value}) + + def test_update_item_body_uri_mismatch(self): + self.stubs.Set(nova.db, + 'flavor_extra_specs_update_or_create', + return_create_flavor_extra_specs) + body = {"key1": "value1"} + + req = self._get_request('1/os-extra_specs/bad', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 1, 'bad', body=body) + + def test_update_flavor_not_found(self): + def fake_instance_type_extra_specs_update_or_create(*args, **kwargs): + raise exception.FlavorNotFound(flavor_id='') + + self.stubs.Set(nova.db, + 'flavor_extra_specs_update_or_create', + fake_instance_type_extra_specs_update_or_create) + body = {"key1": "value1"} + + req = self._get_request('1/os-extra_specs/key1', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.update, + req, 1, 'key1', body=body) + + def test_update_flavor_db_duplicate(self): + def fake_instance_type_extra_specs_update_or_create(*args, **kwargs): + raise exception.FlavorExtraSpecUpdateCreateFailed(id=1, retries=5) + + self.stubs.Set(nova.db, + 'flavor_extra_specs_update_or_create', + fake_instance_type_extra_specs_update_or_create) + body = {"key1": "value1"} + + req = self._get_request('1/os-extra_specs/key1', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPConflict, self.controller.update, + req, 1, 'key1', body=body) + + def test_update_really_long_integer_value(self): + value = 10 ** 1000 + self.stubs.Set(nova.db, + 'flavor_extra_specs_update_or_create', + return_create_flavor_extra_specs) + + req = self._get_request('1/os-extra_specs/key1', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 1, 'key1', body={"key1": value}) + + +class FlavorsExtraSpecsTestV2(FlavorsExtraSpecsTestV21): + bad_request = webob.exc.HTTPBadRequest + flavorextraspecs = flavorextraspecs_v2 + + +class FlavorsExtraSpecsXMLSerializerTest(test.TestCase): + def test_serializer(self): + serializer = flavorextraspecs_v2.ExtraSpecsTemplate() + expected = ("<?xml version='1.0' encoding='UTF-8'?>\n" + '<extra_specs><key1>value1</key1></extra_specs>') + text = serializer.serialize(dict(extra_specs={"key1": "value1"})) + self.assertEqual(text, expected) + + def test_show_update_serializer(self): + serializer = flavorextraspecs_v2.ExtraSpecTemplate() + expected = ("<?xml version='1.0' encoding='UTF-8'?>\n" + '<extra_spec key="key1">value1</extra_spec>') + text = serializer.serialize(dict({"key1": "value1"})) + self.assertEqual(text, expected) + + def test_serializer_with_colon_tagname(self): + # Our test object to serialize + obj = {'extra_specs': {'foo:bar': '999'}} + serializer = flavorextraspecs_v2.ExtraSpecsTemplate() + expected_xml = (("<?xml version='1.0' encoding='UTF-8'?>\n" + '<extra_specs><foo:bar xmlns:foo="foo">999</foo:bar>' + '</extra_specs>')) + result = serializer.serialize(obj) + self.assertEqual(expected_xml, result) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_dns.py b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_dns.py new file mode 100644 index 0000000000..9a68e0de60 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_dns.py @@ -0,0 +1,412 @@ +# Copyright 2011 Andrew Bogott for the Wikimedia Foundation +# All Rights Reserved. +# Copyright 2013 Red Hat, 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 urllib + +from lxml import etree +import webob + +from nova.api.openstack.compute.contrib import floating_ip_dns as fipdns_v2 +from nova.api.openstack.compute.plugins.v3 import floating_ip_dns as \ + fipdns_v21 +from nova import context +from nova import db +from nova import exception +from nova import network +from nova import test +from nova.tests.unit.api.openstack import fakes + + +name = "arbitraryname" +name2 = "anotherarbitraryname" + +test_ipv4_address = '10.0.0.66' +test_ipv4_address2 = '10.0.0.67' + +test_ipv6_address = 'fe80:0:0:0:0:0:a00:42' + +domain = "example.org" +domain2 = "example.net" +floating_ip_id = '1' + + +def _quote_domain(domain): + """Domain names tend to have .'s in them. Urllib doesn't quote dots, + but Routes tends to choke on them, so we need an extra level of + by-hand quoting here. This function needs to duplicate the one in + python-novaclient/novaclient/v1_1/floating_ip_dns.py + """ + return urllib.quote(domain.replace('.', '%2E')) + + +def network_api_get_floating_ip(self, context, id): + return {'id': floating_ip_id, 'address': test_ipv4_address, + 'fixed_ip': None} + + +def network_get_dns_domains(self, context): + return [{'domain': 'example.org', 'scope': 'public'}, + {'domain': 'example.com', 'scope': 'public', + 'project': 'project1'}, + {'domain': 'private.example.com', 'scope': 'private', + 'availability_zone': 'avzone'}] + + +def network_get_dns_entries_by_address(self, context, address, domain): + return [name, name2] + + +def network_get_dns_entries_by_name(self, context, address, domain): + return [test_ipv4_address] + + +def network_add_dns_entry(self, context, address, name, dns_type, domain): + return {'dns_entry': {'ip': test_ipv4_address, + 'name': name, + 'type': dns_type, + 'domain': domain}} + + +def network_modify_dns_entry(self, context, address, name, domain): + return {'dns_entry': {'name': name, + 'ip': address, + 'domain': domain}} + + +def network_create_private_dns_domain(self, context, domain, avail_zone): + pass + + +def network_create_public_dns_domain(self, context, domain, project): + pass + + +class FloatingIpDNSTestV21(test.TestCase): + floating_ip_dns = fipdns_v21 + + def _create_floating_ip(self): + """Create a floating ip object.""" + host = "fake_host" + db.floating_ip_create(self.context, + {'address': test_ipv4_address, + 'host': host}) + db.floating_ip_create(self.context, + {'address': test_ipv6_address, + 'host': host}) + + def _delete_floating_ip(self): + db.floating_ip_destroy(self.context, test_ipv4_address) + db.floating_ip_destroy(self.context, test_ipv6_address) + + def _check_status(self, expected_status, res, controller_methord): + self.assertEqual(expected_status, controller_methord.wsgi_code) + + def _bad_request(self): + return webob.exc.HTTPBadRequest + + def setUp(self): + super(FloatingIpDNSTestV21, self).setUp() + self.stubs.Set(network.api.API, "get_dns_domains", + network_get_dns_domains) + self.stubs.Set(network.api.API, "get_dns_entries_by_address", + network_get_dns_entries_by_address) + self.stubs.Set(network.api.API, "get_dns_entries_by_name", + network_get_dns_entries_by_name) + self.stubs.Set(network.api.API, "get_floating_ip", + network_api_get_floating_ip) + self.stubs.Set(network.api.API, "add_dns_entry", + network_add_dns_entry) + self.stubs.Set(network.api.API, "modify_dns_entry", + network_modify_dns_entry) + self.stubs.Set(network.api.API, "create_public_dns_domain", + network_create_public_dns_domain) + self.stubs.Set(network.api.API, "create_private_dns_domain", + network_create_private_dns_domain) + + self.context = context.get_admin_context() + + self._create_floating_ip() + temp = self.floating_ip_dns.FloatingIPDNSDomainController() + self.domain_controller = temp + self.entry_controller = self.floating_ip_dns.\ + FloatingIPDNSEntryController() + + def tearDown(self): + self._delete_floating_ip() + super(FloatingIpDNSTestV21, self).tearDown() + + def test_dns_domains_list(self): + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns') + res_dict = self.domain_controller.index(req) + entries = res_dict['domain_entries'] + self.assertTrue(entries) + self.assertEqual(entries[0]['domain'], "example.org") + self.assertFalse(entries[0]['project']) + self.assertFalse(entries[0]['availability_zone']) + self.assertEqual(entries[1]['domain'], "example.com") + self.assertEqual(entries[1]['project'], "project1") + self.assertFalse(entries[1]['availability_zone']) + self.assertEqual(entries[2]['domain'], "private.example.com") + self.assertFalse(entries[2]['project']) + self.assertEqual(entries[2]['availability_zone'], "avzone") + + def _test_get_dns_entries_by_address(self, address): + + qparams = {'ip': address} + params = "?%s" % urllib.urlencode(qparams) if qparams else "" + + req = fakes.HTTPRequest.blank( + '/v2/123/os-floating-ip-dns/%s/entries/%s' + % (_quote_domain(domain), params)) + entries = self.entry_controller.show(req, _quote_domain(domain), + address) + entries = entries.obj + self.assertEqual(len(entries['dns_entries']), 2) + self.assertEqual(entries['dns_entries'][0]['name'], + name) + self.assertEqual(entries['dns_entries'][1]['name'], + name2) + self.assertEqual(entries['dns_entries'][0]['domain'], + domain) + + def test_get_dns_entries_by_ipv4_address(self): + self._test_get_dns_entries_by_address(test_ipv4_address) + + def test_get_dns_entries_by_ipv6_address(self): + self._test_get_dns_entries_by_address(test_ipv6_address) + + def test_get_dns_entries_by_name(self): + req = fakes.HTTPRequest.blank( + '/v2/123/os-floating-ip-dns/%s/entries/%s' % + (_quote_domain(domain), name)) + entry = self.entry_controller.show(req, _quote_domain(domain), name) + + self.assertEqual(entry['dns_entry']['ip'], + test_ipv4_address) + self.assertEqual(entry['dns_entry']['domain'], + domain) + + def test_dns_entries_not_found(self): + def fake_get_dns_entries_by_name(self, context, address, domain): + raise webob.exc.HTTPNotFound() + + self.stubs.Set(network.api.API, "get_dns_entries_by_name", + fake_get_dns_entries_by_name) + + req = fakes.HTTPRequest.blank( + '/v2/123/os-floating-ip-dns/%s/entries/%s' % + (_quote_domain(domain), 'nonexistent')) + self.assertRaises(webob.exc.HTTPNotFound, + self.entry_controller.show, + req, _quote_domain(domain), 'nonexistent') + + def test_create_entry(self): + body = {'dns_entry': + {'ip': test_ipv4_address, + 'dns_type': 'A'}} + req = fakes.HTTPRequest.blank( + '/v2/123/os-floating-ip-dns/%s/entries/%s' % + (_quote_domain(domain), name)) + entry = self.entry_controller.update(req, _quote_domain(domain), + name, body=body) + self.assertEqual(entry['dns_entry']['ip'], test_ipv4_address) + + def test_create_domain(self): + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s' % + _quote_domain(domain)) + body = {'domain_entry': + {'scope': 'private', + 'project': 'testproject'}} + self.assertRaises(self._bad_request(), + self.domain_controller.update, + req, _quote_domain(domain), body=body) + + body = {'domain_entry': + {'scope': 'public', + 'availability_zone': 'zone1'}} + self.assertRaises(self._bad_request(), + self.domain_controller.update, + req, _quote_domain(domain), body=body) + + body = {'domain_entry': + {'scope': 'public', + 'project': 'testproject'}} + entry = self.domain_controller.update(req, _quote_domain(domain), + body=body) + self.assertEqual(entry['domain_entry']['domain'], domain) + self.assertEqual(entry['domain_entry']['scope'], 'public') + self.assertEqual(entry['domain_entry']['project'], 'testproject') + + body = {'domain_entry': + {'scope': 'private', + 'availability_zone': 'zone1'}} + entry = self.domain_controller.update(req, _quote_domain(domain), + body=body) + self.assertEqual(entry['domain_entry']['domain'], domain) + self.assertEqual(entry['domain_entry']['scope'], 'private') + self.assertEqual(entry['domain_entry']['availability_zone'], 'zone1') + + def test_delete_entry(self): + calls = [] + + def network_delete_dns_entry(fakeself, context, name, domain): + calls.append((name, domain)) + + self.stubs.Set(network.api.API, "delete_dns_entry", + network_delete_dns_entry) + + req = fakes.HTTPRequest.blank( + '/v2/123/os-floating-ip-dns/%s/entries/%s' % + (_quote_domain(domain), name)) + res = self.entry_controller.delete(req, _quote_domain(domain), name) + + self._check_status(202, res, self.entry_controller.delete) + self.assertEqual([(name, domain)], calls) + + def test_delete_entry_notfound(self): + def delete_dns_entry_notfound(fakeself, context, name, domain): + raise exception.NotFound + + self.stubs.Set(network.api.API, "delete_dns_entry", + delete_dns_entry_notfound) + + req = fakes.HTTPRequest.blank( + '/v2/123/os-floating-ip-dns/%s/entries/%s' % + (_quote_domain(domain), name)) + self.assertRaises(webob.exc.HTTPNotFound, + self.entry_controller.delete, req, _quote_domain(domain), name) + + def test_delete_domain(self): + calls = [] + + def network_delete_dns_domain(fakeself, context, fqdomain): + calls.append(fqdomain) + + self.stubs.Set(network.api.API, "delete_dns_domain", + network_delete_dns_domain) + + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s' % + _quote_domain(domain)) + res = self.domain_controller.delete(req, _quote_domain(domain)) + + self._check_status(202, res, self.domain_controller.delete) + self.assertEqual([domain], calls) + + def test_delete_domain_notfound(self): + def delete_dns_domain_notfound(fakeself, context, fqdomain): + raise exception.NotFound + + self.stubs.Set(network.api.API, "delete_dns_domain", + delete_dns_domain_notfound) + + req = fakes.HTTPRequest.blank('/v2/123/os-floating-ip-dns/%s' % + _quote_domain(domain)) + self.assertRaises(webob.exc.HTTPNotFound, + self.domain_controller.delete, req, _quote_domain(domain)) + + def test_modify(self): + body = {'dns_entry': + {'ip': test_ipv4_address2, + 'dns_type': 'A'}} + req = fakes.HTTPRequest.blank( + '/v2/123/os-floating-ip-dns/%s/entries/%s' % (domain, name)) + entry = self.entry_controller.update(req, domain, name, body=body) + + self.assertEqual(entry['dns_entry']['ip'], test_ipv4_address2) + + +class FloatingIpDNSTestV2(FloatingIpDNSTestV21): + floating_ip_dns = fipdns_v2 + + def _check_status(self, expected_status, res, controller_methord): + self.assertEqual(expected_status, res.status_int) + + def _bad_request(self): + return webob.exc.HTTPUnprocessableEntity + + +class FloatingIpDNSSerializerTestV2(test.TestCase): + floating_ip_dns = fipdns_v2 + + def test_domains(self): + serializer = self.floating_ip_dns.DomainsTemplate() + text = serializer.serialize(dict( + domain_entries=[ + dict(domain=domain, scope='public', project='testproject'), + dict(domain=domain2, scope='private', + availability_zone='avzone')])) + + tree = etree.fromstring(text) + self.assertEqual('domain_entries', tree.tag) + self.assertEqual(2, len(tree)) + self.assertEqual(domain, tree[0].get('domain')) + self.assertEqual(domain2, tree[1].get('domain')) + self.assertEqual('avzone', tree[1].get('availability_zone')) + + def test_domain_serializer(self): + serializer = self.floating_ip_dns.DomainTemplate() + text = serializer.serialize(dict( + domain_entry=dict(domain=domain, + scope='public', + project='testproject'))) + + tree = etree.fromstring(text) + self.assertEqual('domain_entry', tree.tag) + self.assertEqual(domain, tree.get('domain')) + self.assertEqual('testproject', tree.get('project')) + + def test_entries_serializer(self): + serializer = self.floating_ip_dns.FloatingIPDNSsTemplate() + text = serializer.serialize(dict( + dns_entries=[ + dict(ip=test_ipv4_address, + type='A', + domain=domain, + name=name), + dict(ip=test_ipv4_address2, + type='C', + domain=domain, + name=name2)])) + + tree = etree.fromstring(text) + self.assertEqual('dns_entries', tree.tag) + self.assertEqual(2, len(tree)) + self.assertEqual('dns_entry', tree[0].tag) + self.assertEqual('dns_entry', tree[1].tag) + self.assertEqual(test_ipv4_address, tree[0].get('ip')) + self.assertEqual('A', tree[0].get('type')) + self.assertEqual(domain, tree[0].get('domain')) + self.assertEqual(name, tree[0].get('name')) + self.assertEqual(test_ipv4_address2, tree[1].get('ip')) + self.assertEqual('C', tree[1].get('type')) + self.assertEqual(domain, tree[1].get('domain')) + self.assertEqual(name2, tree[1].get('name')) + + def test_entry_serializer(self): + serializer = self.floating_ip_dns.FloatingIPDNSTemplate() + text = serializer.serialize(dict( + dns_entry=dict( + ip=test_ipv4_address, + type='A', + domain=domain, + name=name))) + + tree = etree.fromstring(text) + + self.assertEqual('dns_entry', tree.tag) + self.assertEqual(test_ipv4_address, tree.get('ip')) + self.assertEqual(domain, tree.get('domain')) + self.assertEqual(name, tree.get('name')) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_pools.py b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_pools.py new file mode 100644 index 0000000000..926e88c6ae --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ip_pools.py @@ -0,0 +1,83 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# 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. + +from lxml import etree + +from nova.api.openstack.compute.contrib import floating_ip_pools as fipp_v2 +from nova.api.openstack.compute.plugins.v3 import floating_ip_pools as\ + fipp_v21 +from nova import context +from nova import network +from nova import test +from nova.tests.unit.api.openstack import fakes + + +def fake_get_floating_ip_pools(self, context): + return ['nova', 'other'] + + +class FloatingIpPoolTestV21(test.NoDBTestCase): + floating_ip_pools = fipp_v21 + url = '/v2/fake/os-floating-ip-pools' + + def setUp(self): + super(FloatingIpPoolTestV21, self).setUp() + self.stubs.Set(network.api.API, "get_floating_ip_pools", + fake_get_floating_ip_pools) + + self.context = context.RequestContext('fake', 'fake') + self.controller = self.floating_ip_pools.FloatingIPPoolsController() + + def test_translate_floating_ip_pools_view(self): + pools = fake_get_floating_ip_pools(None, self.context) + view = self.floating_ip_pools._translate_floating_ip_pools_view(pools) + self.assertIn('floating_ip_pools', view) + self.assertEqual(view['floating_ip_pools'][0]['name'], + pools[0]) + self.assertEqual(view['floating_ip_pools'][1]['name'], + pools[1]) + + def test_floating_ips_pools_list(self): + req = fakes.HTTPRequest.blank(self.url) + res_dict = self.controller.index(req) + + pools = fake_get_floating_ip_pools(None, self.context) + response = {'floating_ip_pools': [{'name': name} for name in pools]} + self.assertEqual(res_dict, response) + + +class FloatingIpPoolTestV2(FloatingIpPoolTestV21): + floating_ip_pools = fipp_v2 + + +class FloatingIpPoolSerializerTestV2(test.NoDBTestCase): + floating_ip_pools = fipp_v2 + + def test_index_serializer(self): + serializer = self.floating_ip_pools.FloatingIPPoolsTemplate() + text = serializer.serialize(dict( + floating_ip_pools=[ + dict(name='nova'), + dict(name='other') + ])) + + tree = etree.fromstring(text) + + self.assertEqual('floating_ip_pools', tree.tag) + self.assertEqual(2, len(tree)) + self.assertEqual('floating_ip_pool', tree[0].tag) + self.assertEqual('floating_ip_pool', tree[1].tag) + self.assertEqual('nova', tree[0].get('name')) + self.assertEqual('other', tree[1].get('name')) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_floating_ips.py b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ips.py new file mode 100644 index 0000000000..b383d1dbc1 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ips.py @@ -0,0 +1,853 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2011 Eldar Nugaev +# 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 contextlib +import uuid + +from lxml import etree +import mock +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import floating_ips +from nova.api.openstack import extensions +from nova import compute +from nova.compute import utils as compute_utils +from nova import context +from nova import db +from nova import exception +from nova import network +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_network + + +FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + + +def network_api_get_floating_ip(self, context, id): + return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova', + 'fixed_ip_id': None} + + +def network_api_get_floating_ip_by_address(self, context, address): + return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova', + 'fixed_ip_id': 10} + + +def network_api_get_floating_ips_by_project(self, context): + return [{'id': 1, + 'address': '10.10.10.10', + 'pool': 'nova', + 'fixed_ip': {'address': '10.0.0.1', + 'instance_uuid': FAKE_UUID, + 'instance': {'uuid': FAKE_UUID}}}, + {'id': 2, + 'pool': 'nova', 'interface': 'eth0', + 'address': '10.10.10.11', + 'fixed_ip': None}] + + +def compute_api_get(self, context, instance_id, expected_attrs=None, + want_objects=False): + return dict(uuid=FAKE_UUID, id=instance_id, instance_type_id=1, host='bob') + + +def network_api_allocate(self, context): + return '10.10.10.10' + + +def network_api_release(self, context, address): + pass + + +def compute_api_associate(self, context, instance_id, address): + pass + + +def network_api_associate(self, context, floating_address, fixed_address): + pass + + +def network_api_disassociate(self, context, instance, floating_address): + pass + + +def fake_instance_get(context, instance_id): + return { + "id": 1, + "uuid": uuid.uuid4(), + "name": 'fake', + "user_id": 'fakeuser', + "project_id": '123'} + + +def stub_nw_info(stubs): + def get_nw_info_for_instance(instance): + return fake_network.fake_get_instance_nw_info(stubs) + return get_nw_info_for_instance + + +def get_instance_by_floating_ip_addr(self, context, address): + return None + + +class FloatingIpTestNeutron(test.NoDBTestCase): + + def setUp(self): + super(FloatingIpTestNeutron, self).setUp() + self.flags(network_api_class='nova.network.neutronv2.api.API') + self.controller = floating_ips.FloatingIPController() + + def _get_fake_request(self): + return fakes.HTTPRequest.blank('/v2/fake/os-floating-ips/1') + + def test_floatingip_delete(self): + req = self._get_fake_request() + fip_val = {'address': '1.1.1.1', 'fixed_ip_id': '192.168.1.2'} + with contextlib.nested( + mock.patch.object(self.controller.network_api, + 'disassociate_floating_ip'), + mock.patch.object(self.controller.network_api, + 'disassociate_and_release_floating_ip'), + mock.patch.object(self.controller.network_api, + 'release_floating_ip'), + mock.patch.object(self.controller.network_api, + 'get_instance_id_by_floating_address', + return_value=None), + mock.patch.object(self.controller.network_api, + 'get_floating_ip', + return_value=fip_val)) as ( + disoc_fip, dis_and_del, rel_fip, _, _): + self.controller.delete(req, 1) + self.assertFalse(disoc_fip.called) + self.assertFalse(rel_fip.called) + # Only disassociate_and_release_floating_ip is + # called if using neutron + self.assertTrue(dis_and_del.called) + + +class FloatingIpTest(test.TestCase): + floating_ip = "10.10.10.10" + floating_ip_2 = "10.10.10.11" + + def _create_floating_ips(self, floating_ips=None): + """Create a floating ip object.""" + if floating_ips is None: + floating_ips = [self.floating_ip] + elif not isinstance(floating_ips, (list, tuple)): + floating_ips = [floating_ips] + + def make_ip_dict(ip): + """Shortcut for creating floating ip dict.""" + return + + dict_ = {'pool': 'nova', 'host': 'fake_host'} + return db.floating_ip_bulk_create( + self.context, [dict(address=ip, **dict_) for ip in floating_ips], + ) + + def _delete_floating_ip(self): + db.floating_ip_destroy(self.context, self.floating_ip) + + def _get_fake_fip_request(self, act=''): + return fakes.HTTPRequest.blank('/v2/fake/os-floating-ips/%s' % act) + + def _get_fake_server_request(self): + return fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + + def _get_fake_response(self, req, init_only): + return req.get_response(fakes.wsgi_app(init_only=(init_only,))) + + def setUp(self): + super(FloatingIpTest, self).setUp() + self.stubs.Set(compute.api.API, "get", + compute_api_get) + self.stubs.Set(network.api.API, "get_floating_ip", + network_api_get_floating_ip) + self.stubs.Set(network.api.API, "get_floating_ip_by_address", + network_api_get_floating_ip_by_address) + self.stubs.Set(network.api.API, "get_floating_ips_by_project", + network_api_get_floating_ips_by_project) + self.stubs.Set(network.api.API, "release_floating_ip", + network_api_release) + self.stubs.Set(network.api.API, "disassociate_floating_ip", + network_api_disassociate) + self.stubs.Set(network.api.API, "get_instance_id_by_floating_address", + get_instance_by_floating_ip_addr) + self.stubs.Set(compute_utils, "get_nw_info_for_instance", + stub_nw_info(self.stubs)) + + fake_network.stub_out_nw_api_get_instance_nw_info(self.stubs) + self.stubs.Set(db, 'instance_get', + fake_instance_get) + + self.context = context.get_admin_context() + self._create_floating_ips() + + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = floating_ips.FloatingIPController() + self.manager = floating_ips.FloatingIPActionController(self.ext_mgr) + + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Floating_ips']) + + def tearDown(self): + self._delete_floating_ip() + super(FloatingIpTest, self).tearDown() + + def test_floatingip_delete(self): + req = self._get_fake_fip_request('1') + fip_val = {'address': '1.1.1.1', 'fixed_ip_id': '192.168.1.2'} + with contextlib.nested( + mock.patch.object(self.controller.network_api, + 'disassociate_floating_ip'), + mock.patch.object(self.controller.network_api, + 'release_floating_ip'), + mock.patch.object(self.controller.network_api, + 'get_instance_id_by_floating_address', + return_value=None), + mock.patch.object(self.controller.network_api, + 'get_floating_ip', + return_value=fip_val)) as ( + disoc_fip, rel_fip, _, _): + self.controller.delete(req, 1) + self.assertTrue(disoc_fip.called) + self.assertTrue(rel_fip.called) + + def test_translate_floating_ip_view(self): + floating_ip_address = self.floating_ip + floating_ip = db.floating_ip_get_by_address(self.context, + floating_ip_address) + # NOTE(vish): network_get uses the id not the address + floating_ip = db.floating_ip_get(self.context, floating_ip['id']) + view = floating_ips._translate_floating_ip_view(floating_ip) + self.assertIn('floating_ip', view) + self.assertTrue(view['floating_ip']['id']) + self.assertEqual(view['floating_ip']['ip'], self.floating_ip) + self.assertIsNone(view['floating_ip']['fixed_ip']) + self.assertIsNone(view['floating_ip']['instance_id']) + + def test_translate_floating_ip_view_dict(self): + floating_ip = {'id': 0, 'address': '10.0.0.10', 'pool': 'nova', + 'fixed_ip': None} + view = floating_ips._translate_floating_ip_view(floating_ip) + self.assertIn('floating_ip', view) + + def test_floating_ips_list(self): + req = self._get_fake_fip_request() + res_dict = self.controller.index(req) + + response = {'floating_ips': [{'instance_id': FAKE_UUID, + 'ip': '10.10.10.10', + 'pool': 'nova', + 'fixed_ip': '10.0.0.1', + 'id': 1}, + {'instance_id': None, + 'ip': '10.10.10.11', + 'pool': 'nova', + 'fixed_ip': None, + 'id': 2}]} + self.assertEqual(res_dict, response) + + def test_floating_ip_release_nonexisting(self): + def fake_get_floating_ip(*args, **kwargs): + raise exception.FloatingIpNotFound(id=id) + + self.stubs.Set(network.api.API, "get_floating_ip", + fake_get_floating_ip) + + req = self._get_fake_fip_request('9876') + req.method = 'DELETE' + res = self._get_fake_response(req, 'os-floating-ips') + self.assertEqual(res.status_int, 404) + expected_msg = ('{"itemNotFound": {"message": "Floating ip not found ' + 'for id 9876", "code": 404}}') + self.assertEqual(res.body, expected_msg) + + def test_floating_ip_release_race_cond(self): + def fake_get_floating_ip(*args, **kwargs): + return {'fixed_ip_id': 1, 'address': self.floating_ip} + + def fake_get_instance_by_floating_ip_addr(*args, **kwargs): + return 'test-inst' + + def fake_disassociate_floating_ip(*args, **kwargs): + raise exception.FloatingIpNotAssociated(args[3]) + + self.stubs.Set(network.api.API, "get_floating_ip", + fake_get_floating_ip) + self.stubs.Set(floating_ips, "get_instance_by_floating_ip_addr", + fake_get_instance_by_floating_ip_addr) + self.stubs.Set(floating_ips, "disassociate_floating_ip", + fake_disassociate_floating_ip) + + req = self._get_fake_fip_request('1') + req.method = 'DELETE' + res = self._get_fake_response(req, 'os-floating-ips') + self.assertEqual(res.status_int, 202) + + def test_floating_ip_show(self): + req = self._get_fake_fip_request('1') + res_dict = self.controller.show(req, 1) + + self.assertEqual(res_dict['floating_ip']['id'], 1) + self.assertEqual(res_dict['floating_ip']['ip'], '10.10.10.10') + self.assertIsNone(res_dict['floating_ip']['instance_id']) + + def test_floating_ip_show_not_found(self): + def fake_get_floating_ip(*args, **kwargs): + raise exception.FloatingIpNotFound(id='fake') + + self.stubs.Set(network.api.API, "get_floating_ip", + fake_get_floating_ip) + + req = self._get_fake_fip_request('9876') + res = self._get_fake_response(req, 'os-floating-ips') + self.assertEqual(res.status_int, 404) + expected_msg = ('{"itemNotFound": {"message": "Floating ip not found ' + 'for id 9876", "code": 404}}') + self.assertEqual(res.body, expected_msg) + + def test_show_associated_floating_ip(self): + def get_floating_ip(self, context, id): + return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova', + 'fixed_ip': {'address': '10.0.0.1', + 'instance_uuid': FAKE_UUID, + 'instance': {'uuid': FAKE_UUID}}} + + self.stubs.Set(network.api.API, "get_floating_ip", get_floating_ip) + + req = self._get_fake_fip_request('1') + res_dict = self.controller.show(req, 1) + + self.assertEqual(res_dict['floating_ip']['id'], 1) + self.assertEqual(res_dict['floating_ip']['ip'], '10.10.10.10') + self.assertEqual(res_dict['floating_ip']['fixed_ip'], '10.0.0.1') + self.assertEqual(res_dict['floating_ip']['instance_id'], FAKE_UUID) + + def test_recreation_of_floating_ip(self): + self._delete_floating_ip() + self._create_floating_ips() + + def test_floating_ip_in_bulk_creation(self): + self._delete_floating_ip() + + self._create_floating_ips([self.floating_ip, self.floating_ip_2]) + all_ips = db.floating_ip_get_all(self.context) + ip_list = [ip['address'] for ip in all_ips] + self.assertIn(self.floating_ip, ip_list) + self.assertIn(self.floating_ip_2, ip_list) + + def test_fail_floating_ip_in_bulk_creation(self): + self.assertRaises(exception.FloatingIpExists, + self._create_floating_ips, + [self.floating_ip, self.floating_ip_2]) + all_ips = db.floating_ip_get_all(self.context) + ip_list = [ip['address'] for ip in all_ips] + self.assertIn(self.floating_ip, ip_list) + self.assertNotIn(self.floating_ip_2, ip_list) + + def test_floating_ip_allocate_no_free_ips(self): + def fake_allocate(*args, **kwargs): + raise exception.NoMoreFloatingIps() + + self.stubs.Set(network.api.API, "allocate_floating_ip", fake_allocate) + + req = self._get_fake_fip_request() + ex = self.assertRaises(webob.exc.HTTPNotFound, + self.controller.create, req) + + self.assertIn('No more floating ips', ex.explanation) + + def test_floating_ip_allocate_no_free_ips_pool(self): + def fake_allocate(*args, **kwargs): + raise exception.NoMoreFloatingIps() + + self.stubs.Set(network.api.API, "allocate_floating_ip", fake_allocate) + + req = self._get_fake_fip_request() + ex = self.assertRaises(webob.exc.HTTPNotFound, + self.controller.create, req, {'pool': 'non_existent_pool'}) + + self.assertIn('No more floating ips in pool non_existent_pool', + ex.explanation) + + @mock.patch('nova.network.api.API.allocate_floating_ip', + side_effect=exception.FloatingIpLimitExceeded()) + def test_floating_ip_allocate_over_quota(self, allocate_mock): + req = self._get_fake_fip_request() + ex = self.assertRaises(webob.exc.HTTPForbidden, + self.controller.create, req) + + self.assertIn('IP allocation over quota', ex.explanation) + + @mock.patch('nova.network.api.API.allocate_floating_ip', + side_effect=exception.FloatingIpLimitExceeded()) + def test_floating_ip_allocate_quota_exceed_in_pool(self, allocate_mock): + req = self._get_fake_fip_request() + ex = self.assertRaises(webob.exc.HTTPForbidden, + self.controller.create, req, {'pool': 'non_existent_pool'}) + + self.assertIn('IP allocation over quota in pool non_existent_pool.', + ex.explanation) + + @mock.patch('nova.network.api.API.allocate_floating_ip', + side_effect=exception.FloatingIpPoolNotFound()) + def test_floating_ip_create_with_unknown_pool(self, allocate_mock): + req = self._get_fake_fip_request() + ex = self.assertRaises(webob.exc.HTTPNotFound, + self.controller.create, req, {'pool': 'non_existent_pool'}) + + self.assertIn('Floating ip pool not found.', ex.explanation) + + def test_floating_ip_allocate(self): + def fake1(*args, **kwargs): + pass + + def fake2(*args, **kwargs): + return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova'} + + self.stubs.Set(network.api.API, "allocate_floating_ip", + fake1) + self.stubs.Set(network.api.API, "get_floating_ip_by_address", + fake2) + + req = self._get_fake_fip_request() + res_dict = self.controller.create(req) + + ip = res_dict['floating_ip'] + + expected = { + "id": 1, + "instance_id": None, + "ip": "10.10.10.10", + "fixed_ip": None, + "pool": 'nova'} + self.assertEqual(ip, expected) + + def test_floating_ip_release(self): + req = self._get_fake_fip_request('1') + self.controller.delete(req, 1) + + def test_floating_ip_associate(self): + fixed_address = '192.168.1.100' + + def fake_associate_floating_ip(*args, **kwargs): + self.assertEqual(fixed_address, kwargs['fixed_address']) + + self.stubs.Set(network.api.API, "associate_floating_ip", + fake_associate_floating_ip) + body = dict(addFloatingIp=dict(address=self.floating_ip)) + + req = self._get_fake_server_request() + rsp = self.manager._add_floating_ip(req, 'test_inst', body) + self.assertEqual(202, rsp.status_int) + + def test_floating_ip_associate_invalid_instance(self): + + def fake_get(self, context, id, expected_attrs=None, + want_objects=False): + raise exception.InstanceNotFound(instance_id=id) + + self.stubs.Set(compute.api.API, "get", fake_get) + + body = dict(addFloatingIp=dict(address=self.floating_ip)) + + req = self._get_fake_server_request() + self.assertRaises(webob.exc.HTTPNotFound, + self.manager._add_floating_ip, req, 'test_inst', + body) + + def test_not_extended_floating_ip_associate_fixed(self): + # Check that fixed_address is ignored if os-extended-floating-ips + # is not loaded + fixed_address_requested = '192.168.1.101' + fixed_address_allocated = '192.168.1.100' + + def fake_associate_floating_ip(*args, **kwargs): + self.assertEqual(fixed_address_allocated, + kwargs['fixed_address']) + + self.stubs.Set(network.api.API, "associate_floating_ip", + fake_associate_floating_ip) + body = dict(addFloatingIp=dict(address=self.floating_ip, + fixed_address=fixed_address_requested)) + + req = self._get_fake_server_request() + rsp = self.manager._add_floating_ip(req, 'test_inst', body) + self.assertEqual(202, rsp.status_int) + + def test_associate_not_allocated_floating_ip_to_instance(self): + def fake_associate_floating_ip(self, context, instance, + floating_address, fixed_address, + affect_auto_assigned=False): + raise exception.FloatingIpNotFoundForAddress( + address=floating_address) + self.stubs.Set(network.api.API, "associate_floating_ip", + fake_associate_floating_ip) + floating_ip = '10.10.10.11' + body = dict(addFloatingIp=dict(address=floating_ip)) + req = self._get_fake_server_request() + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + resp = self._get_fake_response(req, 'servers') + res_dict = jsonutils.loads(resp.body) + self.assertEqual(resp.status_int, 404) + self.assertEqual(res_dict['itemNotFound']['message'], + "floating ip not found") + + @mock.patch.object(network.api.API, 'associate_floating_ip', + side_effect=exception.Forbidden) + def test_associate_floating_ip_forbidden(self, associate_mock): + body = dict(addFloatingIp=dict(address='10.10.10.11')) + req = self._get_fake_server_request() + self.assertRaises(webob.exc.HTTPForbidden, + self.manager._add_floating_ip, req, 'test_inst', + body) + + def test_associate_floating_ip_bad_address_key(self): + body = dict(addFloatingIp=dict(bad_address='10.10.10.11')) + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._add_floating_ip, req, 'test_inst', + body) + + def test_associate_floating_ip_bad_addfloatingip_key(self): + body = dict(bad_addFloatingIp=dict(address='10.10.10.11')) + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._add_floating_ip, req, 'test_inst', + body) + + def test_floating_ip_disassociate(self): + def get_instance_by_floating_ip_addr(self, context, address): + if address == '10.10.10.10': + return 'test_inst' + + self.stubs.Set(network.api.API, "get_instance_id_by_floating_address", + get_instance_by_floating_ip_addr) + + body = dict(removeFloatingIp=dict(address='10.10.10.10')) + + req = self._get_fake_server_request() + rsp = self.manager._remove_floating_ip(req, 'test_inst', body) + self.assertEqual(202, rsp.status_int) + + def test_floating_ip_disassociate_missing(self): + body = dict(removeFloatingIp=dict(address='10.10.10.10')) + + req = self._get_fake_server_request() + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.manager._remove_floating_ip, + req, 'test_inst', body) + + def test_floating_ip_associate_non_existent_ip(self): + def fake_network_api_associate(self, context, instance, + floating_address=None, + fixed_address=None): + floating_ips = ["10.10.10.10", "10.10.10.11"] + if floating_address not in floating_ips: + raise exception.FloatingIpNotFoundForAddress( + address=floating_address) + + self.stubs.Set(network.api.API, "associate_floating_ip", + fake_network_api_associate) + + body = dict(addFloatingIp=dict(address='1.1.1.1')) + req = self._get_fake_server_request() + self.assertRaises(webob.exc.HTTPNotFound, + self.manager._add_floating_ip, + req, 'test_inst', body) + + def test_floating_ip_disassociate_non_existent_ip(self): + def network_api_get_floating_ip_by_address(self, context, + floating_address): + floating_ips = ["10.10.10.10", "10.10.10.11"] + if floating_address not in floating_ips: + raise exception.FloatingIpNotFoundForAddress( + address=floating_address) + + self.stubs.Set(network.api.API, "get_floating_ip_by_address", + network_api_get_floating_ip_by_address) + + body = dict(removeFloatingIp=dict(address='1.1.1.1')) + req = self._get_fake_server_request() + self.assertRaises(webob.exc.HTTPNotFound, + self.manager._remove_floating_ip, + req, 'test_inst', body) + + def test_floating_ip_disassociate_wrong_instance_uuid(self): + def get_instance_by_floating_ip_addr(self, context, address): + if address == '10.10.10.10': + return 'test_inst' + + self.stubs.Set(network.api.API, "get_instance_id_by_floating_address", + get_instance_by_floating_ip_addr) + + wrong_uuid = 'aaaaaaaa-ffff-ffff-ffff-aaaaaaaaaaaa' + body = dict(removeFloatingIp=dict(address='10.10.10.10')) + + req = self._get_fake_server_request() + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.manager._remove_floating_ip, + req, wrong_uuid, body) + + def test_floating_ip_disassociate_wrong_instance_id(self): + def get_instance_by_floating_ip_addr(self, context, address): + if address == '10.10.10.10': + return 'wrong_inst' + + self.stubs.Set(network.api.API, "get_instance_id_by_floating_address", + get_instance_by_floating_ip_addr) + + body = dict(removeFloatingIp=dict(address='10.10.10.10')) + + req = self._get_fake_server_request() + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.manager._remove_floating_ip, + req, 'test_inst', body) + + def test_floating_ip_disassociate_auto_assigned(self): + def fake_get_floating_ip_addr_auto_assigned(self, context, address): + return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova', + 'fixed_ip_id': 10, 'auto_assigned': 1} + + def get_instance_by_floating_ip_addr(self, context, address): + if address == '10.10.10.10': + return 'test_inst' + + def network_api_disassociate(self, context, instance, + floating_address): + raise exception.CannotDisassociateAutoAssignedFloatingIP() + + self.stubs.Set(network.api.API, "get_floating_ip_by_address", + fake_get_floating_ip_addr_auto_assigned) + self.stubs.Set(network.api.API, "get_instance_id_by_floating_address", + get_instance_by_floating_ip_addr) + self.stubs.Set(network.api.API, "disassociate_floating_ip", + network_api_disassociate) + body = dict(removeFloatingIp=dict(address='10.10.10.10')) + req = self._get_fake_server_request() + self.assertRaises(webob.exc.HTTPForbidden, + self.manager._remove_floating_ip, + req, 'test_inst', body) + + def test_floating_ip_disassociate_map_authorization_exc(self): + def fake_get_floating_ip_addr_auto_assigned(self, context, address): + return {'id': 1, 'address': '10.10.10.10', 'pool': 'nova', + 'fixed_ip_id': 10, 'auto_assigned': 1} + + def get_instance_by_floating_ip_addr(self, context, address): + if address == '10.10.10.10': + return 'test_inst' + + def network_api_disassociate(self, context, instance, address): + raise exception.Forbidden() + + self.stubs.Set(network.api.API, "get_floating_ip_by_address", + fake_get_floating_ip_addr_auto_assigned) + self.stubs.Set(network.api.API, "get_instance_id_by_floating_address", + get_instance_by_floating_ip_addr) + self.stubs.Set(network.api.API, "disassociate_floating_ip", + network_api_disassociate) + body = dict(removeFloatingIp=dict(address='10.10.10.10')) + req = self._get_fake_server_request() + self.assertRaises(webob.exc.HTTPForbidden, + self.manager._remove_floating_ip, + req, 'test_inst', body) + +# these are a few bad param tests + + def test_bad_address_param_in_remove_floating_ip(self): + body = dict(removeFloatingIp=dict(badparam='11.0.0.1')) + + req = self._get_fake_server_request() + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._remove_floating_ip, req, 'test_inst', + body) + + def test_missing_dict_param_in_remove_floating_ip(self): + body = dict(removeFloatingIp='11.0.0.1') + + req = self._get_fake_server_request() + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._remove_floating_ip, req, 'test_inst', + body) + + def test_missing_dict_param_in_add_floating_ip(self): + body = dict(addFloatingIp='11.0.0.1') + + req = self._get_fake_server_request() + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._add_floating_ip, req, 'test_inst', + body) + + +class ExtendedFloatingIpTest(test.TestCase): + floating_ip = "10.10.10.10" + floating_ip_2 = "10.10.10.11" + + def _create_floating_ips(self, floating_ips=None): + """Create a floating ip object.""" + if floating_ips is None: + floating_ips = [self.floating_ip] + elif not isinstance(floating_ips, (list, tuple)): + floating_ips = [floating_ips] + + def make_ip_dict(ip): + """Shortcut for creating floating ip dict.""" + return + + dict_ = {'pool': 'nova', 'host': 'fake_host'} + return db.floating_ip_bulk_create( + self.context, [dict(address=ip, **dict_) for ip in floating_ips], + ) + + def _delete_floating_ip(self): + db.floating_ip_destroy(self.context, self.floating_ip) + + def _get_fake_request(self): + return fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + + def _get_fake_response(self, req, init_only): + return req.get_response(fakes.wsgi_app(init_only=(init_only,))) + + def setUp(self): + super(ExtendedFloatingIpTest, self).setUp() + self.stubs.Set(compute.api.API, "get", + compute_api_get) + self.stubs.Set(network.api.API, "get_floating_ip", + network_api_get_floating_ip) + self.stubs.Set(network.api.API, "get_floating_ip_by_address", + network_api_get_floating_ip_by_address) + self.stubs.Set(network.api.API, "get_floating_ips_by_project", + network_api_get_floating_ips_by_project) + self.stubs.Set(network.api.API, "release_floating_ip", + network_api_release) + self.stubs.Set(network.api.API, "disassociate_floating_ip", + network_api_disassociate) + self.stubs.Set(network.api.API, "get_instance_id_by_floating_address", + get_instance_by_floating_ip_addr) + self.stubs.Set(compute_utils, "get_nw_info_for_instance", + stub_nw_info(self.stubs)) + + fake_network.stub_out_nw_api_get_instance_nw_info(self.stubs) + self.stubs.Set(db, 'instance_get', + fake_instance_get) + + self.context = context.get_admin_context() + self._create_floating_ips() + + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.ext_mgr.extensions['os-floating-ips'] = True + self.ext_mgr.extensions['os-extended-floating-ips'] = True + self.controller = floating_ips.FloatingIPController() + self.manager = floating_ips.FloatingIPActionController(self.ext_mgr) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Floating_ips', 'Extended_floating_ips']) + + def tearDown(self): + self._delete_floating_ip() + super(ExtendedFloatingIpTest, self).tearDown() + + def test_extended_floating_ip_associate_fixed(self): + fixed_address = '192.168.1.101' + + def fake_associate_floating_ip(*args, **kwargs): + self.assertEqual(fixed_address, kwargs['fixed_address']) + + self.stubs.Set(network.api.API, "associate_floating_ip", + fake_associate_floating_ip) + body = dict(addFloatingIp=dict(address=self.floating_ip, + fixed_address=fixed_address)) + + req = self._get_fake_request() + rsp = self.manager._add_floating_ip(req, 'test_inst', body) + self.assertEqual(202, rsp.status_int) + + def test_extended_floating_ip_associate_fixed_not_allocated(self): + def fake_associate_floating_ip(*args, **kwargs): + pass + + self.stubs.Set(network.api.API, "associate_floating_ip", + fake_associate_floating_ip) + body = dict(addFloatingIp=dict(address=self.floating_ip, + fixed_address='11.11.11.11')) + + req = self._get_fake_request() + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + resp = self._get_fake_response(req, 'servers') + res_dict = jsonutils.loads(resp.body) + self.assertEqual(resp.status_int, 400) + self.assertEqual(res_dict['badRequest']['message'], + "Specified fixed address not assigned to instance") + + +class FloatingIpSerializerTest(test.TestCase): + def test_default_serializer(self): + serializer = floating_ips.FloatingIPTemplate() + text = serializer.serialize(dict( + floating_ip=dict( + instance_id=1, + ip='10.10.10.10', + fixed_ip='10.0.0.1', + id=1))) + + tree = etree.fromstring(text) + + self.assertEqual('floating_ip', tree.tag) + self.assertEqual('1', tree.get('instance_id')) + self.assertEqual('10.10.10.10', tree.get('ip')) + self.assertEqual('10.0.0.1', tree.get('fixed_ip')) + self.assertEqual('1', tree.get('id')) + + def test_index_serializer(self): + serializer = floating_ips.FloatingIPsTemplate() + text = serializer.serialize(dict( + floating_ips=[ + dict(instance_id=1, + ip='10.10.10.10', + fixed_ip='10.0.0.1', + id=1), + dict(instance_id=None, + ip='10.10.10.11', + fixed_ip=None, + id=2)])) + + tree = etree.fromstring(text) + + self.assertEqual('floating_ips', tree.tag) + self.assertEqual(2, len(tree)) + self.assertEqual('floating_ip', tree[0].tag) + self.assertEqual('floating_ip', tree[1].tag) + self.assertEqual('1', tree[0].get('instance_id')) + self.assertEqual('None', tree[1].get('instance_id')) + self.assertEqual('10.10.10.10', tree[0].get('ip')) + self.assertEqual('10.10.10.11', tree[1].get('ip')) + self.assertEqual('10.0.0.1', tree[0].get('fixed_ip')) + self.assertEqual('None', tree[1].get('fixed_ip')) + self.assertEqual('1', tree[0].get('id')) + self.assertEqual('2', tree[1].get('id')) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_floating_ips_bulk.py b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ips_bulk.py new file mode 100644 index 0000000000..8c81d99ab0 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_floating_ips_bulk.py @@ -0,0 +1,139 @@ +# Copyright 2012 IBM Corp. +# +# 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 netaddr +from oslo.config import cfg +import webob + +from nova.api.openstack.compute.contrib import floating_ips_bulk as fipbulk_v2 +from nova.api.openstack.compute.plugins.v3 import floating_ips_bulk as\ + fipbulk_v21 +from nova import context +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + +CONF = cfg.CONF + + +class FloatingIPBulkV21(test.TestCase): + + floating_ips_bulk = fipbulk_v21 + url = '/v2/fake/os-floating-ips-bulk' + delete_url = '/v2/fake/os-fixed-ips/delete' + bad_request = exception.ValidationError + + def setUp(self): + super(FloatingIPBulkV21, self).setUp() + + self.context = context.get_admin_context() + self.controller = self.floating_ips_bulk.FloatingIPBulkController() + + def _setup_floating_ips(self, ip_range): + body = {'floating_ips_bulk_create': {'ip_range': ip_range}} + req = fakes.HTTPRequest.blank(self.url) + res_dict = self.controller.create(req, body=body) + response = {"floating_ips_bulk_create": { + 'ip_range': ip_range, + 'pool': CONF.default_floating_pool, + 'interface': CONF.public_interface}} + self.assertEqual(res_dict, response) + + def test_create_ips(self): + ip_range = '192.168.1.0/24' + self._setup_floating_ips(ip_range) + + def test_create_ips_pool(self): + ip_range = '10.0.1.0/20' + pool = 'a new pool' + body = {'floating_ips_bulk_create': + {'ip_range': ip_range, + 'pool': pool}} + req = fakes.HTTPRequest.blank(self.url) + res_dict = self.controller.create(req, body=body) + response = {"floating_ips_bulk_create": { + 'ip_range': ip_range, + 'pool': pool, + 'interface': CONF.public_interface}} + self.assertEqual(res_dict, response) + + def test_list_ips(self): + ip_range = '192.168.1.1/28' + self._setup_floating_ips(ip_range) + req = fakes.HTTPRequest.blank(self.url, use_admin_context=True) + res_dict = self.controller.index(req) + + ip_info = [{'address': str(ip_addr), + 'pool': CONF.default_floating_pool, + 'interface': CONF.public_interface, + 'project_id': None, + 'instance_uuid': None} + for ip_addr in netaddr.IPNetwork(ip_range).iter_hosts()] + response = {'floating_ip_info': ip_info} + + self.assertEqual(res_dict, response) + + def test_list_ip_by_host(self): + ip_range = '192.168.1.1/28' + self._setup_floating_ips(ip_range) + req = fakes.HTTPRequest.blank(self.url, use_admin_context=True) + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, 'host') + + def test_delete_ips(self): + ip_range = '192.168.1.0/20' + self._setup_floating_ips(ip_range) + + body = {'ip_range': ip_range} + req = fakes.HTTPRequest.blank(self.delete_url) + res_dict = self.controller.update(req, "delete", body=body) + + response = {"floating_ips_bulk_delete": ip_range} + self.assertEqual(res_dict, response) + + # Check that the IPs are actually deleted + req = fakes.HTTPRequest.blank(self.url, use_admin_context=True) + res_dict = self.controller.index(req) + response = {'floating_ip_info': []} + self.assertEqual(res_dict, response) + + def test_create_duplicate_fail(self): + ip_range = '192.168.1.0/20' + self._setup_floating_ips(ip_range) + + ip_range = '192.168.1.0/28' + body = {'floating_ips_bulk_create': {'ip_range': ip_range}} + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, body=body) + + def test_create_bad_cidr_fail(self): + # netaddr can't handle /32 or 31 cidrs + ip_range = '192.168.1.1/32' + body = {'floating_ips_bulk_create': {'ip_range': ip_range}} + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, body=body) + + def test_create_invalid_cidr_fail(self): + ip_range = 'not a cidr' + body = {'floating_ips_bulk_create': {'ip_range': ip_range}} + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(self.bad_request, self.controller.create, + req, body=body) + + +class FloatingIPBulkV2(FloatingIPBulkV21): + floating_ips_bulk = fipbulk_v2 + bad_request = webob.exc.HTTPBadRequest diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_fping.py b/nova/tests/unit/api/openstack/compute/contrib/test_fping.py new file mode 100644 index 0000000000..a6364d6ee7 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_fping.py @@ -0,0 +1,106 @@ +# Copyright 2011 Grid Dynamics +# Copyright 2011 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. + +from nova.api.openstack.compute.contrib import fping +from nova.api.openstack.compute.plugins.v3 import fping as fping_v21 +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +import nova.utils + + +FAKE_UUID = fakes.FAKE_UUID + + +def execute(*cmd, **args): + return "".join(["%s is alive" % ip for ip in cmd[1:]]) + + +class FpingTestV21(test.TestCase): + controller_cls = fping_v21.FpingController + + def setUp(self): + super(FpingTestV21, self).setUp() + self.flags(verbose=True, use_ipv6=False) + return_server = fakes.fake_instance_get() + return_servers = fakes.fake_instance_get_all_by_filters() + self.stubs.Set(nova.db, "instance_get_all_by_filters", + return_servers) + self.stubs.Set(nova.db, "instance_get_by_uuid", + return_server) + self.stubs.Set(nova.utils, "execute", + execute) + self.stubs.Set(self.controller_cls, "check_fping", + lambda self: None) + self.controller = self.controller_cls() + + def _get_url(self): + return "/v3" + + def test_fping_index(self): + req = fakes.HTTPRequest.blank(self._get_url() + "/os-fping") + res_dict = self.controller.index(req) + self.assertIn("servers", res_dict) + for srv in res_dict["servers"]: + for key in "project_id", "id", "alive": + self.assertIn(key, srv) + + def test_fping_index_policy(self): + req = fakes.HTTPRequest.blank(self._get_url() + + "os-fping?all_tenants=1") + self.assertRaises(exception.Forbidden, self.controller.index, req) + req = fakes.HTTPRequest.blank(self._get_url() + + "/os-fping?all_tenants=1") + req.environ["nova.context"].is_admin = True + res_dict = self.controller.index(req) + self.assertIn("servers", res_dict) + + def test_fping_index_include(self): + req = fakes.HTTPRequest.blank(self._get_url() + "/os-fping") + res_dict = self.controller.index(req) + ids = [srv["id"] for srv in res_dict["servers"]] + req = fakes.HTTPRequest.blank(self._get_url() + + "/os-fping?include=%s" % ids[0]) + res_dict = self.controller.index(req) + self.assertEqual(len(res_dict["servers"]), 1) + self.assertEqual(res_dict["servers"][0]["id"], ids[0]) + + def test_fping_index_exclude(self): + req = fakes.HTTPRequest.blank(self._get_url() + "/os-fping") + res_dict = self.controller.index(req) + ids = [srv["id"] for srv in res_dict["servers"]] + req = fakes.HTTPRequest.blank(self._get_url() + + "/os-fping?exclude=%s" % + ",".join(ids[1:])) + res_dict = self.controller.index(req) + self.assertEqual(len(res_dict["servers"]), 1) + self.assertEqual(res_dict["servers"][0]["id"], ids[0]) + + def test_fping_show(self): + req = fakes.HTTPRequest.blank(self._get_url() + + "os-fping/%s" % FAKE_UUID) + res_dict = self.controller.show(req, FAKE_UUID) + self.assertIn("server", res_dict) + srv = res_dict["server"] + for key in "project_id", "id", "alive": + self.assertIn(key, srv) + + +class FpingTestV2(FpingTestV21): + controller_cls = fping.FpingController + + def _get_url(self): + return "/v2/1234" diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_hide_server_addresses.py b/nova/tests/unit/api/openstack/compute/contrib/test_hide_server_addresses.py new file mode 100644 index 0000000000..217fd480f9 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_hide_server_addresses.py @@ -0,0 +1,172 @@ +# 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 itertools + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack import wsgi +from nova import compute +from nova.compute import vm_states +from nova import db +from nova import exception +from nova import objects +from nova.objects import instance as instance_obj +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + + +SENTINEL = object() + + +def fake_compute_get(*args, **kwargs): + def _return_server(*_args, **_kwargs): + inst = fakes.stub_instance(*args, **kwargs) + return fake_instance.fake_instance_obj(_args[1], **inst) + return _return_server + + +class HideServerAddressesTestV21(test.TestCase): + content_type = 'application/json' + base_url = '/v2/fake/servers' + + def _setup_wsgi(self): + self.wsgi_app = fakes.wsgi_app_v21( + init_only=('servers', 'os-hide-server-addresses')) + + def setUp(self): + super(HideServerAddressesTestV21, self).setUp() + fakes.stub_out_nw_api(self.stubs) + return_server = fakes.fake_instance_get() + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + self._setup_wsgi() + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(self.wsgi_app) + return res + + @staticmethod + def _get_server(body): + return jsonutils.loads(body).get('server') + + @staticmethod + def _get_servers(body): + return jsonutils.loads(body).get('servers') + + @staticmethod + def _get_addresses(server): + return server.get('addresses', SENTINEL) + + def _check_addresses(self, addresses, exists): + self.assertTrue(addresses is not SENTINEL) + if exists: + self.assertTrue(addresses) + else: + self.assertFalse(addresses) + + def test_show_hides_in_building(self): + instance_id = 1 + uuid = fakes.get_fake_uuid(instance_id) + self.stubs.Set(compute.api.API, 'get', + fake_compute_get(instance_id, uuid=uuid, + vm_state=vm_states.BUILDING)) + res = self._make_request(self.base_url + '/%s' % uuid) + self.assertEqual(res.status_int, 200) + + server = self._get_server(res.body) + addresses = self._get_addresses(server) + self._check_addresses(addresses, exists=False) + + def test_show(self): + instance_id = 1 + uuid = fakes.get_fake_uuid(instance_id) + self.stubs.Set(compute.api.API, 'get', + fake_compute_get(instance_id, uuid=uuid, + vm_state=vm_states.ACTIVE)) + res = self._make_request(self.base_url + '/%s' % uuid) + self.assertEqual(res.status_int, 200) + + server = self._get_server(res.body) + addresses = self._get_addresses(server) + self._check_addresses(addresses, exists=True) + + def test_detail_hides_building_server_addresses(self): + instance_0 = fakes.stub_instance(0, uuid=fakes.get_fake_uuid(0), + vm_state=vm_states.ACTIVE) + instance_1 = fakes.stub_instance(1, uuid=fakes.get_fake_uuid(1), + vm_state=vm_states.BUILDING) + instances = [instance_0, instance_1] + + def get_all(*args, **kwargs): + fields = instance_obj.INSTANCE_DEFAULT_FIELDS + return instance_obj._make_instance_list( + args[1], objects.InstanceList(), instances, fields) + + self.stubs.Set(compute.api.API, 'get_all', get_all) + res = self._make_request(self.base_url + '/detail') + + self.assertEqual(res.status_int, 200) + servers = self._get_servers(res.body) + + self.assertEqual(len(servers), len(instances)) + + for instance, server in itertools.izip(instances, servers): + addresses = self._get_addresses(server) + exists = (instance['vm_state'] == vm_states.ACTIVE) + self._check_addresses(addresses, exists=exists) + + def test_no_instance_passthrough_404(self): + + def fake_compute_get(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + res = self._make_request(self.base_url + '/' + fakes.get_fake_uuid()) + + self.assertEqual(res.status_int, 404) + + +class HideServerAddressesTestV2(HideServerAddressesTestV21): + + def _setup_wsgi(self): + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Hide_server_addresses']) + self.wsgi_app = fakes.wsgi_app(init_only=('servers',)) + + +class HideAddressesXmlTest(HideServerAddressesTestV2): + content_type = 'application/xml' + + @staticmethod + def _get_server(body): + return etree.XML(body) + + @staticmethod + def _get_servers(body): + return etree.XML(body).getchildren() + + @staticmethod + def _get_addresses(server): + addresses = server.find('{%s}addresses' % wsgi.XMLNS_V11) + if addresses is None: + return SENTINEL + return addresses diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_hosts.py b/nova/tests/unit/api/openstack/compute/contrib/test_hosts.py new file mode 100644 index 0000000000..5478a7dd33 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_hosts.py @@ -0,0 +1,471 @@ +# Copyright (c) 2011 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. + +from lxml import etree +import testtools +import webob.exc + +from nova.api.openstack.compute.contrib import hosts as os_hosts_v2 +from nova.api.openstack.compute.plugins.v3 import hosts as os_hosts_v3 +from nova.compute import power_state +from nova.compute import vm_states +from nova import context as context_maker +from nova import db +from nova import exception +from nova import test +from nova.tests.unit import fake_hosts +from nova.tests.unit import utils + + +def stub_service_get_all(context, disabled=None): + return fake_hosts.SERVICES_LIST + + +def stub_service_get_by_host_and_topic(context, host_name, topic): + for service in stub_service_get_all(context): + if service['host'] == host_name and service['topic'] == topic: + return service + + +def stub_set_host_enabled(context, host_name, enabled): + """Simulates three possible behaviours for VM drivers or compute + drivers when enabling or disabling a host. + + 'enabled' means new instances can go to this host + 'disabled' means they can't + """ + results = {True: "enabled", False: "disabled"} + if host_name == "notimplemented": + # The vm driver for this host doesn't support this feature + raise NotImplementedError() + elif host_name == "dummydest": + # The host does not exist + raise exception.ComputeHostNotFound(host=host_name) + elif host_name == "host_c2": + # Simulate a failure + return results[not enabled] + else: + # Do the right thing + return results[enabled] + + +def stub_set_host_maintenance(context, host_name, mode): + # We'll simulate success and failure by assuming + # that 'host_c1' always succeeds, and 'host_c2' + # always fails + results = {True: "on_maintenance", False: "off_maintenance"} + if host_name == "notimplemented": + # The vm driver for this host doesn't support this feature + raise NotImplementedError() + elif host_name == "dummydest": + # The host does not exist + raise exception.ComputeHostNotFound(host=host_name) + elif host_name == "host_c2": + # Simulate a failure + return results[not mode] + else: + # Do the right thing + return results[mode] + + +def stub_host_power_action(context, host_name, action): + if host_name == "notimplemented": + raise NotImplementedError() + elif host_name == "dummydest": + # The host does not exist + raise exception.ComputeHostNotFound(host=host_name) + return action + + +def _create_instance(**kwargs): + """Create a test instance.""" + ctxt = context_maker.get_admin_context() + return db.instance_create(ctxt, _create_instance_dict(**kwargs)) + + +def _create_instance_dict(**kwargs): + """Create a dictionary for a test instance.""" + inst = {} + inst['image_ref'] = 'cedef40a-ed67-4d10-800e-17455edce175' + inst['reservation_id'] = 'r-fakeres' + inst['user_id'] = kwargs.get('user_id', 'admin') + inst['project_id'] = kwargs.get('project_id', 'fake') + inst['instance_type_id'] = '1' + if 'host' in kwargs: + inst['host'] = kwargs.get('host') + inst['vcpus'] = kwargs.get('vcpus', 1) + inst['memory_mb'] = kwargs.get('memory_mb', 20) + inst['root_gb'] = kwargs.get('root_gb', 30) + inst['ephemeral_gb'] = kwargs.get('ephemeral_gb', 30) + inst['vm_state'] = kwargs.get('vm_state', vm_states.ACTIVE) + inst['power_state'] = kwargs.get('power_state', power_state.RUNNING) + inst['task_state'] = kwargs.get('task_state', None) + inst['availability_zone'] = kwargs.get('availability_zone', None) + inst['ami_launch_index'] = 0 + inst['launched_on'] = kwargs.get('launched_on', 'dummy') + return inst + + +class FakeRequest(object): + environ = {"nova.context": context_maker.get_admin_context()} + GET = {} + + +class FakeRequestWithNovaZone(object): + environ = {"nova.context": context_maker.get_admin_context()} + GET = {"zone": "nova"} + + +class FakeRequestWithNovaService(object): + environ = {"nova.context": context_maker.get_admin_context()} + GET = {"service": "compute"} + + +class FakeRequestWithInvalidNovaService(object): + environ = {"nova.context": context_maker.get_admin_context()} + GET = {"service": "invalid"} + + +class HostTestCaseV21(test.TestCase): + """Test Case for hosts.""" + validation_ex = exception.ValidationError + Controller = os_hosts_v3.HostController + policy_ex = exception.PolicyNotAuthorized + + def _setup_stubs(self): + # Pretend we have fake_hosts.HOST_LIST in the DB + self.stubs.Set(db, 'service_get_all', + stub_service_get_all) + # Only hosts in our fake DB exist + self.stubs.Set(db, 'service_get_by_host_and_topic', + stub_service_get_by_host_and_topic) + # 'host_c1' always succeeds, and 'host_c2' + self.stubs.Set(self.hosts_api, 'set_host_enabled', + stub_set_host_enabled) + # 'host_c1' always succeeds, and 'host_c2' + self.stubs.Set(self.hosts_api, 'set_host_maintenance', + stub_set_host_maintenance) + self.stubs.Set(self.hosts_api, 'host_power_action', + stub_host_power_action) + + def setUp(self): + super(HostTestCaseV21, self).setUp() + self.controller = self.Controller() + self.hosts_api = self.controller.api + self.req = FakeRequest() + + self._setup_stubs() + + def _test_host_update(self, host, key, val, expected_value): + body = {key: val} + result = self.controller.update(self.req, host, body=body) + self.assertEqual(result[key], expected_value) + + def test_list_hosts(self): + """Verify that the compute hosts are returned.""" + result = self.controller.index(self.req) + self.assertIn('hosts', result) + hosts = result['hosts'] + self.assertEqual(fake_hosts.HOST_LIST, hosts) + + def test_disable_host(self): + self._test_host_update('host_c1', 'status', 'disable', 'disabled') + self._test_host_update('host_c2', 'status', 'disable', 'enabled') + + def test_enable_host(self): + self._test_host_update('host_c1', 'status', 'enable', 'enabled') + self._test_host_update('host_c2', 'status', 'enable', 'disabled') + + def test_enable_maintenance(self): + self._test_host_update('host_c1', 'maintenance_mode', + 'enable', 'on_maintenance') + + def test_disable_maintenance(self): + self._test_host_update('host_c1', 'maintenance_mode', + 'disable', 'off_maintenance') + + def _test_host_update_notimpl(self, key, val): + def stub_service_get_all_notimpl(self, req): + return [{'host': 'notimplemented', 'topic': None, + 'availability_zone': None}] + self.stubs.Set(db, 'service_get_all', + stub_service_get_all_notimpl) + body = {key: val} + self.assertRaises(webob.exc.HTTPNotImplemented, + self.controller.update, + self.req, 'notimplemented', body=body) + + def test_disable_host_notimpl(self): + self._test_host_update_notimpl('status', 'disable') + + def test_enable_maintenance_notimpl(self): + self._test_host_update_notimpl('maintenance_mode', 'enable') + + def test_host_startup(self): + result = self.controller.startup(self.req, "host_c1") + self.assertEqual(result["power_action"], "startup") + + def test_host_shutdown(self): + result = self.controller.shutdown(self.req, "host_c1") + self.assertEqual(result["power_action"], "shutdown") + + def test_host_reboot(self): + result = self.controller.reboot(self.req, "host_c1") + self.assertEqual(result["power_action"], "reboot") + + def _test_host_power_action_notimpl(self, method): + self.assertRaises(webob.exc.HTTPNotImplemented, + method, self.req, "notimplemented") + + def test_host_startup_notimpl(self): + self._test_host_power_action_notimpl(self.controller.startup) + + def test_host_shutdown_notimpl(self): + self._test_host_power_action_notimpl(self.controller.shutdown) + + def test_host_reboot_notimpl(self): + self._test_host_power_action_notimpl(self.controller.reboot) + + def test_host_status_bad_host(self): + # A host given as an argument does not exist. + self.req.environ["nova.context"].is_admin = True + dest = 'dummydest' + with testtools.ExpectedException(webob.exc.HTTPNotFound, + ".*%s.*" % dest): + self.controller.update(self.req, dest, body={'status': 'enable'}) + + def test_host_maintenance_bad_host(self): + # A host given as an argument does not exist. + self.req.environ["nova.context"].is_admin = True + dest = 'dummydest' + with testtools.ExpectedException(webob.exc.HTTPNotFound, + ".*%s.*" % dest): + self.controller.update(self.req, dest, + body={'maintenance_mode': 'enable'}) + + def test_host_power_action_bad_host(self): + # A host given as an argument does not exist. + self.req.environ["nova.context"].is_admin = True + dest = 'dummydest' + with testtools.ExpectedException(webob.exc.HTTPNotFound, + ".*%s.*" % dest): + self.controller.reboot(self.req, dest) + + def test_bad_status_value(self): + bad_body = {"status": "bad"} + self.assertRaises(self.validation_ex, self.controller.update, + self.req, "host_c1", body=bad_body) + bad_body2 = {"status": "disablabc"} + self.assertRaises(self.validation_ex, self.controller.update, + self.req, "host_c1", body=bad_body2) + + def test_bad_update_key(self): + bad_body = {"crazy": "bad"} + self.assertRaises(self.validation_ex, self.controller.update, + self.req, "host_c1", body=bad_body) + + def test_bad_update_key_and_correct_update_key(self): + bad_body = {"status": "disable", "crazy": "bad"} + self.assertRaises(self.validation_ex, self.controller.update, + self.req, "host_c1", body=bad_body) + + def test_good_update_keys(self): + body = {"status": "disable", "maintenance_mode": "enable"} + result = self.controller.update(self.req, 'host_c1', body=body) + self.assertEqual(result["host"], "host_c1") + self.assertEqual(result["status"], "disabled") + self.assertEqual(result["maintenance_mode"], "on_maintenance") + + def test_show_forbidden(self): + self.req.environ["nova.context"].is_admin = False + dest = 'dummydest' + self.assertRaises(self.policy_ex, + self.controller.show, + self.req, dest) + self.req.environ["nova.context"].is_admin = True + + def test_show_host_not_exist(self): + # A host given as an argument does not exist. + self.req.environ["nova.context"].is_admin = True + dest = 'dummydest' + with testtools.ExpectedException(webob.exc.HTTPNotFound, + ".*%s.*" % dest): + self.controller.show(self.req, dest) + + def _create_compute_service(self): + """Create compute-manager(ComputeNode and Service record).""" + ctxt = self.req.environ["nova.context"] + dic = {'host': 'dummy', 'binary': 'nova-compute', 'topic': 'compute', + 'report_count': 0} + s_ref = db.service_create(ctxt, dic) + + dic = {'service_id': s_ref['id'], + 'vcpus': 16, 'memory_mb': 32, 'local_gb': 100, + 'vcpus_used': 16, 'memory_mb_used': 32, 'local_gb_used': 10, + 'hypervisor_type': 'qemu', 'hypervisor_version': 12003, + 'cpu_info': '', 'stats': ''} + db.compute_node_create(ctxt, dic) + + return db.service_get(ctxt, s_ref['id']) + + def test_show_no_project(self): + """No instances are running on the given host.""" + ctxt = context_maker.get_admin_context() + s_ref = self._create_compute_service() + + result = self.controller.show(self.req, s_ref['host']) + + proj = ['(total)', '(used_now)', '(used_max)'] + column = ['host', 'project', 'cpu', 'memory_mb', 'disk_gb'] + self.assertEqual(len(result['host']), 3) + for resource in result['host']: + self.assertIn(resource['resource']['project'], proj) + self.assertEqual(len(resource['resource']), 5) + self.assertEqual(set(column), set(resource['resource'].keys())) + db.service_destroy(ctxt, s_ref['id']) + + def test_show_works_correctly(self): + """show() works correctly as expected.""" + ctxt = context_maker.get_admin_context() + s_ref = self._create_compute_service() + i_ref1 = _create_instance(project_id='p-01', host=s_ref['host']) + i_ref2 = _create_instance(project_id='p-02', vcpus=3, + host=s_ref['host']) + + result = self.controller.show(self.req, s_ref['host']) + + proj = ['(total)', '(used_now)', '(used_max)', 'p-01', 'p-02'] + column = ['host', 'project', 'cpu', 'memory_mb', 'disk_gb'] + self.assertEqual(len(result['host']), 5) + for resource in result['host']: + self.assertIn(resource['resource']['project'], proj) + self.assertEqual(len(resource['resource']), 5) + self.assertEqual(set(column), set(resource['resource'].keys())) + db.service_destroy(ctxt, s_ref['id']) + db.instance_destroy(ctxt, i_ref1['uuid']) + db.instance_destroy(ctxt, i_ref2['uuid']) + + def test_list_hosts_with_zone(self): + result = self.controller.index(FakeRequestWithNovaZone()) + self.assertIn('hosts', result) + hosts = result['hosts'] + self.assertEqual(fake_hosts.HOST_LIST_NOVA_ZONE, hosts) + + def test_list_hosts_with_service(self): + result = self.controller.index(FakeRequestWithNovaService()) + self.assertEqual(fake_hosts.HOST_LIST_NOVA_ZONE, result['hosts']) + + def test_list_hosts_with_invalid_service(self): + result = self.controller.index(FakeRequestWithInvalidNovaService()) + self.assertEqual([], result['hosts']) + + +class HostTestCaseV20(HostTestCaseV21): + validation_ex = webob.exc.HTTPBadRequest + policy_ex = webob.exc.HTTPForbidden + Controller = os_hosts_v2.HostController + + # Note: V2 api don't support list with services + def test_list_hosts_with_service(self): + pass + + def test_list_hosts_with_invalid_service(self): + pass + + +class HostSerializerTest(test.TestCase): + def setUp(self): + super(HostSerializerTest, self).setUp() + self.deserializer = os_hosts_v2.HostUpdateDeserializer() + + def test_index_serializer(self): + serializer = os_hosts_v2.HostIndexTemplate() + text = serializer.serialize(fake_hosts.OS_API_HOST_LIST) + + tree = etree.fromstring(text) + + self.assertEqual('hosts', tree.tag) + self.assertEqual(len(fake_hosts.HOST_LIST), len(tree)) + for i in range(len(fake_hosts.HOST_LIST)): + self.assertEqual('host', tree[i].tag) + self.assertEqual(fake_hosts.HOST_LIST[i]['host_name'], + tree[i].get('host_name')) + self.assertEqual(fake_hosts.HOST_LIST[i]['service'], + tree[i].get('service')) + self.assertEqual(fake_hosts.HOST_LIST[i]['zone'], + tree[i].get('zone')) + + def test_update_serializer_with_status(self): + exemplar = dict(host='host_c1', status='enabled') + serializer = os_hosts_v2.HostUpdateTemplate() + text = serializer.serialize(exemplar) + + tree = etree.fromstring(text) + + self.assertEqual('host', tree.tag) + for key, value in exemplar.items(): + self.assertEqual(value, tree.get(key)) + + def test_update_serializer_with_maintenance_mode(self): + exemplar = dict(host='host_c1', maintenance_mode='enabled') + serializer = os_hosts_v2.HostUpdateTemplate() + text = serializer.serialize(exemplar) + + tree = etree.fromstring(text) + + self.assertEqual('host', tree.tag) + for key, value in exemplar.items(): + self.assertEqual(value, tree.get(key)) + + def test_update_serializer_with_maintenance_mode_and_status(self): + exemplar = dict(host='host_c1', + maintenance_mode='enabled', + status='enabled') + serializer = os_hosts_v2.HostUpdateTemplate() + text = serializer.serialize(exemplar) + + tree = etree.fromstring(text) + + self.assertEqual('host', tree.tag) + for key, value in exemplar.items(): + self.assertEqual(value, tree.get(key)) + + def test_action_serializer(self): + exemplar = dict(host='host_c1', power_action='reboot') + serializer = os_hosts_v2.HostActionTemplate() + text = serializer.serialize(exemplar) + + tree = etree.fromstring(text) + + self.assertEqual('host', tree.tag) + for key, value in exemplar.items(): + self.assertEqual(value, tree.get(key)) + + def test_update_deserializer(self): + exemplar = dict(status='enabled', maintenance_mode='disable') + intext = """<?xml version='1.0' encoding='UTF-8'?> + <updates> + <status>enabled</status> + <maintenance_mode>disable</maintenance_mode> + </updates>""" + result = self.deserializer.deserialize(intext) + + self.assertEqual(dict(body=exemplar), result) + + def test_corrupt_xml(self): + self.assertRaises( + exception.MalformedRequestBody, + self.deserializer.deserialize, + utils.killer_xml_body()) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_hypervisor_status.py b/nova/tests/unit/api/openstack/compute/contrib/test_hypervisor_status.py new file mode 100644 index 0000000000..2d9187a7d1 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_hypervisor_status.py @@ -0,0 +1,92 @@ +# Copyright 2014 Intel Corp. +# +# 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 copy + +import mock + +from nova.api.openstack.compute.contrib import hypervisors as hypervisors_v2 +from nova.api.openstack.compute.plugins.v3 import hypervisors \ + as hypervisors_v21 +from nova.api.openstack import extensions +from nova import test +from nova.tests.unit.api.openstack.compute.contrib import test_hypervisors + +TEST_HYPER = dict(test_hypervisors.TEST_HYPERS[0], + service=dict(id=1, + host="compute1", + binary="nova-compute", + topic="compute_topic", + report_count=5, + disabled=False, + disabled_reason=None, + availability_zone="nova"), + ) + + +class HypervisorStatusTestV21(test.NoDBTestCase): + def _prepare_extension(self): + self.controller = hypervisors_v21.HypervisorsController() + self.controller.servicegroup_api.service_is_up = mock.MagicMock( + return_value=True) + + def test_view_hypervisor_service_status(self): + self._prepare_extension() + result = self.controller._view_hypervisor( + TEST_HYPER, False) + self.assertEqual('enabled', result['status']) + self.assertEqual('up', result['state']) + self.assertEqual('enabled', result['status']) + + self.controller.servicegroup_api.service_is_up.return_value = False + result = self.controller._view_hypervisor( + TEST_HYPER, False) + self.assertEqual('down', result['state']) + + hyper = copy.deepcopy(TEST_HYPER) + hyper['service']['disabled'] = True + result = self.controller._view_hypervisor(hyper, False) + self.assertEqual('disabled', result['status']) + + def test_view_hypervisor_detail_status(self): + self._prepare_extension() + + result = self.controller._view_hypervisor( + TEST_HYPER, True) + + self.assertEqual('enabled', result['status']) + self.assertEqual('up', result['state']) + self.assertIsNone(result['service']['disabled_reason']) + + self.controller.servicegroup_api.service_is_up.return_value = False + result = self.controller._view_hypervisor( + TEST_HYPER, True) + self.assertEqual('down', result['state']) + + hyper = copy.deepcopy(TEST_HYPER) + hyper['service']['disabled'] = True + hyper['service']['disabled_reason'] = "fake" + result = self.controller._view_hypervisor(hyper, True) + self.assertEqual('disabled', result['status'],) + self.assertEqual('fake', result['service']['disabled_reason']) + + +class HypervisorStatusTestV2(HypervisorStatusTestV21): + def _prepare_extension(self): + ext_mgr = extensions.ExtensionManager() + ext_mgr.extensions = {} + ext_mgr.extensions['os-hypervisor-status'] = True + self.controller = hypervisors_v2.HypervisorsController(ext_mgr) + self.controller.servicegroup_api.service_is_up = mock.MagicMock( + return_value=True) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_hypervisors.py b/nova/tests/unit/api/openstack/compute/contrib/test_hypervisors.py new file mode 100644 index 0000000000..9ae3c307c5 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_hypervisors.py @@ -0,0 +1,596 @@ +# Copyright (c) 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 copy + +from lxml import etree +import mock +from webob import exc + +from nova.api.openstack.compute.contrib import hypervisors as hypervisors_v2 +from nova.api.openstack.compute.plugins.v3 import hypervisors \ + as hypervisors_v21 +from nova.api.openstack import extensions +from nova import context +from nova import db +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + + +TEST_HYPERS = [ + dict(id=1, + service_id=1, + service=dict(id=1, + host="compute1", + binary="nova-compute", + topic="compute_topic", + report_count=5, + disabled=False, + disabled_reason=None, + availability_zone="nova"), + vcpus=4, + memory_mb=10 * 1024, + local_gb=250, + vcpus_used=2, + memory_mb_used=5 * 1024, + local_gb_used=125, + hypervisor_type="xen", + hypervisor_version=3, + hypervisor_hostname="hyper1", + free_ram_mb=5 * 1024, + free_disk_gb=125, + current_workload=2, + running_vms=2, + cpu_info='cpu_info', + disk_available_least=100, + host_ip='1.1.1.1'), + dict(id=2, + service_id=2, + service=dict(id=2, + host="compute2", + binary="nova-compute", + topic="compute_topic", + report_count=5, + disabled=False, + disabled_reason=None, + availability_zone="nova"), + vcpus=4, + memory_mb=10 * 1024, + local_gb=250, + vcpus_used=2, + memory_mb_used=5 * 1024, + local_gb_used=125, + hypervisor_type="xen", + hypervisor_version=3, + hypervisor_hostname="hyper2", + free_ram_mb=5 * 1024, + free_disk_gb=125, + current_workload=2, + running_vms=2, + cpu_info='cpu_info', + disk_available_least=100, + host_ip='2.2.2.2')] +TEST_SERVERS = [dict(name="inst1", uuid="uuid1", host="compute1"), + dict(name="inst2", uuid="uuid2", host="compute2"), + dict(name="inst3", uuid="uuid3", host="compute1"), + dict(name="inst4", uuid="uuid4", host="compute2")] + + +def fake_compute_node_get_all(context): + return TEST_HYPERS + + +def fake_compute_node_search_by_hypervisor(context, hypervisor_re): + return TEST_HYPERS + + +def fake_compute_node_get(context, compute_id): + for hyper in TEST_HYPERS: + if hyper['id'] == compute_id: + return hyper + raise exception.ComputeHostNotFound(host=compute_id) + + +def fake_compute_node_statistics(context): + result = dict( + count=0, + vcpus=0, + memory_mb=0, + local_gb=0, + vcpus_used=0, + memory_mb_used=0, + local_gb_used=0, + free_ram_mb=0, + free_disk_gb=0, + current_workload=0, + running_vms=0, + disk_available_least=0, + ) + + for hyper in TEST_HYPERS: + for key in result: + if key == 'count': + result[key] += 1 + else: + result[key] += hyper[key] + + return result + + +def fake_instance_get_all_by_host(context, host): + results = [] + for inst in TEST_SERVERS: + if inst['host'] == host: + results.append(inst) + return results + + +class HypervisorsTestV21(test.NoDBTestCase): + DETAIL_HYPERS_DICTS = copy.deepcopy(TEST_HYPERS) + del DETAIL_HYPERS_DICTS[0]['service_id'] + del DETAIL_HYPERS_DICTS[1]['service_id'] + DETAIL_HYPERS_DICTS[0].update({'state': 'up', + 'status': 'enabled', + 'service': dict(id=1, host='compute1', + disabled_reason=None)}) + DETAIL_HYPERS_DICTS[1].update({'state': 'up', + 'status': 'enabled', + 'service': dict(id=2, host='compute2', + disabled_reason=None)}) + + INDEX_HYPER_DICTS = [ + dict(id=1, hypervisor_hostname="hyper1", + state='up', status='enabled'), + dict(id=2, hypervisor_hostname="hyper2", + state='up', status='enabled')] + + NO_SERVER_HYPER_DICTS = copy.deepcopy(INDEX_HYPER_DICTS) + NO_SERVER_HYPER_DICTS[0].update({'servers': []}) + NO_SERVER_HYPER_DICTS[1].update({'servers': []}) + + def _get_request(self, use_admin_context): + return fakes.HTTPRequest.blank('/v2/fake/os-hypervisors/statistics', + use_admin_context=use_admin_context) + + def _set_up_controller(self): + self.controller = hypervisors_v21.HypervisorsController() + self.controller.servicegroup_api.service_is_up = mock.MagicMock( + return_value=True) + + def setUp(self): + super(HypervisorsTestV21, self).setUp() + self._set_up_controller() + + self.stubs.Set(db, 'compute_node_get_all', fake_compute_node_get_all) + self.stubs.Set(db, 'compute_node_search_by_hypervisor', + fake_compute_node_search_by_hypervisor) + self.stubs.Set(db, 'compute_node_get', + fake_compute_node_get) + self.stubs.Set(db, 'compute_node_statistics', + fake_compute_node_statistics) + self.stubs.Set(db, 'instance_get_all_by_host', + fake_instance_get_all_by_host) + + def test_view_hypervisor_nodetail_noservers(self): + result = self.controller._view_hypervisor(TEST_HYPERS[0], False) + + self.assertEqual(result, self.INDEX_HYPER_DICTS[0]) + + def test_view_hypervisor_detail_noservers(self): + result = self.controller._view_hypervisor(TEST_HYPERS[0], True) + + self.assertEqual(result, self.DETAIL_HYPERS_DICTS[0]) + + def test_view_hypervisor_servers(self): + result = self.controller._view_hypervisor(TEST_HYPERS[0], False, + TEST_SERVERS) + expected_dict = copy.deepcopy(self.INDEX_HYPER_DICTS[0]) + expected_dict.update({'servers': [ + dict(name="inst1", uuid="uuid1"), + dict(name="inst2", uuid="uuid2"), + dict(name="inst3", uuid="uuid3"), + dict(name="inst4", uuid="uuid4")]}) + + self.assertEqual(result, expected_dict) + + def test_index(self): + req = self._get_request(True) + result = self.controller.index(req) + + self.assertEqual(result, dict(hypervisors=self.INDEX_HYPER_DICTS)) + + def test_index_non_admin(self): + req = self._get_request(False) + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.index, req) + + def test_detail(self): + req = self._get_request(True) + result = self.controller.detail(req) + + self.assertEqual(result, dict(hypervisors=self.DETAIL_HYPERS_DICTS)) + + def test_detail_non_admin(self): + req = self._get_request(False) + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.detail, req) + + def test_show_noid(self): + req = self._get_request(True) + self.assertRaises(exc.HTTPNotFound, self.controller.show, req, '3') + + def test_show_non_integer_id(self): + req = self._get_request(True) + self.assertRaises(exc.HTTPNotFound, self.controller.show, req, 'abc') + + def test_show_withid(self): + req = self._get_request(True) + result = self.controller.show(req, '1') + + self.assertEqual(result, dict(hypervisor=self.DETAIL_HYPERS_DICTS[0])) + + def test_show_non_admin(self): + req = self._get_request(False) + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.show, req, '1') + + def test_uptime_noid(self): + req = self._get_request(True) + self.assertRaises(exc.HTTPNotFound, self.controller.uptime, req, '3') + + def test_uptime_notimplemented(self): + def fake_get_host_uptime(context, hyp): + raise exc.HTTPNotImplemented() + + self.stubs.Set(self.controller.host_api, 'get_host_uptime', + fake_get_host_uptime) + + req = self._get_request(True) + self.assertRaises(exc.HTTPNotImplemented, + self.controller.uptime, req, '1') + + def test_uptime_implemented(self): + def fake_get_host_uptime(context, hyp): + return "fake uptime" + + self.stubs.Set(self.controller.host_api, 'get_host_uptime', + fake_get_host_uptime) + + req = self._get_request(True) + result = self.controller.uptime(req, '1') + + expected_dict = copy.deepcopy(self.INDEX_HYPER_DICTS[0]) + expected_dict.update({'uptime': "fake uptime"}) + self.assertEqual(result, dict(hypervisor=expected_dict)) + + def test_uptime_non_integer_id(self): + req = self._get_request(True) + self.assertRaises(exc.HTTPNotFound, self.controller.uptime, req, 'abc') + + def test_uptime_non_admin(self): + req = self._get_request(False) + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.uptime, req, '1') + + def test_search(self): + req = self._get_request(True) + result = self.controller.search(req, 'hyper') + + self.assertEqual(result, dict(hypervisors=self.INDEX_HYPER_DICTS)) + + def test_search_non_admin(self): + req = self._get_request(False) + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.search, req, '1') + + def test_search_non_exist(self): + def fake_compute_node_search_by_hypervisor_return_empty(context, + hypervisor_re): + return [] + self.stubs.Set(db, 'compute_node_search_by_hypervisor', + fake_compute_node_search_by_hypervisor_return_empty) + req = self._get_request(True) + self.assertRaises(exc.HTTPNotFound, self.controller.search, req, 'a') + + def test_servers(self): + req = self._get_request(True) + result = self.controller.servers(req, 'hyper') + + expected_dict = copy.deepcopy(self.INDEX_HYPER_DICTS) + expected_dict[0].update({'servers': [ + dict(name="inst1", uuid="uuid1"), + dict(name="inst3", uuid="uuid3")]}) + expected_dict[1].update({'servers': [ + dict(name="inst2", uuid="uuid2"), + dict(name="inst4", uuid="uuid4")]}) + + self.assertEqual(result, dict(hypervisors=expected_dict)) + + def test_servers_non_id(self): + def fake_compute_node_search_by_hypervisor_return_empty(context, + hypervisor_re): + return [] + self.stubs.Set(db, 'compute_node_search_by_hypervisor', + fake_compute_node_search_by_hypervisor_return_empty) + + req = self._get_request(True) + self.assertRaises(exc.HTTPNotFound, + self.controller.servers, + req, '115') + + def test_servers_non_admin(self): + req = self._get_request(False) + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.servers, req, '1') + + def test_servers_with_non_integer_hypervisor_id(self): + def fake_compute_node_search_by_hypervisor_return_empty(context, + hypervisor_re): + return [] + self.stubs.Set(db, 'compute_node_search_by_hypervisor', + fake_compute_node_search_by_hypervisor_return_empty) + + req = self._get_request(True) + self.assertRaises(exc.HTTPNotFound, + self.controller.servers, req, 'abc') + + def test_servers_with_no_server(self): + def fake_instance_get_all_by_host_return_empty(context, hypervisor_re): + return [] + self.stubs.Set(db, 'instance_get_all_by_host', + fake_instance_get_all_by_host_return_empty) + req = self._get_request(True) + result = self.controller.servers(req, '1') + self.assertEqual(result, dict(hypervisors=self.NO_SERVER_HYPER_DICTS)) + + def test_statistics(self): + req = self._get_request(True) + result = self.controller.statistics(req) + + self.assertEqual(result, dict(hypervisor_statistics=dict( + count=2, + vcpus=8, + memory_mb=20 * 1024, + local_gb=500, + vcpus_used=4, + memory_mb_used=10 * 1024, + local_gb_used=250, + free_ram_mb=10 * 1024, + free_disk_gb=250, + current_workload=4, + running_vms=4, + disk_available_least=200))) + + def test_statistics_non_admin(self): + req = self._get_request(False) + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.statistics, req) + + +class HypervisorsTestV2(HypervisorsTestV21): + DETAIL_HYPERS_DICTS = copy.deepcopy( + HypervisorsTestV21.DETAIL_HYPERS_DICTS) + del DETAIL_HYPERS_DICTS[0]['state'] + del DETAIL_HYPERS_DICTS[1]['state'] + del DETAIL_HYPERS_DICTS[0]['status'] + del DETAIL_HYPERS_DICTS[1]['status'] + del DETAIL_HYPERS_DICTS[0]['service']['disabled_reason'] + del DETAIL_HYPERS_DICTS[1]['service']['disabled_reason'] + del DETAIL_HYPERS_DICTS[0]['host_ip'] + del DETAIL_HYPERS_DICTS[1]['host_ip'] + + INDEX_HYPER_DICTS = copy.deepcopy(HypervisorsTestV21.INDEX_HYPER_DICTS) + del INDEX_HYPER_DICTS[0]['state'] + del INDEX_HYPER_DICTS[1]['state'] + del INDEX_HYPER_DICTS[0]['status'] + del INDEX_HYPER_DICTS[1]['status'] + + NO_SERVER_HYPER_DICTS = copy.deepcopy( + HypervisorsTestV21.NO_SERVER_HYPER_DICTS) + del NO_SERVER_HYPER_DICTS[0]['state'] + del NO_SERVER_HYPER_DICTS[1]['state'] + del NO_SERVER_HYPER_DICTS[0]['status'] + del NO_SERVER_HYPER_DICTS[1]['status'] + del NO_SERVER_HYPER_DICTS[0]['servers'] + del NO_SERVER_HYPER_DICTS[1]['servers'] + + def _set_up_controller(self): + self.context = context.get_admin_context() + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = hypervisors_v2.HypervisorsController(self.ext_mgr) + + +class HypervisorsSerializersTest(test.NoDBTestCase): + def compare_to_exemplar(self, exemplar, hyper): + # Check attributes + for key, value in exemplar.items(): + if key in ('service', 'servers'): + # These turn into child elements and get tested + # separately below... + continue + + self.assertEqual(str(value), hyper.get(key)) + + # Check child elements + required_children = set([child for child in ('service', 'servers') + if child in exemplar]) + for child in hyper: + self.assertIn(child.tag, required_children) + required_children.remove(child.tag) + + # Check the node... + if child.tag == 'service': + for key, value in exemplar['service'].items(): + self.assertEqual(str(value), child.get(key)) + elif child.tag == 'servers': + for idx, grandchild in enumerate(child): + self.assertEqual('server', grandchild.tag) + for key, value in exemplar['servers'][idx].items(): + self.assertEqual(str(value), grandchild.get(key)) + + # Are they all accounted for? + self.assertEqual(len(required_children), 0) + + def test_index_serializer(self): + serializer = hypervisors_v2.HypervisorIndexTemplate() + exemplar = dict(hypervisors=[ + dict(hypervisor_hostname="hyper1", + id=1), + dict(hypervisor_hostname="hyper2", + id=2)]) + text = serializer.serialize(exemplar) + tree = etree.fromstring(text) + + self.assertEqual('hypervisors', tree.tag) + self.assertEqual(len(exemplar['hypervisors']), len(tree)) + for idx, hyper in enumerate(tree): + self.assertEqual('hypervisor', hyper.tag) + self.compare_to_exemplar(exemplar['hypervisors'][idx], hyper) + + def test_detail_serializer(self): + serializer = hypervisors_v2.HypervisorDetailTemplate() + exemplar = dict(hypervisors=[ + dict(hypervisor_hostname="hyper1", + id=1, + vcpus=4, + memory_mb=10 * 1024, + local_gb=500, + vcpus_used=2, + memory_mb_used=5 * 1024, + local_gb_used=250, + hypervisor_type='xen', + hypervisor_version=3, + free_ram_mb=5 * 1024, + free_disk_gb=250, + current_workload=2, + running_vms=2, + cpu_info="json data", + disk_available_least=100, + host_ip='1.1.1.1', + service=dict(id=1, host="compute1")), + dict(hypervisor_hostname="hyper2", + id=2, + vcpus=4, + memory_mb=10 * 1024, + local_gb=500, + vcpus_used=2, + memory_mb_used=5 * 1024, + local_gb_used=250, + hypervisor_type='xen', + hypervisor_version=3, + free_ram_mb=5 * 1024, + free_disk_gb=250, + current_workload=2, + running_vms=2, + cpu_info="json data", + disk_available_least=100, + host_ip='2.2.2.2', + service=dict(id=2, host="compute2"))]) + text = serializer.serialize(exemplar) + tree = etree.fromstring(text) + + self.assertEqual('hypervisors', tree.tag) + self.assertEqual(len(exemplar['hypervisors']), len(tree)) + for idx, hyper in enumerate(tree): + self.assertEqual('hypervisor', hyper.tag) + self.compare_to_exemplar(exemplar['hypervisors'][idx], hyper) + + def test_show_serializer(self): + serializer = hypervisors_v2.HypervisorTemplate() + exemplar = dict(hypervisor=dict( + hypervisor_hostname="hyper1", + id=1, + vcpus=4, + memory_mb=10 * 1024, + local_gb=500, + vcpus_used=2, + memory_mb_used=5 * 1024, + local_gb_used=250, + hypervisor_type='xen', + hypervisor_version=3, + free_ram_mb=5 * 1024, + free_disk_gb=250, + current_workload=2, + running_vms=2, + cpu_info="json data", + disk_available_least=100, + host_ip='1.1.1.1', + service=dict(id=1, host="compute1"))) + text = serializer.serialize(exemplar) + tree = etree.fromstring(text) + + self.assertEqual('hypervisor', tree.tag) + self.compare_to_exemplar(exemplar['hypervisor'], tree) + + def test_uptime_serializer(self): + serializer = hypervisors_v2.HypervisorUptimeTemplate() + exemplar = dict(hypervisor=dict( + hypervisor_hostname="hyper1", + id=1, + uptime='fake uptime')) + text = serializer.serialize(exemplar) + tree = etree.fromstring(text) + + self.assertEqual('hypervisor', tree.tag) + self.compare_to_exemplar(exemplar['hypervisor'], tree) + + def test_servers_serializer(self): + serializer = hypervisors_v2.HypervisorServersTemplate() + exemplar = dict(hypervisors=[ + dict(hypervisor_hostname="hyper1", + id=1, + servers=[ + dict(name="inst1", + uuid="uuid1"), + dict(name="inst2", + uuid="uuid2")]), + dict(hypervisor_hostname="hyper2", + id=2, + servers=[ + dict(name="inst3", + uuid="uuid3"), + dict(name="inst4", + uuid="uuid4")])]) + text = serializer.serialize(exemplar) + tree = etree.fromstring(text) + + self.assertEqual('hypervisors', tree.tag) + self.assertEqual(len(exemplar['hypervisors']), len(tree)) + for idx, hyper in enumerate(tree): + self.assertEqual('hypervisor', hyper.tag) + self.compare_to_exemplar(exemplar['hypervisors'][idx], hyper) + + def test_statistics_serializer(self): + serializer = hypervisors_v2.HypervisorStatisticsTemplate() + exemplar = dict(hypervisor_statistics=dict( + count=2, + vcpus=8, + memory_mb=20 * 1024, + local_gb=500, + vcpus_used=4, + memory_mb_used=10 * 1024, + local_gb_used=250, + free_ram_mb=10 * 1024, + free_disk_gb=250, + current_workload=4, + running_vms=4, + disk_available_least=200)) + text = serializer.serialize(exemplar) + tree = etree.fromstring(text) + + self.assertEqual('hypervisor_statistics', tree.tag) + self.compare_to_exemplar(exemplar['hypervisor_statistics'], tree) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_image_size.py b/nova/tests/unit/api/openstack/compute/contrib/test_image_size.py new file mode 100644 index 0000000000..2a8d95cb86 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_image_size.py @@ -0,0 +1,138 @@ +# Copyright 2013 Rackspace Hosting +# 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import image_size +from nova.image import glance +from nova import test +from nova.tests.unit.api.openstack import fakes + +NOW_API_FORMAT = "2010-10-11T10:30:22Z" +IMAGES = [{ + 'id': '123', + 'name': 'public image', + 'metadata': {'key1': 'value1'}, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ACTIVE', + 'progress': 100, + 'minDisk': 10, + 'minRam': 128, + 'size': 12345678, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/123", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/123", + }], + }, + { + 'id': '124', + 'name': 'queued snapshot', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'SAVING', + 'progress': 25, + 'minDisk': 0, + 'minRam': 0, + 'size': 87654321, + "links": [{ + "rel": "self", + "href": "http://localhost/v2/fake/images/124", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/124", + }], + }] + + +def fake_show(*args, **kwargs): + return IMAGES[0] + + +def fake_detail(*args, **kwargs): + return IMAGES + + +class ImageSizeTestV21(test.NoDBTestCase): + content_type = 'application/json' + prefix = 'OS-EXT-IMG-SIZE' + + def setUp(self): + super(ImageSizeTestV21, self).setUp() + self.stubs.Set(glance.GlanceImageService, 'show', fake_show) + self.stubs.Set(glance.GlanceImageService, 'detail', fake_detail) + self.flags(osapi_compute_extension=['nova.api.openstack.compute' + '.contrib.image_size.Image_size']) + + def _make_request(self, url): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + res = req.get_response(self._get_app()) + return res + + def _get_app(self): + return fakes.wsgi_app_v21() + + def _get_image(self, body): + return jsonutils.loads(body).get('image') + + def _get_images(self, body): + return jsonutils.loads(body).get('images') + + def assertImageSize(self, image, size): + self.assertEqual(image.get('%s:size' % self.prefix), size) + + def test_show(self): + url = '/v2/fake/images/1' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + image = self._get_image(res.body) + self.assertImageSize(image, 12345678) + + def test_detail(self): + url = '/v2/fake/images/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + images = self._get_images(res.body) + self.assertImageSize(images[0], 12345678) + self.assertImageSize(images[1], 87654321) + + +class ImageSizeTestV2(ImageSizeTestV21): + def _get_app(self): + return fakes.wsgi_app() + + +class ImageSizeXmlTest(ImageSizeTestV2): + content_type = 'application/xml' + prefix = '{%s}' % image_size.Image_size.namespace + + def _get_image(self, body): + return etree.XML(body) + + def _get_images(self, body): + return etree.XML(body).getchildren() + + def assertImageSize(self, image, size): + self.assertEqual(int(image.get('%ssize' % self.prefix)), size) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_instance_actions.py b/nova/tests/unit/api/openstack/compute/contrib/test_instance_actions.py new file mode 100644 index 0000000000..a5ea3784e3 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_instance_actions.py @@ -0,0 +1,327 @@ +# Copyright 2013 Rackspace Hosting +# 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 copy +import uuid + +from lxml import etree +from webob import exc + +from nova.api.openstack.compute.contrib import instance_actions \ + as instance_actions_v2 +from nova.api.openstack.compute.plugins.v3 import instance_actions \ + as instance_actions_v21 +from nova.compute import api as compute_api +from nova import db +from nova.db.sqlalchemy import models +from nova import exception +from nova.openstack.common import policy as common_policy +from nova import policy +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance +from nova.tests.unit import fake_server_actions + +FAKE_UUID = fake_server_actions.FAKE_UUID +FAKE_REQUEST_ID = fake_server_actions.FAKE_REQUEST_ID1 + + +def format_action(action): + '''Remove keys that aren't serialized.''' + to_delete = ('id', 'finish_time', 'created_at', 'updated_at', 'deleted_at', + 'deleted') + for key in to_delete: + if key in action: + del(action[key]) + if 'start_time' in action: + # NOTE(danms): Without WSGI above us, these will be just stringified + action['start_time'] = str(action['start_time'].replace(tzinfo=None)) + for event in action.get('events', []): + format_event(event) + return action + + +def format_event(event): + '''Remove keys that aren't serialized.''' + to_delete = ('id', 'created_at', 'updated_at', 'deleted_at', 'deleted', + 'action_id') + for key in to_delete: + if key in event: + del(event[key]) + if 'start_time' in event: + # NOTE(danms): Without WSGI above us, these will be just stringified + event['start_time'] = str(event['start_time'].replace(tzinfo=None)) + if 'finish_time' in event: + # NOTE(danms): Without WSGI above us, these will be just stringified + event['finish_time'] = str(event['finish_time'].replace(tzinfo=None)) + return event + + +class InstanceActionsPolicyTestV21(test.NoDBTestCase): + instance_actions = instance_actions_v21 + + def setUp(self): + super(InstanceActionsPolicyTestV21, self).setUp() + self.controller = self.instance_actions.InstanceActionsController() + + def _get_http_req(self, action): + fake_url = '/123/servers/12/%s' % action + return fakes.HTTPRequest.blank(fake_url) + + def _set_policy_rules(self): + rules = {'compute:get': common_policy.parse_rule(''), + 'compute_extension:v3:os-instance-actions': + common_policy.parse_rule('project_id:%(project_id)s')} + policy.set_rules(rules) + + def test_list_actions_restricted_by_project(self): + self._set_policy_rules() + + def fake_instance_get_by_uuid(context, instance_id, + columns_to_join=None, + use_slave=False): + return fake_instance.fake_db_instance( + **{'name': 'fake', 'project_id': '%s_unequal' % + context.project_id}) + + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + req = self._get_http_req('os-instance-actions') + self.assertRaises(exception.Forbidden, self.controller.index, req, + str(uuid.uuid4())) + + def test_get_action_restricted_by_project(self): + self._set_policy_rules() + + def fake_instance_get_by_uuid(context, instance_id, + columns_to_join=None, + use_slave=False): + return fake_instance.fake_db_instance( + **{'name': 'fake', 'project_id': '%s_unequal' % + context.project_id}) + + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + req = self._get_http_req('os-instance-actions/1') + self.assertRaises(exception.Forbidden, self.controller.show, req, + str(uuid.uuid4()), '1') + + +class InstanceActionsPolicyTestV2(InstanceActionsPolicyTestV21): + instance_actions = instance_actions_v2 + + def _set_policy_rules(self): + rules = {'compute:get': common_policy.parse_rule(''), + 'compute_extension:instance_actions': + common_policy.parse_rule('project_id:%(project_id)s')} + policy.set_rules(rules) + + +class InstanceActionsTestV21(test.NoDBTestCase): + instance_actions = instance_actions_v21 + + def setUp(self): + super(InstanceActionsTestV21, self).setUp() + self.controller = self.instance_actions.InstanceActionsController() + self.fake_actions = copy.deepcopy(fake_server_actions.FAKE_ACTIONS) + self.fake_events = copy.deepcopy(fake_server_actions.FAKE_EVENTS) + + def fake_get(self, context, instance_uuid, expected_attrs=None, + want_objects=False): + return {'uuid': instance_uuid} + + def fake_instance_get_by_uuid(context, instance_id, use_slave=False): + return {'name': 'fake', 'project_id': context.project_id} + + self.stubs.Set(compute_api.API, 'get', fake_get) + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + + def _get_http_req(self, action, use_admin_context=False): + fake_url = '/123/servers/12/%s' % action + return fakes.HTTPRequest.blank(fake_url, + use_admin_context=use_admin_context) + + def _set_policy_rules(self): + rules = {'compute:get': common_policy.parse_rule(''), + 'compute_extension:v3:os-instance-actions': + common_policy.parse_rule(''), + 'compute_extension:v3:os-instance-actions:events': + common_policy.parse_rule('is_admin:True')} + policy.set_rules(rules) + + def test_list_actions(self): + def fake_get_actions(context, uuid): + actions = [] + for act in self.fake_actions[uuid].itervalues(): + action = models.InstanceAction() + action.update(act) + actions.append(action) + return actions + + self.stubs.Set(db, 'actions_get', fake_get_actions) + req = self._get_http_req('os-instance-actions') + res_dict = self.controller.index(req, FAKE_UUID) + for res in res_dict['instanceActions']: + fake_action = self.fake_actions[FAKE_UUID][res['request_id']] + self.assertEqual(format_action(fake_action), format_action(res)) + + def test_get_action_with_events_allowed(self): + def fake_get_action(context, uuid, request_id): + action = models.InstanceAction() + action.update(self.fake_actions[uuid][request_id]) + return action + + def fake_get_events(context, action_id): + events = [] + for evt in self.fake_events[action_id]: + event = models.InstanceActionEvent() + event.update(evt) + events.append(event) + return events + + self.stubs.Set(db, 'action_get_by_request_id', fake_get_action) + self.stubs.Set(db, 'action_events_get', fake_get_events) + req = self._get_http_req('os-instance-actions/1', + use_admin_context=True) + res_dict = self.controller.show(req, FAKE_UUID, FAKE_REQUEST_ID) + fake_action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID] + fake_events = self.fake_events[fake_action['id']] + fake_action['events'] = fake_events + self.assertEqual(format_action(fake_action), + format_action(res_dict['instanceAction'])) + + def test_get_action_with_events_not_allowed(self): + def fake_get_action(context, uuid, request_id): + return self.fake_actions[uuid][request_id] + + def fake_get_events(context, action_id): + return self.fake_events[action_id] + + self.stubs.Set(db, 'action_get_by_request_id', fake_get_action) + self.stubs.Set(db, 'action_events_get', fake_get_events) + + self._set_policy_rules() + req = self._get_http_req('os-instance-actions/1') + res_dict = self.controller.show(req, FAKE_UUID, FAKE_REQUEST_ID) + fake_action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID] + self.assertEqual(format_action(fake_action), + format_action(res_dict['instanceAction'])) + + def test_action_not_found(self): + def fake_no_action(context, uuid, action_id): + return None + + self.stubs.Set(db, 'action_get_by_request_id', fake_no_action) + req = self._get_http_req('os-instance-actions/1') + self.assertRaises(exc.HTTPNotFound, self.controller.show, req, + FAKE_UUID, FAKE_REQUEST_ID) + + def test_index_instance_not_found(self): + def fake_get(self, context, instance_uuid, expected_attrs=None, + want_objects=False): + raise exception.InstanceNotFound(instance_id=instance_uuid) + self.stubs.Set(compute_api.API, 'get', fake_get) + req = self._get_http_req('os-instance-actions') + self.assertRaises(exc.HTTPNotFound, self.controller.index, req, + FAKE_UUID) + + def test_show_instance_not_found(self): + def fake_get(self, context, instance_uuid, expected_attrs=None, + want_objects=False): + raise exception.InstanceNotFound(instance_id=instance_uuid) + self.stubs.Set(compute_api.API, 'get', fake_get) + req = self._get_http_req('os-instance-actions/fake') + self.assertRaises(exc.HTTPNotFound, self.controller.show, req, + FAKE_UUID, 'fake') + + +class InstanceActionsTestV2(InstanceActionsTestV21): + instance_actions = instance_actions_v2 + + def _set_policy_rules(self): + rules = {'compute:get': common_policy.parse_rule(''), + 'compute_extension:instance_actions': + common_policy.parse_rule(''), + 'compute_extension:instance_actions:events': + common_policy.parse_rule('is_admin:True')} + policy.set_rules(rules) + + +class InstanceActionsSerializerTestV2(test.NoDBTestCase): + def setUp(self): + super(InstanceActionsSerializerTestV2, self).setUp() + self.fake_actions = copy.deepcopy(fake_server_actions.FAKE_ACTIONS) + self.fake_events = copy.deepcopy(fake_server_actions.FAKE_EVENTS) + + def _verify_instance_action_attachment(self, attach, tree): + for key in attach.keys(): + if key != 'events': + self.assertEqual(attach[key], tree.get(key), + '%s did not match' % key) + + def _verify_instance_action_event_attachment(self, attach, tree): + for key in attach.keys(): + self.assertEqual(attach[key], tree.get(key), + '%s did not match' % key) + + def test_instance_action_serializer(self): + serializer = instance_actions_v2.InstanceActionTemplate() + action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID] + text = serializer.serialize({'instanceAction': action}) + tree = etree.fromstring(text) + + action = format_action(action) + self.assertEqual('instanceAction', tree.tag) + self._verify_instance_action_attachment(action, tree) + found_events = False + for child in tree: + if child.tag == 'events': + found_events = True + self.assertFalse(found_events) + + def test_instance_action_events_serializer(self): + serializer = instance_actions_v2.InstanceActionTemplate() + action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID] + event = self.fake_events[action['id']][0] + action['events'] = [dict(event), dict(event)] + text = serializer.serialize({'instanceAction': action}) + tree = etree.fromstring(text) + + action = format_action(action) + self.assertEqual('instanceAction', tree.tag) + self._verify_instance_action_attachment(action, tree) + + event = format_event(event) + found_events = False + for child in tree: + if child.tag == 'events': + found_events = True + for key in event: + self.assertEqual(event[key], child.get(key)) + self.assertTrue(found_events) + + def test_instance_actions_serializer(self): + serializer = instance_actions_v2.InstanceActionsTemplate() + action_list = self.fake_actions[FAKE_UUID].values() + text = serializer.serialize({'instanceActions': action_list}) + tree = etree.fromstring(text) + + action_list = [format_action(action) for action in action_list] + self.assertEqual('instanceActions', tree.tag) + self.assertEqual(len(action_list), len(tree)) + for idx, child in enumerate(tree): + self.assertEqual('instanceAction', child.tag) + request_id = child.get('request_id') + self._verify_instance_action_attachment( + self.fake_actions[FAKE_UUID][request_id], + child) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_instance_usage_audit_log.py b/nova/tests/unit/api/openstack/compute/contrib/test_instance_usage_audit_log.py new file mode 100644 index 0000000000..1ae85c8625 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_instance_usage_audit_log.py @@ -0,0 +1,210 @@ +# Copyright (c) 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 datetime + +from oslo.utils import timeutils + +from nova.api.openstack.compute.contrib import instance_usage_audit_log as ial +from nova import context +from nova import db +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit.objects import test_service +from nova import utils + + +service_base = test_service.fake_service +TEST_COMPUTE_SERVICES = [dict(service_base, host='foo', topic='compute'), + dict(service_base, host='bar', topic='compute'), + dict(service_base, host='baz', topic='compute'), + dict(service_base, host='plonk', topic='compute'), + dict(service_base, host='wibble', topic='bogus'), + ] + + +begin1 = datetime.datetime(2012, 7, 4, 6, 0, 0) +begin2 = end1 = datetime.datetime(2012, 7, 5, 6, 0, 0) +begin3 = end2 = datetime.datetime(2012, 7, 6, 6, 0, 0) +end3 = datetime.datetime(2012, 7, 7, 6, 0, 0) + + +# test data + + +TEST_LOGS1 = [ + # all services done, no errors. + dict(host="plonk", period_beginning=begin1, period_ending=end1, + state="DONE", errors=0, task_items=23, message="test1"), + dict(host="baz", period_beginning=begin1, period_ending=end1, + state="DONE", errors=0, task_items=17, message="test2"), + dict(host="bar", period_beginning=begin1, period_ending=end1, + state="DONE", errors=0, task_items=10, message="test3"), + dict(host="foo", period_beginning=begin1, period_ending=end1, + state="DONE", errors=0, task_items=7, message="test4"), + ] + + +TEST_LOGS2 = [ + # some still running... + dict(host="plonk", period_beginning=begin2, period_ending=end2, + state="DONE", errors=0, task_items=23, message="test5"), + dict(host="baz", period_beginning=begin2, period_ending=end2, + state="DONE", errors=0, task_items=17, message="test6"), + dict(host="bar", period_beginning=begin2, period_ending=end2, + state="RUNNING", errors=0, task_items=10, message="test7"), + dict(host="foo", period_beginning=begin2, period_ending=end2, + state="DONE", errors=0, task_items=7, message="test8"), + ] + + +TEST_LOGS3 = [ + # some errors.. + dict(host="plonk", period_beginning=begin3, period_ending=end3, + state="DONE", errors=0, task_items=23, message="test9"), + dict(host="baz", period_beginning=begin3, period_ending=end3, + state="DONE", errors=2, task_items=17, message="test10"), + dict(host="bar", period_beginning=begin3, period_ending=end3, + state="DONE", errors=0, task_items=10, message="test11"), + dict(host="foo", period_beginning=begin3, period_ending=end3, + state="DONE", errors=1, task_items=7, message="test12"), + ] + + +def fake_task_log_get_all(context, task_name, begin, end, + host=None, state=None): + assert task_name == "instance_usage_audit" + + if begin == begin1 and end == end1: + return TEST_LOGS1 + if begin == begin2 and end == end2: + return TEST_LOGS2 + if begin == begin3 and end == end3: + return TEST_LOGS3 + raise AssertionError("Invalid date %s to %s" % (begin, end)) + + +def fake_last_completed_audit_period(unit=None, before=None): + audit_periods = [(begin3, end3), + (begin2, end2), + (begin1, end1)] + if before is not None: + for begin, end in audit_periods: + if before > end: + return begin, end + raise AssertionError("Invalid before date %s" % (before)) + return begin1, end1 + + +class InstanceUsageAuditLogTest(test.NoDBTestCase): + def setUp(self): + super(InstanceUsageAuditLogTest, self).setUp() + self.context = context.get_admin_context() + timeutils.set_time_override(datetime.datetime(2012, 7, 5, 10, 0, 0)) + self.controller = ial.InstanceUsageAuditLogController() + self.host_api = self.controller.host_api + + def fake_service_get_all(context, disabled): + self.assertIsNone(disabled) + return TEST_COMPUTE_SERVICES + + self.stubs.Set(utils, 'last_completed_audit_period', + fake_last_completed_audit_period) + self.stubs.Set(db, 'service_get_all', + fake_service_get_all) + self.stubs.Set(db, 'task_log_get_all', + fake_task_log_get_all) + + def tearDown(self): + super(InstanceUsageAuditLogTest, self).tearDown() + timeutils.clear_time_override() + + def test_index(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-instance_usage_audit_log', + use_admin_context=True) + result = self.controller.index(req) + self.assertIn('instance_usage_audit_logs', result) + logs = result['instance_usage_audit_logs'] + self.assertEqual(57, logs['total_instances']) + self.assertEqual(0, logs['total_errors']) + self.assertEqual(4, len(logs['log'])) + self.assertEqual(4, logs['num_hosts']) + self.assertEqual(4, logs['num_hosts_done']) + self.assertEqual(0, logs['num_hosts_running']) + self.assertEqual(0, logs['num_hosts_not_run']) + self.assertEqual("ALL hosts done. 0 errors.", logs['overall_status']) + + def test_index_non_admin(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-instance_usage_audit_log', + use_admin_context=False) + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.index, req) + + def test_show(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/os-instance_usage_audit_log/show', + use_admin_context=True) + result = self.controller.show(req, '2012-07-05 10:00:00') + self.assertIn('instance_usage_audit_log', result) + logs = result['instance_usage_audit_log'] + self.assertEqual(57, logs['total_instances']) + self.assertEqual(0, logs['total_errors']) + self.assertEqual(4, len(logs['log'])) + self.assertEqual(4, logs['num_hosts']) + self.assertEqual(4, logs['num_hosts_done']) + self.assertEqual(0, logs['num_hosts_running']) + self.assertEqual(0, logs['num_hosts_not_run']) + self.assertEqual("ALL hosts done. 0 errors.", logs['overall_status']) + + def test_show_non_admin(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-instance_usage_audit_log', + use_admin_context=False) + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.show, req, '2012-07-05 10:00:00') + + def test_show_with_running(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/os-instance_usage_audit_log/show', + use_admin_context=True) + result = self.controller.show(req, '2012-07-06 10:00:00') + self.assertIn('instance_usage_audit_log', result) + logs = result['instance_usage_audit_log'] + self.assertEqual(57, logs['total_instances']) + self.assertEqual(0, logs['total_errors']) + self.assertEqual(4, len(logs['log'])) + self.assertEqual(4, logs['num_hosts']) + self.assertEqual(3, logs['num_hosts_done']) + self.assertEqual(1, logs['num_hosts_running']) + self.assertEqual(0, logs['num_hosts_not_run']) + self.assertEqual("3 of 4 hosts done. 0 errors.", + logs['overall_status']) + + def test_show_with_errors(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/os-instance_usage_audit_log/show', + use_admin_context=True) + result = self.controller.show(req, '2012-07-07 10:00:00') + self.assertIn('instance_usage_audit_log', result) + logs = result['instance_usage_audit_log'] + self.assertEqual(57, logs['total_instances']) + self.assertEqual(3, logs['total_errors']) + self.assertEqual(4, len(logs['log'])) + self.assertEqual(4, logs['num_hosts']) + self.assertEqual(4, logs['num_hosts_done']) + self.assertEqual(0, logs['num_hosts_running']) + self.assertEqual(0, logs['num_hosts_not_run']) + self.assertEqual("ALL hosts done. 3 errors.", + logs['overall_status']) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py b/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py new file mode 100644 index 0000000000..6a6c6f0736 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py @@ -0,0 +1,497 @@ +# Copyright 2011 Eldar Nugaev +# 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import keypairs as keypairs_v2 +from nova.api.openstack.compute.plugins.v3 import keypairs as keypairs_v21 +from nova.api.openstack import wsgi +from nova import db +from nova import exception +from nova.openstack.common import policy as common_policy +from nova import policy +from nova import quota +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit.objects import test_keypair + + +QUOTAS = quota.QUOTAS + + +keypair_data = { + 'public_key': 'FAKE_KEY', + 'fingerprint': 'FAKE_FINGERPRINT', +} + + +def fake_keypair(name): + return dict(test_keypair.fake_keypair, + name=name, **keypair_data) + + +def db_key_pair_get_all_by_user(self, user_id): + return [fake_keypair('FAKE')] + + +def db_key_pair_create(self, keypair): + return fake_keypair(name=keypair['name']) + + +def db_key_pair_destroy(context, user_id, name): + if not (user_id and name): + raise Exception() + + +def db_key_pair_create_duplicate(context, keypair): + raise exception.KeyPairExists(key_name=keypair.get('name', '')) + + +class KeypairsTestV21(test.TestCase): + base_url = '/v2/fake' + + def _setup_app(self): + self.app = fakes.wsgi_app_v21(init_only=('os-keypairs', 'servers')) + self.app_server = self.app + + def setUp(self): + super(KeypairsTestV21, self).setUp() + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + + self.stubs.Set(db, "key_pair_get_all_by_user", + db_key_pair_get_all_by_user) + self.stubs.Set(db, "key_pair_create", + db_key_pair_create) + self.stubs.Set(db, "key_pair_destroy", + db_key_pair_destroy) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Keypairs']) + self._setup_app() + + def test_keypair_list(self): + req = webob.Request.blank(self.base_url + '/os-keypairs') + res = req.get_response(self.app) + self.assertEqual(res.status_int, 200) + res_dict = jsonutils.loads(res.body) + response = {'keypairs': [{'keypair': dict(keypair_data, name='FAKE')}]} + self.assertEqual(res_dict, response) + + def test_keypair_create(self): + body = {'keypair': {'name': 'create_test'}} + req = webob.Request.blank(self.base_url + '/os-keypairs') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + self.assertEqual(res.status_int, 200) + res_dict = jsonutils.loads(res.body) + self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0) + self.assertTrue(len(res_dict['keypair']['private_key']) > 0) + + def _test_keypair_create_bad_request_case(self, body): + req = webob.Request.blank(self.base_url + '/os-keypairs') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + + def test_keypair_create_with_empty_name(self): + body = {'keypair': {'name': ''}} + self._test_keypair_create_bad_request_case(body) + + def test_keypair_create_with_name_too_long(self): + body = { + 'keypair': { + 'name': 'a' * 256 + } + } + self._test_keypair_create_bad_request_case(body) + + def test_keypair_create_with_non_alphanumeric_name(self): + body = { + 'keypair': { + 'name': 'test/keypair' + } + } + self._test_keypair_create_bad_request_case(body) + + def test_keypair_import_bad_key(self): + body = { + 'keypair': { + 'name': 'create_test', + 'public_key': 'ssh-what negative', + }, + } + self._test_keypair_create_bad_request_case(body) + + def test_keypair_create_with_invalid_keypair_body(self): + body = {'alpha': {'name': 'create_test'}} + self._test_keypair_create_bad_request_case(body) + + def test_keypair_import(self): + body = { + 'keypair': { + 'name': 'create_test', + 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznA' + 'x9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4' + 'a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6Y' + 'Qj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymi' + 'MZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7lj' + 'j5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1' + 'GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGc' + 'j7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwB' + 'bHkXa6OciiJDvkRzJXzf', + }, + } + + req = webob.Request.blank(self.base_url + '/os-keypairs') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + self.assertEqual(res.status_int, 200) + # FIXME(ja): sholud we check that public_key was sent to create? + res_dict = jsonutils.loads(res.body) + self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0) + self.assertNotIn('private_key', res_dict['keypair']) + + def test_keypair_import_quota_limit(self): + + def fake_quotas_count(self, context, resource, *args, **kwargs): + return 100 + + self.stubs.Set(QUOTAS, "count", fake_quotas_count) + + body = { + 'keypair': { + 'name': 'create_test', + 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznA' + 'x9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4' + 'a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6Y' + 'Qj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymi' + 'MZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7lj' + 'j5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1' + 'GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGc' + 'j7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwB' + 'bHkXa6OciiJDvkRzJXzf', + }, + } + + req = webob.Request.blank(self.base_url + '/os-keypairs') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + self.assertEqual(res.status_int, 403) + res_dict = jsonutils.loads(res.body) + self.assertEqual( + "Quota exceeded, too many key pairs.", + res_dict['forbidden']['message']) + + def test_keypair_create_quota_limit(self): + + def fake_quotas_count(self, context, resource, *args, **kwargs): + return 100 + + self.stubs.Set(QUOTAS, "count", fake_quotas_count) + + body = { + 'keypair': { + 'name': 'create_test', + }, + } + + req = webob.Request.blank(self.base_url + '/os-keypairs') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + self.assertEqual(res.status_int, 403) + res_dict = jsonutils.loads(res.body) + self.assertEqual( + "Quota exceeded, too many key pairs.", + res_dict['forbidden']['message']) + + def test_keypair_create_duplicate(self): + self.stubs.Set(db, "key_pair_create", db_key_pair_create_duplicate) + body = {'keypair': {'name': 'create_duplicate'}} + req = webob.Request.blank(self.base_url + '/os-keypairs') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + self.assertEqual(res.status_int, 409) + res_dict = jsonutils.loads(res.body) + self.assertEqual( + "Key pair 'create_duplicate' already exists.", + res_dict['conflictingRequest']['message']) + + def test_keypair_delete(self): + req = webob.Request.blank(self.base_url + '/os-keypairs/FAKE') + req.method = 'DELETE' + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + self.assertEqual(res.status_int, 202) + + def test_keypair_get_keypair_not_found(self): + req = webob.Request.blank(self.base_url + '/os-keypairs/DOESNOTEXIST') + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_keypair_delete_not_found(self): + + def db_key_pair_get_not_found(context, user_id, name): + raise exception.KeypairNotFound(user_id=user_id, name=name) + + self.stubs.Set(db, "key_pair_get", + db_key_pair_get_not_found) + req = webob.Request.blank(self.base_url + '/os-keypairs/WHAT') + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_keypair_show(self): + + def _db_key_pair_get(context, user_id, name): + return dict(test_keypair.fake_keypair, + name='foo', public_key='XXX', fingerprint='YYY') + + self.stubs.Set(db, "key_pair_get", _db_key_pair_get) + + req = webob.Request.blank(self.base_url + '/os-keypairs/FAKE') + req.method = 'GET' + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + res_dict = jsonutils.loads(res.body) + self.assertEqual(res.status_int, 200) + self.assertEqual('foo', res_dict['keypair']['name']) + self.assertEqual('XXX', res_dict['keypair']['public_key']) + self.assertEqual('YYY', res_dict['keypair']['fingerprint']) + + def test_keypair_show_not_found(self): + + def _db_key_pair_get(context, user_id, name): + raise exception.KeypairNotFound(user_id=user_id, name=name) + + self.stubs.Set(db, "key_pair_get", _db_key_pair_get) + + req = webob.Request.blank(self.base_url + '/os-keypairs/FAKE') + req.method = 'GET' + req.headers['Content-Type'] = 'application/json' + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_show_server(self): + self.stubs.Set(db, 'instance_get', + fakes.fake_instance_get()) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get()) + req = webob.Request.blank(self.base_url + '/servers/1') + req.headers['Content-Type'] = 'application/json' + response = req.get_response(self.app_server) + self.assertEqual(response.status_int, 200) + res_dict = jsonutils.loads(response.body) + self.assertIn('key_name', res_dict['server']) + self.assertEqual(res_dict['server']['key_name'], '') + + def test_detail_servers(self): + self.stubs.Set(db, 'instance_get_all_by_filters', + fakes.fake_instance_get_all_by_filters()) + req = fakes.HTTPRequest.blank(self.base_url + '/servers/detail') + res = req.get_response(self.app_server) + server_dicts = jsonutils.loads(res.body)['servers'] + self.assertEqual(len(server_dicts), 5) + + for server_dict in server_dicts: + self.assertIn('key_name', server_dict) + self.assertEqual(server_dict['key_name'], '') + + +class KeypairPolicyTestV21(test.TestCase): + KeyPairController = keypairs_v21.KeypairController() + policy_path = 'compute_extension:v3:os-keypairs' + base_url = '/v2/fake' + + def setUp(self): + super(KeypairPolicyTestV21, self).setUp() + + def _db_key_pair_get(context, user_id, name): + return dict(test_keypair.fake_keypair, + name='foo', public_key='XXX', fingerprint='YYY') + + self.stubs.Set(db, "key_pair_get", + _db_key_pair_get) + self.stubs.Set(db, "key_pair_get_all_by_user", + db_key_pair_get_all_by_user) + self.stubs.Set(db, "key_pair_create", + db_key_pair_create) + self.stubs.Set(db, "key_pair_destroy", + db_key_pair_destroy) + + def test_keypair_list_fail_policy(self): + rules = {self.policy_path + ':index': + common_policy.parse_rule('role:admin')} + policy.set_rules(rules) + req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs') + self.assertRaises(exception.Forbidden, + self.KeyPairController.index, + req) + + def test_keypair_list_pass_policy(self): + rules = {self.policy_path + ':index': + common_policy.parse_rule('')} + policy.set_rules(rules) + req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs') + res = self.KeyPairController.index(req) + self.assertIn('keypairs', res) + + def test_keypair_show_fail_policy(self): + rules = {self.policy_path + ':show': + common_policy.parse_rule('role:admin')} + policy.set_rules(rules) + req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs/FAKE') + self.assertRaises(exception.Forbidden, + self.KeyPairController.show, + req, 'FAKE') + + def test_keypair_show_pass_policy(self): + rules = {self.policy_path + ':show': + common_policy.parse_rule('')} + policy.set_rules(rules) + req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs/FAKE') + res = self.KeyPairController.show(req, 'FAKE') + self.assertIn('keypair', res) + + def test_keypair_create_fail_policy(self): + body = {'keypair': {'name': 'create_test'}} + rules = {self.policy_path + ':create': + common_policy.parse_rule('role:admin')} + policy.set_rules(rules) + req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs') + req.method = 'POST' + self.assertRaises(exception.Forbidden, + self.KeyPairController.create, + req, body=body) + + def test_keypair_create_pass_policy(self): + body = {'keypair': {'name': 'create_test'}} + rules = {self.policy_path + ':create': + common_policy.parse_rule('')} + policy.set_rules(rules) + req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs') + req.method = 'POST' + res = self.KeyPairController.create(req, body=body) + self.assertIn('keypair', res) + + def test_keypair_delete_fail_policy(self): + rules = {self.policy_path + ':delete': + common_policy.parse_rule('role:admin')} + policy.set_rules(rules) + req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs/FAKE') + req.method = 'DELETE' + self.assertRaises(exception.Forbidden, + self.KeyPairController.delete, + req, 'FAKE') + + def test_keypair_delete_pass_policy(self): + rules = {self.policy_path + ':delete': + common_policy.parse_rule('')} + policy.set_rules(rules) + req = fakes.HTTPRequest.blank(self.base_url + '/os-keypairs/FAKE') + req.method = 'DELETE' + res = self.KeyPairController.delete(req, 'FAKE') + + # NOTE: on v2.1, http status code is set as wsgi_code of API + # method instead of status_int in a response object. + if isinstance(self.KeyPairController, keypairs_v21.KeypairController): + status_int = self.KeyPairController.delete.wsgi_code + else: + status_int = res.status_int + self.assertEqual(202, status_int) + + +class KeypairsXMLSerializerTest(test.TestCase): + def setUp(self): + super(KeypairsXMLSerializerTest, self).setUp() + self.deserializer = wsgi.XMLDeserializer() + + def test_default_serializer(self): + exemplar = dict(keypair=dict( + public_key='fake_public_key', + private_key='fake_private_key', + fingerprint='fake_fingerprint', + user_id='fake_user_id', + name='fake_key_name')) + serializer = keypairs_v2.KeypairTemplate() + text = serializer.serialize(exemplar) + + tree = etree.fromstring(text) + + self.assertEqual('keypair', tree.tag) + for child in tree: + self.assertIn(child.tag, exemplar['keypair']) + self.assertEqual(child.text, exemplar['keypair'][child.tag]) + + def test_index_serializer(self): + exemplar = dict(keypairs=[ + dict(keypair=dict( + name='key1_name', + public_key='key1_key', + fingerprint='key1_fingerprint')), + dict(keypair=dict( + name='key2_name', + public_key='key2_key', + fingerprint='key2_fingerprint'))]) + serializer = keypairs_v2.KeypairsTemplate() + text = serializer.serialize(exemplar) + + tree = etree.fromstring(text) + + self.assertEqual('keypairs', tree.tag) + self.assertEqual(len(exemplar['keypairs']), len(tree)) + for idx, keypair in enumerate(tree): + self.assertEqual('keypair', keypair.tag) + kp_data = exemplar['keypairs'][idx]['keypair'] + for child in keypair: + self.assertIn(child.tag, kp_data) + self.assertEqual(child.text, kp_data[child.tag]) + + def test_deserializer(self): + exemplar = dict(keypair=dict( + name='key_name', + public_key='public_key')) + intext = ("<?xml version='1.0' encoding='UTF-8'?>\n" + '<keypair><name>key_name</name>' + '<public_key>public_key</public_key></keypair>') + + result = self.deserializer.deserialize(intext)['body'] + self.assertEqual(result, exemplar) + + +class KeypairsTestV2(KeypairsTestV21): + + def _setup_app(self): + self.app = fakes.wsgi_app(init_only=('os-keypairs',)) + self.app_server = fakes.wsgi_app(init_only=('servers',)) + + +class KeypairPolicyTestV2(KeypairPolicyTestV21): + KeyPairController = keypairs_v2.KeypairController() + policy_path = 'compute_extension:keypairs' diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_migrate_server.py b/nova/tests/unit/api/openstack/compute/contrib/test_migrate_server.py new file mode 100644 index 0000000000..069b688837 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_migrate_server.py @@ -0,0 +1,231 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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. + +from nova.api.openstack.compute.plugins.v3 import migrate_server +from nova import exception +from nova.openstack.common import uuidutils +from nova.tests.unit.api.openstack.compute.plugins.v3 import \ + admin_only_action_common +from nova.tests.unit.api.openstack import fakes + + +class MigrateServerTests(admin_only_action_common.CommonTests): + def setUp(self): + super(MigrateServerTests, self).setUp() + self.controller = migrate_server.MigrateServerController() + self.compute_api = self.controller.compute_api + + def _fake_controller(*args, **kwargs): + return self.controller + + self.stubs.Set(migrate_server, 'MigrateServerController', + _fake_controller) + self.app = fakes.wsgi_app_v21(init_only=('servers', + 'os-migrate-server'), + fake_auth_context=self.context) + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_migrate(self): + method_translations = {'migrate': 'resize', + 'os-migrateLive': 'live_migrate'} + body_map = {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + args_map = {'os-migrateLive': ((False, False, 'hostname'), {})} + self._test_actions(['migrate', 'os-migrateLive'], body_map=body_map, + method_translations=method_translations, + args_map=args_map) + + def test_migrate_none_hostname(self): + method_translations = {'migrate': 'resize', + 'os-migrateLive': 'live_migrate'} + body_map = {'os-migrateLive': {'host': None, + 'block_migration': False, + 'disk_over_commit': False}} + args_map = {'os-migrateLive': ((False, False, None), {})} + self._test_actions(['migrate', 'os-migrateLive'], body_map=body_map, + method_translations=method_translations, + args_map=args_map) + + def test_migrate_with_non_existed_instance(self): + body_map = {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + self._test_actions_with_non_existed_instance( + ['migrate', 'os-migrateLive'], body_map=body_map) + + def test_migrate_raise_conflict_on_invalid_state(self): + method_translations = {'migrate': 'resize', + 'os-migrateLive': 'live_migrate'} + body_map = {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + args_map = {'os-migrateLive': ((False, False, 'hostname'), {})} + self._test_actions_raise_conflict_on_invalid_state( + ['migrate', 'os-migrateLive'], body_map=body_map, + args_map=args_map, method_translations=method_translations) + + def test_actions_with_locked_instance(self): + method_translations = {'migrate': 'resize', + 'os-migrateLive': 'live_migrate'} + body_map = {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + args_map = {'os-migrateLive': ((False, False, 'hostname'), {})} + self._test_actions_with_locked_instance( + ['migrate', 'os-migrateLive'], body_map=body_map, + args_map=args_map, method_translations=method_translations) + + def _test_migrate_exception(self, exc_info, expected_result): + self.mox.StubOutWithMock(self.compute_api, 'resize') + instance = self._stub_instance_get() + self.compute_api.resize(self.context, instance).AndRaise(exc_info) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance['uuid'], + {'migrate': None}) + self.assertEqual(expected_result, res.status_int) + + def test_migrate_too_many_instances(self): + exc_info = exception.TooManyInstances(overs='', req='', used=0, + allowed=0, resource='') + self._test_migrate_exception(exc_info, 403) + + def _test_migrate_live_succeeded(self, param): + self.mox.StubOutWithMock(self.compute_api, 'live_migrate') + instance = self._stub_instance_get() + self.compute_api.live_migrate(self.context, instance, False, + False, 'hostname') + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {'os-migrateLive': param}) + self.assertEqual(202, res.status_int) + + def test_migrate_live_enabled(self): + param = {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False} + self._test_migrate_live_succeeded(param) + + def test_migrate_live_enabled_with_string_param(self): + param = {'host': 'hostname', + 'block_migration': "False", + 'disk_over_commit': "False"} + self._test_migrate_live_succeeded(param) + + def test_migrate_live_without_host(self): + res = self._make_request('/servers/FAKE/action', + {'os-migrateLive': + {'block_migration': False, + 'disk_over_commit': False}}) + self.assertEqual(400, res.status_int) + + def test_migrate_live_without_block_migration(self): + res = self._make_request('/servers/FAKE/action', + {'os-migrateLive': + {'host': 'hostname', + 'disk_over_commit': False}}) + self.assertEqual(400, res.status_int) + + def test_migrate_live_without_disk_over_commit(self): + res = self._make_request('/servers/FAKE/action', + {'os-migrateLive': + {'host': 'hostname', + 'block_migration': False}}) + self.assertEqual(400, res.status_int) + + def test_migrate_live_with_invalid_block_migration(self): + res = self._make_request('/servers/FAKE/action', + {'os-migrateLive': + {'host': 'hostname', + 'block_migration': "foo", + 'disk_over_commit': False}}) + self.assertEqual(400, res.status_int) + + def test_migrate_live_with_invalid_disk_over_commit(self): + res = self._make_request('/servers/FAKE/action', + {'os-migrateLive': + {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': "foo"}}) + self.assertEqual(400, res.status_int) + + def _test_migrate_live_failed_with_exception(self, fake_exc, + uuid=None): + self.mox.StubOutWithMock(self.compute_api, 'live_migrate') + + instance = self._stub_instance_get(uuid=uuid) + self.compute_api.live_migrate(self.context, instance, False, + False, 'hostname').AndRaise(fake_exc) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {'os-migrateLive': + {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}}) + self.assertEqual(400, res.status_int) + self.assertIn(unicode(fake_exc), res.body) + + def test_migrate_live_compute_service_unavailable(self): + self._test_migrate_live_failed_with_exception( + exception.ComputeServiceUnavailable(host='host')) + + def test_migrate_live_invalid_hypervisor_type(self): + self._test_migrate_live_failed_with_exception( + exception.InvalidHypervisorType()) + + def test_migrate_live_invalid_cpu_info(self): + self._test_migrate_live_failed_with_exception( + exception.InvalidCPUInfo(reason="")) + + def test_migrate_live_unable_to_migrate_to_self(self): + uuid = uuidutils.generate_uuid() + self._test_migrate_live_failed_with_exception( + exception.UnableToMigrateToSelf(instance_id=uuid, + host='host'), + uuid=uuid) + + def test_migrate_live_destination_hypervisor_too_old(self): + self._test_migrate_live_failed_with_exception( + exception.DestinationHypervisorTooOld()) + + def test_migrate_live_no_valid_host(self): + self._test_migrate_live_failed_with_exception( + exception.NoValidHost(reason='')) + + def test_migrate_live_invalid_local_storage(self): + self._test_migrate_live_failed_with_exception( + exception.InvalidLocalStorage(path='', reason='')) + + def test_migrate_live_invalid_shared_storage(self): + self._test_migrate_live_failed_with_exception( + exception.InvalidSharedStorage(path='', reason='')) + + def test_migrate_live_hypervisor_unavailable(self): + self._test_migrate_live_failed_with_exception( + exception.HypervisorUnavailable(host="")) + + def test_migrate_live_instance_not_running(self): + self._test_migrate_live_failed_with_exception( + exception.InstanceNotRunning(instance_id="")) + + def test_migrate_live_pre_check_error(self): + self._test_migrate_live_failed_with_exception( + exception.MigrationPreCheckError(reason='')) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_migrations.py b/nova/tests/unit/api/openstack/compute/contrib/test_migrations.py new file mode 100644 index 0000000000..ac18576389 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_migrations.py @@ -0,0 +1,139 @@ +# 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 datetime + +from lxml import etree + +from nova.api.openstack.compute.contrib import migrations +from nova import context +from nova import exception +from nova import objects +from nova.objects import base +from nova.openstack.common.fixture import moxstubout +from nova import test + +fake_migrations = [ + { + 'id': 1234, + 'source_node': 'node1', + 'dest_node': 'node2', + 'source_compute': 'compute1', + 'dest_compute': 'compute2', + 'dest_host': '1.2.3.4', + 'status': 'Done', + 'instance_uuid': 'instance_id_123', + 'old_instance_type_id': 1, + 'new_instance_type_id': 2, + 'created_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + 'deleted_at': None, + 'deleted': False + }, + { + 'id': 5678, + 'source_node': 'node10', + 'dest_node': 'node20', + 'source_compute': 'compute10', + 'dest_compute': 'compute20', + 'dest_host': '5.6.7.8', + 'status': 'Done', + 'instance_uuid': 'instance_id_456', + 'old_instance_type_id': 5, + 'new_instance_type_id': 6, + 'created_at': datetime.datetime(2013, 10, 22, 13, 42, 2), + 'updated_at': datetime.datetime(2013, 10, 22, 13, 42, 2), + 'deleted_at': None, + 'deleted': False + } +] + +migrations_obj = base.obj_make_list( + 'fake-context', + objects.MigrationList(), + objects.Migration, + fake_migrations) + + +class FakeRequest(object): + environ = {"nova.context": context.get_admin_context()} + GET = {} + + +class MigrationsTestCase(test.NoDBTestCase): + def setUp(self): + """Run before each test.""" + super(MigrationsTestCase, self).setUp() + self.controller = migrations.MigrationsController() + self.context = context.get_admin_context() + self.req = FakeRequest() + self.req.environ['nova.context'] = self.context + mox_fixture = self.useFixture(moxstubout.MoxStubout()) + self.mox = mox_fixture.mox + + def test_index(self): + migrations_in_progress = { + 'migrations': migrations.output(migrations_obj)} + + for mig in migrations_in_progress['migrations']: + self.assertIn('id', mig) + self.assertNotIn('deleted', mig) + self.assertNotIn('deleted_at', mig) + + filters = {'host': 'host1', 'status': 'migrating', + 'cell_name': 'ChildCell'} + self.req.GET = filters + self.mox.StubOutWithMock(self.controller.compute_api, + "get_migrations") + + self.controller.compute_api.get_migrations( + self.context, filters).AndReturn(migrations_obj) + self.mox.ReplayAll() + + response = self.controller.index(self.req) + self.assertEqual(migrations_in_progress, response) + + def test_index_needs_authorization(self): + user_context = context.RequestContext(user_id=None, + project_id=None, + is_admin=False, + read_deleted="no", + overwrite=False) + self.req.environ['nova.context'] = user_context + + self.assertRaises(exception.PolicyNotAuthorized, self.controller.index, + self.req) + + +class MigrationsTemplateTest(test.NoDBTestCase): + def setUp(self): + super(MigrationsTemplateTest, self).setUp() + self.serializer = migrations.MigrationsTemplate() + + def test_index_serialization(self): + migrations_out = migrations.output(migrations_obj) + res_xml = self.serializer.serialize( + {'migrations': migrations_out}) + + tree = etree.XML(res_xml) + children = tree.findall('migration') + self.assertEqual(tree.tag, 'migrations') + self.assertEqual(2, len(children)) + + for idx, child in enumerate(children): + self.assertEqual(child.tag, 'migration') + migration = migrations_out[idx] + for attr in migration.keys(): + self.assertEqual(str(migration[attr]), + child.get(attr)) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_multinic.py b/nova/tests/unit/api/openstack/compute/contrib/test_multinic.py new file mode 100644 index 0000000000..dcf1dd299f --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_multinic.py @@ -0,0 +1,204 @@ +# Copyright 2011 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 mock +from oslo.serialization import jsonutils +import webob + +from nova import compute +from nova import exception +from nova import objects +from nova import test +from nova.tests.unit.api.openstack import fakes + + +UUID = '70f6db34-de8d-4fbd-aafb-4065bdfa6114' +last_add_fixed_ip = (None, None) +last_remove_fixed_ip = (None, None) + + +def compute_api_add_fixed_ip(self, context, instance, network_id): + global last_add_fixed_ip + + last_add_fixed_ip = (instance['uuid'], network_id) + + +def compute_api_remove_fixed_ip(self, context, instance, address): + global last_remove_fixed_ip + + last_remove_fixed_ip = (instance['uuid'], address) + + +def compute_api_get(self, context, instance_id, want_objects=False, + expected_attrs=None): + instance = objects.Instance() + instance.uuid = instance_id + instance.id = 1 + instance.vm_state = 'fake' + instance.task_state = 'fake' + instance.obj_reset_changes() + return instance + + +class FixedIpTestV21(test.NoDBTestCase): + def setUp(self): + super(FixedIpTestV21, self).setUp() + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + self.stubs.Set(compute.api.API, "add_fixed_ip", + compute_api_add_fixed_ip) + self.stubs.Set(compute.api.API, "remove_fixed_ip", + compute_api_remove_fixed_ip) + self.stubs.Set(compute.api.API, 'get', compute_api_get) + self.app = self._get_app() + + def _get_app(self): + return fakes.wsgi_app_v21(init_only=('servers', 'os-multinic')) + + def _get_url(self): + return '/v2/fake' + + def test_add_fixed_ip(self): + global last_add_fixed_ip + last_add_fixed_ip = (None, None) + + body = dict(addFixedIp=dict(networkId='test_net')) + req = webob.Request.blank( + self._get_url() + '/servers/%s/action' % UUID) + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 202) + self.assertEqual(last_add_fixed_ip, (UUID, 'test_net')) + + def _test_add_fixed_ip_bad_request(self, body): + req = webob.Request.blank( + self._get_url() + '/servers/%s/action' % UUID) + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + resp = req.get_response(self.app) + self.assertEqual(400, resp.status_int) + + def test_add_fixed_ip_empty_network_id(self): + body = {'addFixedIp': {'network_id': ''}} + self._test_add_fixed_ip_bad_request(body) + + def test_add_fixed_ip_network_id_bigger_than_36(self): + body = {'addFixedIp': {'network_id': 'a' * 37}} + self._test_add_fixed_ip_bad_request(body) + + def test_add_fixed_ip_no_network(self): + global last_add_fixed_ip + last_add_fixed_ip = (None, None) + + body = dict(addFixedIp=dict()) + req = webob.Request.blank( + self._get_url() + '/servers/%s/action' % UUID) + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 400) + self.assertEqual(last_add_fixed_ip, (None, None)) + + @mock.patch.object(compute.api.API, 'add_fixed_ip') + def test_add_fixed_ip_no_more_ips_available(self, mock_add_fixed_ip): + mock_add_fixed_ip.side_effect = exception.NoMoreFixedIps(net='netid') + + body = dict(addFixedIp=dict(networkId='test_net')) + req = webob.Request.blank( + self._get_url() + '/servers/%s/action' % UUID) + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 400) + + def test_remove_fixed_ip(self): + global last_remove_fixed_ip + last_remove_fixed_ip = (None, None) + + body = dict(removeFixedIp=dict(address='10.10.10.1')) + req = webob.Request.blank( + self._get_url() + '/servers/%s/action' % UUID) + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 202) + self.assertEqual(last_remove_fixed_ip, (UUID, '10.10.10.1')) + + def test_remove_fixed_ip_no_address(self): + global last_remove_fixed_ip + last_remove_fixed_ip = (None, None) + + body = dict(removeFixedIp=dict()) + req = webob.Request.blank( + self._get_url() + '/servers/%s/action' % UUID) + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 400) + self.assertEqual(last_remove_fixed_ip, (None, None)) + + def test_remove_fixed_ip_invalid_address(self): + body = {'remove_fixed_ip': {'address': ''}} + req = webob.Request.blank( + self._get_url() + '/servers/%s/action' % UUID) + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + resp = req.get_response(self.app) + self.assertEqual(400, resp.status_int) + + @mock.patch.object(compute.api.API, 'remove_fixed_ip', + side_effect=exception.FixedIpNotFoundForSpecificInstance( + instance_uuid=UUID, ip='10.10.10.1')) + def test_remove_fixed_ip_not_found(self, _remove_fixed_ip): + + body = {'remove_fixed_ip': {'address': '10.10.10.1'}} + req = webob.Request.blank( + self._get_url() + '/servers/%s/action' % UUID) + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(self.app) + self.assertEqual(400, resp.status_int) + + +class FixedIpTestV2(FixedIpTestV21): + def setUp(self): + super(FixedIpTestV2, self).setUp() + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Multinic']) + + def _get_app(self): + return fakes.wsgi_app(init_only=('servers',)) + + def test_remove_fixed_ip_invalid_address(self): + # NOTE(cyeoh): This test is disabled for the V2 API because it is + # has poorer input validation. + pass diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_networks.py b/nova/tests/unit/api/openstack/compute/contrib/test_networks.py new file mode 100644 index 0000000000..5636a06d0d --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_networks.py @@ -0,0 +1,610 @@ +# Copyright 2011 Grid Dynamics +# Copyright 2011 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 copy +import datetime +import math +import uuid + +import iso8601 +import mock +import netaddr +from oslo.config import cfg +import webob + +from nova.api.openstack.compute.contrib import networks_associate +from nova.api.openstack.compute.contrib import os_networks as networks +from nova.api.openstack.compute.plugins.v3 import networks as networks_v21 +from nova.api.openstack.compute.plugins.v3 import networks_associate as \ + networks_associate_v21 +from nova.api.openstack import extensions +import nova.context +from nova import exception +from nova.network import manager +from nova import objects +from nova import test +from nova.tests.unit.api.openstack import fakes +import nova.utils + +CONF = cfg.CONF + +UTC = iso8601.iso8601.Utc() +FAKE_NETWORKS = [ + { + 'bridge': 'br100', 'vpn_public_port': 1000, + 'dhcp_start': '10.0.0.3', 'bridge_interface': 'eth0', + 'updated_at': datetime.datetime(2011, 8, 16, 9, 26, 13, 48257, + tzinfo=UTC), + 'id': 1, 'uuid': '20c8acc0-f747-4d71-a389-46d078ebf047', + 'cidr_v6': None, 'deleted_at': None, + 'gateway': '10.0.0.1', 'label': 'mynet_0', + 'project_id': '1234', 'rxtx_base': None, + 'vpn_private_address': '10.0.0.2', 'deleted': False, + 'vlan': 100, 'broadcast': '10.0.0.7', + 'netmask': '255.255.255.248', 'injected': False, + 'cidr': '10.0.0.0/29', + 'vpn_public_address': '127.0.0.1', 'multi_host': False, + 'dns1': None, 'dns2': None, 'host': 'nsokolov-desktop', + 'gateway_v6': None, 'netmask_v6': None, 'priority': None, + 'created_at': datetime.datetime(2011, 8, 15, 6, 19, 19, 387525, + tzinfo=UTC), + 'mtu': None, 'dhcp_server': '10.0.0.1', 'enable_dhcp': True, + 'share_address': False, + }, + { + 'bridge': 'br101', 'vpn_public_port': 1001, + 'dhcp_start': '10.0.0.11', 'bridge_interface': 'eth0', + 'updated_at': None, 'id': 2, 'cidr_v6': None, + 'uuid': '20c8acc0-f747-4d71-a389-46d078ebf000', + 'deleted_at': None, 'gateway': '10.0.0.9', + 'label': 'mynet_1', 'project_id': None, + 'vpn_private_address': '10.0.0.10', 'deleted': False, + 'vlan': 101, 'broadcast': '10.0.0.15', 'rxtx_base': None, + 'netmask': '255.255.255.248', 'injected': False, + 'cidr': '10.0.0.10/29', 'vpn_public_address': None, + 'multi_host': False, 'dns1': None, 'dns2': None, 'host': None, + 'gateway_v6': None, 'netmask_v6': None, 'priority': None, + 'created_at': datetime.datetime(2011, 8, 15, 6, 19, 19, 885495, + tzinfo=UTC), + 'mtu': None, 'dhcp_server': '10.0.0.9', 'enable_dhcp': True, + 'share_address': False, + }, +] + + +FAKE_USER_NETWORKS = [ + { + 'id': 1, 'cidr': '10.0.0.0/29', 'netmask': '255.255.255.248', + 'gateway': '10.0.0.1', 'broadcast': '10.0.0.7', 'dns1': None, + 'dns2': None, 'cidr_v6': None, 'gateway_v6': None, 'label': 'mynet_0', + 'netmask_v6': None, 'uuid': '20c8acc0-f747-4d71-a389-46d078ebf047', + }, + { + 'id': 2, 'cidr': '10.0.0.10/29', 'netmask': '255.255.255.248', + 'gateway': '10.0.0.9', 'broadcast': '10.0.0.15', 'dns1': None, + 'dns2': None, 'cidr_v6': None, 'gateway_v6': None, 'label': 'mynet_1', + 'netmask_v6': None, 'uuid': '20c8acc0-f747-4d71-a389-46d078ebf000', + }, +] + +NEW_NETWORK = { + "network": { + "bridge_interface": "eth0", + "cidr": "10.20.105.0/24", + "label": "new net 111", + "vlan_start": 111, + "injected": False, + "multi_host": False, + 'mtu': None, + 'dhcp_server': '10.0.0.1', + 'enable_dhcp': True, + 'share_address': False, + } +} + + +class FakeNetworkAPI(object): + + _sentinel = object() + _vlan_is_disabled = False + + def __init__(self): + self.networks = copy.deepcopy(FAKE_NETWORKS) + + def disable_vlan(self): + self._vlan_is_disabled = True + + def delete(self, context, network_id): + if network_id == 'always_delete': + return True + if network_id == -1: + raise exception.NetworkInUse(network_id=network_id) + for i, network in enumerate(self.networks): + if network['id'] == network_id: + del self.networks[0] + return True + raise exception.NetworkNotFoundForUUID(uuid=network_id) + + def disassociate(self, context, network_uuid): + for network in self.networks: + if network.get('uuid') == network_uuid: + network['project_id'] = None + return True + raise exception.NetworkNotFound(network_id=network_uuid) + + def associate(self, context, network_uuid, host=_sentinel, + project=_sentinel): + for network in self.networks: + if network.get('uuid') == network_uuid: + if host is not FakeNetworkAPI._sentinel: + network['host'] = host + if project is not FakeNetworkAPI._sentinel: + network['project_id'] = project + return True + raise exception.NetworkNotFound(network_id=network_uuid) + + def add_network_to_project(self, context, + project_id, network_uuid=None): + if self._vlan_is_disabled: + raise NotImplementedError() + if network_uuid: + for network in self.networks: + if network.get('project_id', None) is None: + network['project_id'] = project_id + return + return + for network in self.networks: + if network.get('uuid') == network_uuid: + network['project_id'] = project_id + return + + def get_all(self, context): + return self._fake_db_network_get_all(context, project_only=True) + + def _fake_db_network_get_all(self, context, project_only="allow_none"): + project_id = context.project_id + nets = self.networks + if nova.context.is_user_context(context) and project_only: + if project_only == 'allow_none': + nets = [n for n in self.networks + if (n['project_id'] == project_id or + n['project_id'] is None)] + else: + nets = [n for n in self.networks + if n['project_id'] == project_id] + objs = [objects.Network._from_db_object(context, + objects.Network(), + net) + for net in nets] + return objects.NetworkList(objects=objs) + + def get(self, context, network_id): + for network in self.networks: + if network.get('uuid') == network_id: + return objects.Network._from_db_object(context, + objects.Network(), + network) + raise exception.NetworkNotFound(network_id=network_id) + + def create(self, context, **kwargs): + subnet_bits = int(math.ceil(math.log(kwargs.get( + 'network_size', CONF.network_size), 2))) + fixed_net_v4 = netaddr.IPNetwork(kwargs['cidr']) + prefixlen_v4 = 32 - subnet_bits + subnets_v4 = list(fixed_net_v4.subnet( + prefixlen_v4, + count=kwargs.get('num_networks', CONF.num_networks))) + new_networks = [] + new_id = max((net['id'] for net in self.networks)) + for index, subnet_v4 in enumerate(subnets_v4): + new_id += 1 + net = {'id': new_id, 'uuid': str(uuid.uuid4())} + + net['cidr'] = str(subnet_v4) + net['netmask'] = str(subnet_v4.netmask) + net['gateway'] = kwargs.get('gateway') or str(subnet_v4[1]) + net['broadcast'] = str(subnet_v4.broadcast) + net['dhcp_start'] = str(subnet_v4[2]) + + for key in FAKE_NETWORKS[0].iterkeys(): + net.setdefault(key, kwargs.get(key)) + new_networks.append(net) + self.networks += new_networks + return new_networks + + +# NOTE(vish): tests that network create Exceptions actually return +# the proper error responses +class NetworkCreateExceptionsTestV21(test.TestCase): + url_prefix = '/v2/1234' + + class PassthroughAPI(object): + def __init__(self): + self.network_manager = manager.FlatDHCPManager() + + def create(self, *args, **kwargs): + if kwargs['label'] == 'fail_NetworkNotCreated': + raise exception.NetworkNotCreated(req='fake_fail') + return self.network_manager.create_networks(*args, **kwargs) + + def setUp(self): + super(NetworkCreateExceptionsTestV21, self).setUp() + self._setup() + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + + def _setup(self): + self.controller = networks_v21.NetworkController(self.PassthroughAPI()) + + def test_network_create_bad_vlan(self): + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks') + net = copy.deepcopy(NEW_NETWORK) + net['network']['vlan_start'] = 'foo' + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, net) + + def test_network_create_no_cidr(self): + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks') + net = copy.deepcopy(NEW_NETWORK) + net['network']['cidr'] = '' + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, net) + + def test_network_create_invalid_fixed_cidr(self): + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks') + net = copy.deepcopy(NEW_NETWORK) + net['network']['fixed_cidr'] = 'foo' + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, net) + + def test_network_create_invalid_start(self): + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks') + net = copy.deepcopy(NEW_NETWORK) + net['network']['allowed_start'] = 'foo' + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, net) + + def test_network_create_handle_network_not_created(self): + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks') + net = copy.deepcopy(NEW_NETWORK) + net['network']['label'] = 'fail_NetworkNotCreated' + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, net) + + def test_network_create_cidr_conflict(self): + + @staticmethod + def get_all(context): + ret = objects.NetworkList(context=context, objects=[]) + net = objects.Network(cidr='10.0.0.0/23') + ret.objects.append(net) + return ret + + self.stubs.Set(objects.NetworkList, 'get_all', get_all) + + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks') + net = copy.deepcopy(NEW_NETWORK) + net['network']['cidr'] = '10.0.0.0/24' + self.assertRaises(webob.exc.HTTPConflict, + self.controller.create, req, net) + + +class NetworkCreateExceptionsTestV2(NetworkCreateExceptionsTestV21): + + def _setup(self): + ext_mgr = extensions.ExtensionManager() + ext_mgr.extensions = {'os-extended-networks': 'fake'} + + self.controller = networks.NetworkController( + self.PassthroughAPI(), ext_mgr) + + +class NetworksTestV21(test.NoDBTestCase): + url_prefix = '/v2/1234' + + def setUp(self): + super(NetworksTestV21, self).setUp() + self.fake_network_api = FakeNetworkAPI() + self._setup() + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + + def _setup(self): + self.controller = networks_v21.NetworkController( + self.fake_network_api) + + def _check_status(self, res, method, code): + self.assertEqual(method.wsgi_code, 202) + + @staticmethod + def network_uuid_to_id(network): + network['id'] = network['uuid'] + del network['uuid'] + + def test_network_list_all_as_user(self): + self.maxDiff = None + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks') + res_dict = self.controller.index(req) + self.assertEqual(res_dict, {'networks': []}) + + project_id = req.environ["nova.context"].project_id + cxt = req.environ["nova.context"] + uuid = FAKE_NETWORKS[0]['uuid'] + self.fake_network_api.associate(context=cxt, + network_uuid=uuid, + project=project_id) + res_dict = self.controller.index(req) + expected = [copy.deepcopy(FAKE_USER_NETWORKS[0])] + for network in expected: + self.network_uuid_to_id(network) + self.assertEqual({'networks': expected}, res_dict) + + def test_network_list_all_as_admin(self): + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks') + req.environ["nova.context"].is_admin = True + res_dict = self.controller.index(req) + expected = copy.deepcopy(FAKE_NETWORKS) + for network in expected: + self.network_uuid_to_id(network) + self.assertEqual({'networks': expected}, res_dict) + + def test_network_disassociate(self): + uuid = FAKE_NETWORKS[0]['uuid'] + req = fakes.HTTPRequest.blank(self.url_prefix + + '/os-networks/%s/action' % uuid) + res = self.controller._disassociate_host_and_project( + req, uuid, {'disassociate': None}) + self._check_status(res, self.controller._disassociate_host_and_project, + 202) + self.assertIsNone(self.fake_network_api.networks[0]['project_id']) + self.assertIsNone(self.fake_network_api.networks[0]['host']) + + def test_network_disassociate_not_found(self): + req = fakes.HTTPRequest.blank(self.url_prefix + + '/os-networks/100/action') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._disassociate_host_and_project, + req, 100, {'disassociate': None}) + + def test_network_get_as_user(self): + uuid = FAKE_USER_NETWORKS[0]['uuid'] + req = fakes.HTTPRequest.blank(self.url_prefix + + '/os-networks/%s' % uuid) + res_dict = self.controller.show(req, uuid) + expected = {'network': copy.deepcopy(FAKE_USER_NETWORKS[0])} + self.network_uuid_to_id(expected['network']) + self.assertEqual(expected, res_dict) + + def test_network_get_as_admin(self): + uuid = FAKE_NETWORKS[0]['uuid'] + req = fakes.HTTPRequest.blank(self.url_prefix + + '/os-networks/%s' % uuid) + req.environ["nova.context"].is_admin = True + res_dict = self.controller.show(req, uuid) + expected = {'network': copy.deepcopy(FAKE_NETWORKS[0])} + self.network_uuid_to_id(expected['network']) + self.assertEqual(expected, res_dict) + + def test_network_get_not_found(self): + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks/100') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, 100) + + def test_network_delete(self): + uuid = FAKE_NETWORKS[0]['uuid'] + req = fakes.HTTPRequest.blank(self.url_prefix + + '/os-networks/%s' % uuid) + res = self.controller.delete(req, 1) + self._check_status(res, self.controller._disassociate_host_and_project, + 202) + + def test_network_delete_not_found(self): + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks/100') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, req, 100) + + def test_network_delete_in_use(self): + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks/-1') + self.assertRaises(webob.exc.HTTPConflict, + self.controller.delete, req, -1) + + def test_network_add(self): + uuid = FAKE_NETWORKS[1]['uuid'] + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks/add') + res = self.controller.add(req, {'id': uuid}) + self._check_status(res, self.controller._disassociate_host_and_project, + 202) + req = fakes.HTTPRequest.blank(self.url_prefix + + '/os-networks/%s' % uuid) + req.environ["nova.context"].is_admin = True + res_dict = self.controller.show(req, uuid) + self.assertEqual(res_dict['network']['project_id'], 'fake') + + @mock.patch('nova.tests.unit.api.openstack.compute.contrib.test_networks.' + 'FakeNetworkAPI.add_network_to_project', + side_effect=exception.NoMoreNetworks) + def test_network_add_no_more_networks_fail(self, mock_add): + uuid = FAKE_NETWORKS[1]['uuid'] + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks/add') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.add, req, + {'id': uuid}) + + @mock.patch('nova.tests.unit.api.openstack.compute.contrib.test_networks.' + 'FakeNetworkAPI.add_network_to_project', + side_effect=exception.NetworkNotFoundForUUID(uuid='fake_uuid')) + def test_network_add_network_not_found_networks_fail(self, mock_add): + uuid = FAKE_NETWORKS[1]['uuid'] + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks/add') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.add, req, + {'id': uuid}) + + def test_network_create(self): + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks') + res_dict = self.controller.create(req, NEW_NETWORK) + self.assertIn('network', res_dict) + uuid = res_dict['network']['id'] + req = fakes.HTTPRequest.blank(self.url_prefix + + '/os-networks/%s' % uuid) + res_dict = self.controller.show(req, uuid) + self.assertTrue(res_dict['network']['label']. + startswith(NEW_NETWORK['network']['label'])) + + def test_network_create_large(self): + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks') + large_network = copy.deepcopy(NEW_NETWORK) + large_network['network']['cidr'] = '128.0.0.0/4' + res_dict = self.controller.create(req, large_network) + self.assertEqual(res_dict['network']['cidr'], + large_network['network']['cidr']) + + def test_network_create_bad_cidr(self): + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks') + net = copy.deepcopy(NEW_NETWORK) + net['network']['cidr'] = '128.0.0.0/900' + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, net) + + def test_network_neutron_disassociate_not_implemented(self): + uuid = FAKE_NETWORKS[1]['uuid'] + self.flags(network_api_class='nova.network.neutronv2.api.API') + controller = networks.NetworkController() + req = fakes.HTTPRequest.blank(self.url_prefix + + '/os-networks/%s/action' % uuid) + self.assertRaises(webob.exc.HTTPNotImplemented, + controller._disassociate_host_and_project, + req, uuid, {'disassociate': None}) + + +class NetworksTestV2(NetworksTestV21): + + def _setup(self): + ext_mgr = extensions.ExtensionManager() + ext_mgr.extensions = {'os-extended-networks': 'fake'} + self.controller = networks.NetworkController(self.fake_network_api, + ext_mgr) + + def _check_status(self, res, method, code): + self.assertEqual(res.status_int, 202) + + def test_network_create_not_extended(self): + self.stubs.Set(self.controller, 'extended', False) + # NOTE(vish): Verify that new params are not passed through if + # extension is not enabled. + + def no_mtu(*args, **kwargs): + if 'mtu' in kwargs: + raise test.TestingException("mtu should not pass through") + return [{}] + + self.stubs.Set(self.controller.network_api, 'create', no_mtu) + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-networks') + net = copy.deepcopy(NEW_NETWORK) + net['network']['mtu'] = 9000 + self.controller.create(req, net) + + +class NetworksAssociateTestV21(test.NoDBTestCase): + + def setUp(self): + super(NetworksAssociateTestV21, self).setUp() + self.fake_network_api = FakeNetworkAPI() + self._setup() + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + + def _setup(self): + self.controller = networks.NetworkController(self.fake_network_api) + self.associate_controller = networks_associate_v21\ + .NetworkAssociateActionController(self.fake_network_api) + + def _check_status(self, res, method, code): + self.assertEqual(method.wsgi_code, code) + + def test_network_disassociate_host_only(self): + uuid = FAKE_NETWORKS[0]['uuid'] + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid) + res = self.associate_controller._disassociate_host_only( + req, uuid, {'disassociate_host': None}) + self._check_status(res, + self.associate_controller._disassociate_host_only, + 202) + self.assertIsNotNone(self.fake_network_api.networks[0]['project_id']) + self.assertIsNone(self.fake_network_api.networks[0]['host']) + + def test_network_disassociate_project_only(self): + uuid = FAKE_NETWORKS[0]['uuid'] + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid) + res = self.associate_controller._disassociate_project_only( + req, uuid, {'disassociate_project': None}) + self._check_status( + res, self.associate_controller._disassociate_project_only, 202) + self.assertIsNone(self.fake_network_api.networks[0]['project_id']) + self.assertIsNotNone(self.fake_network_api.networks[0]['host']) + + def test_network_associate_with_host(self): + uuid = FAKE_NETWORKS[1]['uuid'] + req = fakes.HTTPRequest.blank('/v2/1234//os-networks/%s/action' % uuid) + res = self.associate_controller._associate_host( + req, uuid, {'associate_host': "TestHost"}) + self._check_status(res, self.associate_controller._associate_host, 202) + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s' % uuid) + req.environ["nova.context"].is_admin = True + res_dict = self.controller.show(req, uuid) + self.assertEqual(res_dict['network']['host'], 'TestHost') + + def test_network_neutron_associate_not_implemented(self): + uuid = FAKE_NETWORKS[1]['uuid'] + self.flags(network_api_class='nova.network.neutronv2.api.API') + assoc_ctrl = networks_associate.NetworkAssociateActionController() + + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid) + self.assertRaises(webob.exc.HTTPNotImplemented, + assoc_ctrl._associate_host, + req, uuid, {'associate_host': "TestHost"}) + + def test_network_neutron_disassociate_project_not_implemented(self): + uuid = FAKE_NETWORKS[1]['uuid'] + self.flags(network_api_class='nova.network.neutronv2.api.API') + assoc_ctrl = networks_associate.NetworkAssociateActionController() + + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid) + self.assertRaises(webob.exc.HTTPNotImplemented, + assoc_ctrl._disassociate_project_only, + req, uuid, {'disassociate_project': None}) + + def test_network_neutron_disassociate_host_not_implemented(self): + uuid = FAKE_NETWORKS[1]['uuid'] + self.flags(network_api_class='nova.network.neutronv2.api.API') + assoc_ctrl = networks_associate.NetworkAssociateActionController() + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid) + self.assertRaises(webob.exc.HTTPNotImplemented, + assoc_ctrl._disassociate_host_only, + req, uuid, {'disassociate_host': None}) + + +class NetworksAssociateTestV2(NetworksAssociateTestV21): + + def _setup(self): + ext_mgr = extensions.ExtensionManager() + ext_mgr.extensions = {'os-extended-networks': 'fake'} + self.controller = networks.NetworkController( + self.fake_network_api, + ext_mgr) + self.associate_controller = networks_associate\ + .NetworkAssociateActionController(self.fake_network_api) + + def _check_status(self, res, method, code): + self.assertEqual(res.status_int, 202) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_neutron_security_groups.py b/nova/tests/unit/api/openstack/compute/contrib/test_neutron_security_groups.py new file mode 100644 index 0000000000..704de21005 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_neutron_security_groups.py @@ -0,0 +1,918 @@ +# Copyright 2013 Nicira, Inc. +# 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 uuid + +from lxml import etree +import mock +from neutronclient.common import exceptions as n_exc +from neutronclient.neutron import v2_0 as neutronv20 +from oslo.config import cfg +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import security_groups +from nova.api.openstack import xmlutil +from nova import compute +from nova import context +import nova.db +from nova import exception +from nova.network import model +from nova.network import neutronv2 +from nova.network.neutronv2 import api as neutron_api +from nova.network.security_group import neutron_driver +from nova.objects import instance as instance_obj +from nova import test +from nova.tests.unit.api.openstack.compute.contrib import test_security_groups +from nova.tests.unit.api.openstack import fakes + + +class TestNeutronSecurityGroupsTestCase(test.TestCase): + def setUp(self): + super(TestNeutronSecurityGroupsTestCase, self).setUp() + cfg.CONF.set_override('security_group_api', 'neutron') + self.original_client = neutronv2.get_client + neutronv2.get_client = get_client + + def tearDown(self): + neutronv2.get_client = self.original_client + get_client()._reset() + super(TestNeutronSecurityGroupsTestCase, self).tearDown() + + +class TestNeutronSecurityGroupsV21( + test_security_groups.TestSecurityGroupsV21, + TestNeutronSecurityGroupsTestCase): + + def _create_sg_template(self, **kwargs): + sg = test_security_groups.security_group_template(**kwargs) + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + return self.controller.create(req, {'security_group': sg}) + + def _create_network(self): + body = {'network': {'name': 'net1'}} + neutron = get_client() + net = neutron.create_network(body) + body = {'subnet': {'network_id': net['network']['id'], + 'cidr': '10.0.0.0/24'}} + neutron.create_subnet(body) + return net + + def _create_port(self, **kwargs): + body = {'port': {'binding:vnic_type': model.VNIC_TYPE_NORMAL}} + fields = ['security_groups', 'device_id', 'network_id', + 'port_security_enabled'] + for field in fields: + if field in kwargs: + body['port'][field] = kwargs[field] + neutron = get_client() + return neutron.create_port(body) + + def _create_security_group(self, **kwargs): + body = {'security_group': {}} + fields = ['name', 'description'] + for field in fields: + if field in kwargs: + body['security_group'][field] = kwargs[field] + neutron = get_client() + return neutron.create_security_group(body) + + def test_create_security_group_with_no_description(self): + # Neutron's security group description field is optional. + pass + + def test_create_security_group_with_empty_description(self): + # Neutron's security group description field is optional. + pass + + def test_create_security_group_with_blank_name(self): + # Neutron's security group name field is optional. + pass + + def test_create_security_group_with_whitespace_name(self): + # Neutron allows security group name to be whitespace. + pass + + def test_create_security_group_with_blank_description(self): + # Neutron's security group description field is optional. + pass + + def test_create_security_group_with_whitespace_description(self): + # Neutron allows description to be whitespace. + pass + + def test_create_security_group_with_duplicate_name(self): + # Neutron allows duplicate names for security groups. + pass + + def test_create_security_group_non_string_name(self): + # Neutron allows security group name to be non string. + pass + + def test_create_security_group_non_string_description(self): + # Neutron allows non string description. + pass + + def test_create_security_group_quota_limit(self): + # Enforced by Neutron server. + pass + + def test_update_security_group(self): + # Enforced by Neutron server. + pass + + def test_get_security_group_list(self): + self._create_sg_template().get('security_group') + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + list_dict = self.controller.index(req) + self.assertEqual(len(list_dict['security_groups']), 2) + + def test_get_security_group_list_all_tenants(self): + pass + + def test_get_security_group_by_instance(self): + sg = self._create_sg_template().get('security_group') + net = self._create_network() + self._create_port( + network_id=net['network']['id'], security_groups=[sg['id']], + device_id=test_security_groups.FAKE_UUID1) + expected = [{'rules': [], 'tenant_id': 'fake', 'id': sg['id'], + 'name': 'test', 'description': 'test-description'}] + self.stubs.Set(nova.db, 'instance_get_by_uuid', + test_security_groups.return_server_by_uuid) + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s/os-security-groups' + % test_security_groups.FAKE_UUID1) + res_dict = self.server_controller.index( + req, test_security_groups.FAKE_UUID1)['security_groups'] + self.assertEqual(expected, res_dict) + + def test_get_security_group_by_id(self): + sg = self._create_sg_template().get('security_group') + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/%s' + % sg['id']) + res_dict = self.controller.show(req, sg['id']) + expected = {'security_group': sg} + self.assertEqual(res_dict, expected) + + def test_delete_security_group_by_id(self): + sg = self._create_sg_template().get('security_group') + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/%s' % + sg['id']) + self.controller.delete(req, sg['id']) + + def test_delete_security_group_by_admin(self): + sg = self._create_sg_template().get('security_group') + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/%s' % + sg['id'], use_admin_context=True) + self.controller.delete(req, sg['id']) + + def test_delete_security_group_in_use(self): + sg = self._create_sg_template().get('security_group') + self._create_network() + db_inst = fakes.stub_instance(id=1, nw_cache=[], security_groups=[]) + _context = context.get_admin_context() + instance = instance_obj.Instance._from_db_object( + _context, instance_obj.Instance(), db_inst, + expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS) + neutron = neutron_api.API() + with mock.patch.object(nova.db, 'instance_get_by_uuid', + return_value=db_inst): + neutron.allocate_for_instance(_context, instance, + security_groups=[sg['id']]) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/%s' + % sg['id']) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete, + req, sg['id']) + + def test_associate_non_running_instance(self): + # Neutron does not care if the instance is running or not. When the + # instances is detected by nuetron it will push down the security + # group policy to it. + pass + + def test_associate_already_associated_security_group_to_instance(self): + # Neutron security groups does not raise an error if you update a + # port adding a security group to it that was already associated + # to the port. This is because PUT semantics are used. + pass + + def test_associate(self): + sg = self._create_sg_template().get('security_group') + net = self._create_network() + self._create_port( + network_id=net['network']['id'], security_groups=[sg['id']], + device_id=test_security_groups.FAKE_UUID1) + + self.stubs.Set(nova.db, 'instance_get', + test_security_groups.return_server) + body = dict(addSecurityGroup=dict(name="test")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.manager._addSecurityGroup(req, '1', body) + + def test_associate_duplicate_names(self): + sg1 = self._create_security_group(name='sg1', + description='sg1')['security_group'] + self._create_security_group(name='sg1', + description='sg1')['security_group'] + net = self._create_network() + self._create_port( + network_id=net['network']['id'], security_groups=[sg1['id']], + device_id=test_security_groups.FAKE_UUID1) + + self.stubs.Set(nova.db, 'instance_get', + test_security_groups.return_server) + body = dict(addSecurityGroup=dict(name="sg1")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPConflict, + self.manager._addSecurityGroup, req, '1', body) + + def test_associate_port_security_enabled_true(self): + sg = self._create_sg_template().get('security_group') + net = self._create_network() + self._create_port( + network_id=net['network']['id'], security_groups=[sg['id']], + port_security_enabled=True, + device_id=test_security_groups.FAKE_UUID1) + + self.stubs.Set(nova.db, 'instance_get', + test_security_groups.return_server) + body = dict(addSecurityGroup=dict(name="test")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.manager._addSecurityGroup(req, '1', body) + + def test_associate_port_security_enabled_false(self): + self._create_sg_template().get('security_group') + net = self._create_network() + self._create_port( + network_id=net['network']['id'], port_security_enabled=False, + device_id=test_security_groups.FAKE_UUID1) + + self.stubs.Set(nova.db, 'instance_get', + test_security_groups.return_server) + body = dict(addSecurityGroup=dict(name="test")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._addSecurityGroup, + req, '1', body) + + def test_disassociate_by_non_existing_security_group_name(self): + self.stubs.Set(nova.db, 'instance_get', + test_security_groups.return_server) + body = dict(removeSecurityGroup=dict(name='non-existing')) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPNotFound, + self.manager._removeSecurityGroup, req, '1', body) + + def test_disassociate_non_running_instance(self): + # Neutron does not care if the instance is running or not. When the + # instances is detected by neutron it will push down the security + # group policy to it. + pass + + def test_disassociate_already_associated_security_group_to_instance(self): + # Neutron security groups does not raise an error if you update a + # port adding a security group to it that was already associated + # to the port. This is because PUT semantics are used. + pass + + def test_disassociate(self): + sg = self._create_sg_template().get('security_group') + net = self._create_network() + self._create_port( + network_id=net['network']['id'], security_groups=[sg['id']], + device_id=test_security_groups.FAKE_UUID1) + + self.stubs.Set(nova.db, 'instance_get', + test_security_groups.return_server) + body = dict(removeSecurityGroup=dict(name="test")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.manager._removeSecurityGroup(req, '1', body) + + def test_get_raises_no_unique_match_error(self): + + def fake_find_resourceid_by_name_or_id(client, param, name, + project_id=None): + raise n_exc.NeutronClientNoUniqueMatch() + + self.stubs.Set(neutronv20, 'find_resourceid_by_name_or_id', + fake_find_resourceid_by_name_or_id) + security_group_api = self.controller.security_group_api + self.assertRaises(exception.NoUniqueMatch, security_group_api.get, + context.get_admin_context(), 'foobar') + + def test_get_instances_security_groups_bindings(self): + servers = [{'id': test_security_groups.FAKE_UUID1}, + {'id': test_security_groups.FAKE_UUID2}] + sg1 = self._create_sg_template(name='test1').get('security_group') + sg2 = self._create_sg_template(name='test2').get('security_group') + # test name='' is replaced with id + sg3 = self._create_sg_template(name='').get('security_group') + net = self._create_network() + self._create_port( + network_id=net['network']['id'], security_groups=[sg1['id'], + sg2['id']], + device_id=test_security_groups.FAKE_UUID1) + self._create_port( + network_id=net['network']['id'], security_groups=[sg2['id'], + sg3['id']], + device_id=test_security_groups.FAKE_UUID2) + expected = {test_security_groups.FAKE_UUID1: [{'name': sg1['name']}, + {'name': sg2['name']}], + test_security_groups.FAKE_UUID2: [{'name': sg2['name']}, + {'name': sg3['id']}]} + security_group_api = self.controller.security_group_api + bindings = ( + security_group_api.get_instances_security_groups_bindings( + context.get_admin_context(), servers)) + self.assertEqual(bindings, expected) + + def test_get_instance_security_groups(self): + sg1 = self._create_sg_template(name='test1').get('security_group') + sg2 = self._create_sg_template(name='test2').get('security_group') + # test name='' is replaced with id + sg3 = self._create_sg_template(name='').get('security_group') + net = self._create_network() + self._create_port( + network_id=net['network']['id'], security_groups=[sg1['id'], + sg2['id'], + sg3['id']], + device_id=test_security_groups.FAKE_UUID1) + + expected = [{'name': sg1['name']}, {'name': sg2['name']}, + {'name': sg3['id']}] + security_group_api = self.controller.security_group_api + sgs = security_group_api.get_instance_security_groups( + context.get_admin_context(), test_security_groups.FAKE_UUID1) + self.assertEqual(sgs, expected) + + @mock.patch('nova.network.security_group.neutron_driver.SecurityGroupAPI.' + 'get_instances_security_groups_bindings') + def test_get_security_group_empty_for_instance(self, neutron_sg_bind_mock): + servers = [{'id': test_security_groups.FAKE_UUID1}] + neutron_sg_bind_mock.return_value = {} + + security_group_api = self.controller.security_group_api + ctx = context.get_admin_context() + sgs = security_group_api.get_instance_security_groups(ctx, + test_security_groups.FAKE_UUID1) + + neutron_sg_bind_mock.assert_called_once_with(ctx, servers, False) + self.assertEqual([], sgs) + + def test_create_port_with_sg_and_port_security_enabled_true(self): + sg1 = self._create_sg_template(name='test1').get('security_group') + net = self._create_network() + self._create_port( + network_id=net['network']['id'], security_groups=[sg1['id']], + port_security_enabled=True, + device_id=test_security_groups.FAKE_UUID1) + security_group_api = self.controller.security_group_api + sgs = security_group_api.get_instance_security_groups( + context.get_admin_context(), test_security_groups.FAKE_UUID1) + self.assertEqual(sgs, [{'name': 'test1'}]) + + def test_create_port_with_sg_and_port_security_enabled_false(self): + sg1 = self._create_sg_template(name='test1').get('security_group') + net = self._create_network() + self.assertRaises(exception.SecurityGroupCannotBeApplied, + self._create_port, + network_id=net['network']['id'], + security_groups=[sg1['id']], + port_security_enabled=False, + device_id=test_security_groups.FAKE_UUID1) + + +class TestNeutronSecurityGroupsV2(TestNeutronSecurityGroupsV21): + controller_cls = security_groups.SecurityGroupController + server_secgrp_ctl_cls = security_groups.ServerSecurityGroupController + secgrp_act_ctl_cls = security_groups.SecurityGroupActionController + + +class TestNeutronSecurityGroupRulesTestCase(TestNeutronSecurityGroupsTestCase): + def setUp(self): + super(TestNeutronSecurityGroupRulesTestCase, self).setUp() + id1 = '11111111-1111-1111-1111-111111111111' + sg_template1 = test_security_groups.security_group_template( + security_group_rules=[], id=id1) + id2 = '22222222-2222-2222-2222-222222222222' + sg_template2 = test_security_groups.security_group_template( + security_group_rules=[], id=id2) + self.controller_sg = security_groups.SecurityGroupController() + neutron = get_client() + neutron._fake_security_groups[id1] = sg_template1 + neutron._fake_security_groups[id2] = sg_template2 + + def tearDown(self): + neutronv2.get_client = self.original_client + get_client()._reset() + super(TestNeutronSecurityGroupsTestCase, self).tearDown() + + +class _TestNeutronSecurityGroupRulesBase(object): + + def test_create_add_existing_rules_by_cidr(self): + sg = test_security_groups.security_group_template() + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.controller_sg.create(req, {'security_group': sg}) + rule = test_security_groups.security_group_rule_template( + cidr='15.0.0.0/8', parent_group_id=self.sg2['id']) + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.controller.create(req, {'security_group_rule': rule}) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_add_existing_rules_by_group_id(self): + sg = test_security_groups.security_group_template() + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.controller_sg.create(req, {'security_group': sg}) + rule = test_security_groups.security_group_rule_template( + group=self.sg1['id'], parent_group_id=self.sg2['id']) + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.controller.create(req, {'security_group_rule': rule}) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_delete(self): + rule = test_security_groups.security_group_rule_template( + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + security_group_rule = res_dict['security_group_rule'] + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules/%s' + % security_group_rule['id']) + self.controller.delete(req, security_group_rule['id']) + + def test_create_rule_quota_limit(self): + # Enforced by neutron + pass + + +class TestNeutronSecurityGroupRulesV2( + _TestNeutronSecurityGroupRulesBase, + test_security_groups.TestSecurityGroupRulesV2, + TestNeutronSecurityGroupRulesTestCase): + pass + + +class TestNeutronSecurityGroupRulesV21( + _TestNeutronSecurityGroupRulesBase, + test_security_groups.TestSecurityGroupRulesV21, + TestNeutronSecurityGroupRulesTestCase): + pass + + +class TestNeutronSecurityGroupsXMLDeserializer( + test_security_groups.TestSecurityGroupXMLDeserializer, + TestNeutronSecurityGroupsTestCase): + pass + + +class TestNeutronSecurityGroupsXMLSerializer( + test_security_groups.TestSecurityGroupXMLSerializer, + TestNeutronSecurityGroupsTestCase): + pass + + +class TestNeutronSecurityGroupsOutputTest(TestNeutronSecurityGroupsTestCase): + content_type = 'application/json' + + def setUp(self): + super(TestNeutronSecurityGroupsOutputTest, self).setUp() + fakes.stub_out_nw_api(self.stubs) + self.controller = security_groups.SecurityGroupController() + self.stubs.Set(compute.api.API, 'get', + test_security_groups.fake_compute_get) + self.stubs.Set(compute.api.API, 'get_all', + test_security_groups.fake_compute_get_all) + self.stubs.Set(compute.api.API, 'create', + test_security_groups.fake_compute_create) + self.stubs.Set(neutron_driver.SecurityGroupAPI, + 'get_instances_security_groups_bindings', + (test_security_groups. + fake_get_instances_security_groups_bindings)) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Security_groups']) + + def _make_request(self, url, body=None): + req = webob.Request.blank(url) + if body: + req.method = 'POST' + req.body = self._encode_body(body) + req.content_type = self.content_type + req.headers['Accept'] = self.content_type + res = req.get_response(fakes.wsgi_app(init_only=('servers',))) + return res + + def _encode_body(self, body): + return jsonutils.dumps(body) + + def _get_server(self, body): + return jsonutils.loads(body).get('server') + + def _get_servers(self, body): + return jsonutils.loads(body).get('servers') + + def _get_groups(self, server): + return server.get('security_groups') + + def test_create(self): + url = '/v2/fake/servers' + image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + security_groups = [{'name': 'fake-2-0'}, {'name': 'fake-2-1'}] + for security_group in security_groups: + sg = test_security_groups.security_group_template( + name=security_group['name']) + self.controller.create(req, {'security_group': sg}) + + server = dict(name='server_test', imageRef=image_uuid, flavorRef=2, + security_groups=security_groups) + res = self._make_request(url, {'server': server}) + self.assertEqual(res.status_int, 202) + server = self._get_server(res.body) + for i, group in enumerate(self._get_groups(server)): + name = 'fake-2-%s' % i + self.assertEqual(group.get('name'), name) + + def test_create_server_get_default_security_group(self): + url = '/v2/fake/servers' + image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + server = dict(name='server_test', imageRef=image_uuid, flavorRef=2) + res = self._make_request(url, {'server': server}) + self.assertEqual(res.status_int, 202) + server = self._get_server(res.body) + group = self._get_groups(server)[0] + self.assertEqual(group.get('name'), 'default') + + def test_show(self): + def fake_get_instance_security_groups(inst, context, id): + return [{'name': 'fake-2-0'}, {'name': 'fake-2-1'}] + + self.stubs.Set(neutron_driver.SecurityGroupAPI, + 'get_instance_security_groups', + fake_get_instance_security_groups) + + url = '/v2/fake/servers' + image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + security_groups = [{'name': 'fake-2-0'}, {'name': 'fake-2-1'}] + for security_group in security_groups: + sg = test_security_groups.security_group_template( + name=security_group['name']) + self.controller.create(req, {'security_group': sg}) + server = dict(name='server_test', imageRef=image_uuid, flavorRef=2, + security_groups=security_groups) + + res = self._make_request(url, {'server': server}) + self.assertEqual(res.status_int, 202) + server = self._get_server(res.body) + for i, group in enumerate(self._get_groups(server)): + name = 'fake-2-%s' % i + self.assertEqual(group.get('name'), name) + + # Test that show (GET) returns the same information as create (POST) + url = '/v2/fake/servers/' + test_security_groups.UUID3 + res = self._make_request(url) + self.assertEqual(res.status_int, 200) + server = self._get_server(res.body) + + for i, group in enumerate(self._get_groups(server)): + name = 'fake-2-%s' % i + self.assertEqual(group.get('name'), name) + + def test_detail(self): + url = '/v2/fake/servers/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + for i, server in enumerate(self._get_servers(res.body)): + for j, group in enumerate(self._get_groups(server)): + name = 'fake-%s-%s' % (i, j) + self.assertEqual(group.get('name'), name) + + def test_no_instance_passthrough_404(self): + + def fake_compute_get(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + url = '/v2/fake/servers/70f6db34-de8d-4fbd-aafb-4065bdfa6115' + res = self._make_request(url) + + self.assertEqual(res.status_int, 404) + + +class TestNeutronSecurityGroupsOutputXMLTest( + TestNeutronSecurityGroupsOutputTest): + + content_type = 'application/xml' + + class MinimalCreateServerTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server', selector='server') + root.set('name') + root.set('id') + root.set('imageRef') + root.set('flavorRef') + elem = xmlutil.SubTemplateElement(root, 'security_groups') + sg = xmlutil.SubTemplateElement(elem, 'security_group', + selector='security_groups') + sg.set('name') + return xmlutil.MasterTemplate(root, 1, + nsmap={None: xmlutil.XMLNS_V11}) + + def _encode_body(self, body): + serializer = self.MinimalCreateServerTemplate() + return serializer.serialize(body) + + def _get_server(self, body): + return etree.XML(body) + + def _get_servers(self, body): + return etree.XML(body).getchildren() + + def _get_groups(self, server): + # NOTE(vish): we are adding security groups without an extension + # namespace so we don't break people using the existing + # functionality, but that means we need to use find with + # the existing server namespace. + namespace = server.nsmap[None] + return server.find('{%s}security_groups' % namespace).getchildren() + + +def get_client(context=None, admin=False): + return MockClient() + + +class MockClient(object): + + # Needs to be global to survive multiple calls to get_client. + _fake_security_groups = {} + _fake_ports = {} + _fake_networks = {} + _fake_subnets = {} + _fake_security_group_rules = {} + + def __init__(self): + # add default security group + if not len(self._fake_security_groups): + ret = {'name': 'default', 'description': 'default', + 'tenant_id': 'fake_tenant', 'security_group_rules': [], + 'id': str(uuid.uuid4())} + self._fake_security_groups[ret['id']] = ret + + def _reset(self): + self._fake_security_groups.clear() + self._fake_ports.clear() + self._fake_networks.clear() + self._fake_subnets.clear() + self._fake_security_group_rules.clear() + + def create_security_group(self, body=None): + s = body.get('security_group') + if len(s.get('name')) > 255 or len(s.get('description')) > 255: + msg = 'Security Group name great than 255' + raise n_exc.NeutronClientException(message=msg, status_code=401) + ret = {'name': s.get('name'), 'description': s.get('description'), + 'tenant_id': 'fake', 'security_group_rules': [], + 'id': str(uuid.uuid4())} + + self._fake_security_groups[ret['id']] = ret + return {'security_group': ret} + + def create_network(self, body): + n = body.get('network') + ret = {'status': 'ACTIVE', 'subnets': [], 'name': n.get('name'), + 'admin_state_up': n.get('admin_state_up', True), + 'tenant_id': 'fake_tenant', + 'id': str(uuid.uuid4())} + if 'port_security_enabled' in n: + ret['port_security_enabled'] = n['port_security_enabled'] + self._fake_networks[ret['id']] = ret + return {'network': ret} + + def create_subnet(self, body): + s = body.get('subnet') + try: + net = self._fake_networks[s.get('network_id')] + except KeyError: + msg = 'Network %s not found' % s.get('network_id') + raise n_exc.NeutronClientException(message=msg, status_code=404) + ret = {'name': s.get('name'), 'network_id': s.get('network_id'), + 'tenant_id': 'fake_tenant', 'cidr': s.get('cidr'), + 'id': str(uuid.uuid4()), 'gateway_ip': '10.0.0.1'} + net['subnets'].append(ret['id']) + self._fake_networks[net['id']] = net + self._fake_subnets[ret['id']] = ret + return {'subnet': ret} + + def create_port(self, body): + p = body.get('port') + ret = {'status': 'ACTIVE', 'id': str(uuid.uuid4()), + 'mac_address': p.get('mac_address', 'fa:16:3e:b8:f5:fb'), + 'device_id': p.get('device_id', str(uuid.uuid4())), + 'admin_state_up': p.get('admin_state_up', True), + 'security_groups': p.get('security_groups', []), + 'network_id': p.get('network_id'), + 'binding:vnic_type': + p.get('binding:vnic_type') or model.VNIC_TYPE_NORMAL} + + network = self._fake_networks[p['network_id']] + if 'port_security_enabled' in p: + ret['port_security_enabled'] = p['port_security_enabled'] + elif 'port_security_enabled' in network: + ret['port_security_enabled'] = network['port_security_enabled'] + + port_security = ret.get('port_security_enabled', True) + # port_security must be True if security groups are present + if not port_security and ret['security_groups']: + raise exception.SecurityGroupCannotBeApplied() + + if network['subnets']: + ret['fixed_ips'] = [{'subnet_id': network['subnets'][0], + 'ip_address': '10.0.0.1'}] + if not ret['security_groups'] and (port_security is None or + port_security is True): + for security_group in self._fake_security_groups.values(): + if security_group['name'] == 'default': + ret['security_groups'] = [security_group['id']] + break + self._fake_ports[ret['id']] = ret + return {'port': ret} + + def create_security_group_rule(self, body): + # does not handle bulk case so just picks rule[0] + r = body.get('security_group_rules')[0] + fields = ['direction', 'protocol', 'port_range_min', 'port_range_max', + 'ethertype', 'remote_ip_prefix', 'tenant_id', + 'security_group_id', 'remote_group_id'] + ret = {} + for field in fields: + ret[field] = r.get(field) + ret['id'] = str(uuid.uuid4()) + self._fake_security_group_rules[ret['id']] = ret + return {'security_group_rules': [ret]} + + def show_security_group(self, security_group, **_params): + try: + sg = self._fake_security_groups[security_group] + except KeyError: + msg = 'Security Group %s not found' % security_group + raise n_exc.NeutronClientException(message=msg, status_code=404) + for security_group_rule in self._fake_security_group_rules.values(): + if security_group_rule['security_group_id'] == sg['id']: + sg['security_group_rules'].append(security_group_rule) + + return {'security_group': sg} + + def show_security_group_rule(self, security_group_rule, **_params): + try: + return {'security_group_rule': + self._fake_security_group_rules[security_group_rule]} + except KeyError: + msg = 'Security Group rule %s not found' % security_group_rule + raise n_exc.NeutronClientException(message=msg, status_code=404) + + def show_network(self, network, **_params): + try: + return {'network': + self._fake_networks[network]} + except KeyError: + msg = 'Network %s not found' % network + raise n_exc.NeutronClientException(message=msg, status_code=404) + + def show_port(self, port, **_params): + try: + return {'port': + self._fake_ports[port]} + except KeyError: + msg = 'Port %s not found' % port + raise n_exc.NeutronClientException(message=msg, status_code=404) + + def show_subnet(self, subnet, **_params): + try: + return {'subnet': + self._fake_subnets[subnet]} + except KeyError: + msg = 'Port %s not found' % subnet + raise n_exc.NeutronClientException(message=msg, status_code=404) + + def list_security_groups(self, **_params): + ret = [] + for security_group in self._fake_security_groups.values(): + names = _params.get('name') + if names: + if not isinstance(names, list): + names = [names] + for name in names: + if security_group.get('name') == name: + ret.append(security_group) + ids = _params.get('id') + if ids: + if not isinstance(ids, list): + ids = [ids] + for id in ids: + if security_group.get('id') == id: + ret.append(security_group) + elif not (names or ids): + ret.append(security_group) + return {'security_groups': ret} + + def list_networks(self, **_params): + # neutronv2/api.py _get_available_networks calls this assuming + # search_opts filter "shared" is implemented and not ignored + shared = _params.get("shared", None) + if shared: + return {'networks': []} + else: + return {'networks': + [network for network in self._fake_networks.values()]} + + def list_ports(self, **_params): + ret = [] + device_id = _params.get('device_id') + for port in self._fake_ports.values(): + if device_id: + if port['device_id'] in device_id: + ret.append(port) + else: + ret.append(port) + return {'ports': ret} + + def list_subnets(self, **_params): + return {'subnets': + [subnet for subnet in self._fake_subnets.values()]} + + def list_floatingips(self, **_params): + return {'floatingips': []} + + def delete_security_group(self, security_group): + self.show_security_group(security_group) + ports = self.list_ports() + for port in ports.get('ports'): + for sg_port in port['security_groups']: + if sg_port == security_group: + msg = ('Unable to delete Security group %s in use' + % security_group) + raise n_exc.NeutronClientException(message=msg, + status_code=409) + del self._fake_security_groups[security_group] + + def delete_security_group_rule(self, security_group_rule): + self.show_security_group_rule(security_group_rule) + del self._fake_security_group_rules[security_group_rule] + + def delete_network(self, network): + self.show_network(network) + self._check_ports_on_network(network) + for subnet in self._fake_subnets.values(): + if subnet['network_id'] == network: + del self._fake_subnets[subnet['id']] + del self._fake_networks[network] + + def delete_subnet(self, subnet): + subnet = self.show_subnet(subnet).get('subnet') + self._check_ports_on_network(subnet['network_id']) + del self._fake_subnet[subnet] + + def delete_port(self, port): + self.show_port(port) + del self._fake_ports[port] + + def update_port(self, port, body=None): + self.show_port(port) + self._fake_ports[port].update(body['port']) + return {'port': self._fake_ports[port]} + + def list_extensions(self, **_parms): + return {'extensions': []} + + def _check_ports_on_network(self, network): + ports = self.list_ports() + for port in ports: + if port['network_id'] == network: + msg = ('Unable to complete operation on network %s. There is ' + 'one or more ports still in use on the network' + % network) + raise n_exc.NeutronClientException(message=msg, status_code=409) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_quota_classes.py b/nova/tests/unit/api/openstack/compute/contrib/test_quota_classes.py new file mode 100644 index 0000000000..228b44f369 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_quota_classes.py @@ -0,0 +1,222 @@ +# 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. + +from lxml import etree +import webob + +from nova.api.openstack.compute.contrib import quota_classes +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova import test +from nova.tests.unit.api.openstack import fakes + + +def quota_set(class_name): + return {'quota_class_set': {'id': class_name, 'metadata_items': 128, + 'ram': 51200, 'floating_ips': 10, + 'fixed_ips': -1, 'instances': 10, + 'injected_files': 5, 'cores': 20, + 'injected_file_content_bytes': 10240, + 'security_groups': 10, + 'security_group_rules': 20, 'key_pairs': 100, + 'injected_file_path_bytes': 255}} + + +class QuotaClassSetsTest(test.TestCase): + + def setUp(self): + super(QuotaClassSetsTest, self).setUp() + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = quota_classes.QuotaClassSetsController(self.ext_mgr) + + def test_format_quota_set(self): + raw_quota_set = { + 'instances': 10, + 'cores': 20, + 'ram': 51200, + 'floating_ips': 10, + 'fixed_ips': -1, + 'metadata_items': 128, + 'injected_files': 5, + 'injected_file_path_bytes': 255, + 'injected_file_content_bytes': 10240, + 'security_groups': 10, + 'security_group_rules': 20, + 'key_pairs': 100, + } + + quota_set = self.controller._format_quota_set('test_class', + raw_quota_set) + qs = quota_set['quota_class_set'] + + self.assertEqual(qs['id'], 'test_class') + self.assertEqual(qs['instances'], 10) + self.assertEqual(qs['cores'], 20) + self.assertEqual(qs['ram'], 51200) + self.assertEqual(qs['floating_ips'], 10) + self.assertEqual(qs['fixed_ips'], -1) + self.assertEqual(qs['metadata_items'], 128) + self.assertEqual(qs['injected_files'], 5) + self.assertEqual(qs['injected_file_path_bytes'], 255) + self.assertEqual(qs['injected_file_content_bytes'], 10240) + self.assertEqual(qs['security_groups'], 10) + self.assertEqual(qs['security_group_rules'], 20) + self.assertEqual(qs['key_pairs'], 100) + + def test_quotas_show_as_admin(self): + req = fakes.HTTPRequest.blank( + '/v2/fake4/os-quota-class-sets/test_class', + use_admin_context=True) + res_dict = self.controller.show(req, 'test_class') + + self.assertEqual(res_dict, quota_set('test_class')) + + def test_quotas_show_as_unauthorized_user(self): + req = fakes.HTTPRequest.blank( + '/v2/fake4/os-quota-class-sets/test_class') + self.assertRaises(webob.exc.HTTPForbidden, self.controller.show, + req, 'test_class') + + def test_quotas_update_as_admin(self): + body = {'quota_class_set': {'instances': 50, 'cores': 50, + 'ram': 51200, 'floating_ips': 10, + 'fixed_ips': -1, 'metadata_items': 128, + 'injected_files': 5, + 'injected_file_content_bytes': 10240, + 'injected_file_path_bytes': 255, + 'security_groups': 10, + 'security_group_rules': 20, + 'key_pairs': 100}} + + req = fakes.HTTPRequest.blank( + '/v2/fake4/os-quota-class-sets/test_class', + use_admin_context=True) + res_dict = self.controller.update(req, 'test_class', body) + + self.assertEqual(res_dict, body) + + def test_quotas_update_as_user(self): + body = {'quota_class_set': {'instances': 50, 'cores': 50, + 'ram': 51200, 'floating_ips': 10, + 'fixed_ips': -1, 'metadata_items': 128, + 'injected_files': 5, + 'injected_file_content_bytes': 10240, + 'security_groups': 10, + 'security_group_rules': 20, + 'key_pairs': 100, + }} + + req = fakes.HTTPRequest.blank( + '/v2/fake4/os-quota-class-sets/test_class') + self.assertRaises(webob.exc.HTTPForbidden, self.controller.update, + req, 'test_class', body) + + def test_quotas_update_with_empty_body(self): + body = {} + req = fakes.HTTPRequest.blank( + '/v2/fake4/os-quota-class-sets/test_class', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 'test_class', body) + + def test_quotas_update_with_non_integer(self): + body = {'quota_class_set': {'instances': "abc"}} + req = fakes.HTTPRequest.blank( + '/v2/fake4/os-quota-class-sets/test_class', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 'test_class', body) + + body = {'quota_class_set': {'instances': 50.5}} + req = fakes.HTTPRequest.blank( + '/v2/fake4/os-quota-class-sets/test_class', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 'test_class', body) + + body = {'quota_class_set': { + 'instances': u'\u30aa\u30fc\u30d7\u30f3'}} + req = fakes.HTTPRequest.blank( + '/v2/fake4/os-quota-class-sets/test_class', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 'test_class', body) + + +class QuotaTemplateXMLSerializerTest(test.TestCase): + def setUp(self): + super(QuotaTemplateXMLSerializerTest, self).setUp() + self.serializer = quota_classes.QuotaClassTemplate() + self.deserializer = wsgi.XMLDeserializer() + + def test_serializer(self): + exemplar = dict(quota_class_set=dict( + id='test_class', + metadata_items=10, + injected_file_path_bytes=255, + injected_file_content_bytes=20, + ram=50, + floating_ips=60, + fixed_ips=-1, + instances=70, + injected_files=80, + security_groups=10, + security_group_rules=20, + key_pairs=100, + cores=90)) + text = self.serializer.serialize(exemplar) + + tree = etree.fromstring(text) + + self.assertEqual('quota_class_set', tree.tag) + self.assertEqual('test_class', tree.get('id')) + self.assertEqual(len(exemplar['quota_class_set']) - 1, len(tree)) + for child in tree: + self.assertIn(child.tag, exemplar['quota_class_set']) + self.assertEqual(int(child.text), + exemplar['quota_class_set'][child.tag]) + + def test_deserializer(self): + exemplar = dict(quota_class_set=dict( + metadata_items='10', + injected_file_content_bytes='20', + ram='50', + floating_ips='60', + fixed_ips='-1', + instances='70', + injected_files='80', + security_groups='10', + security_group_rules='20', + key_pairs='100', + cores='90')) + intext = ("<?xml version='1.0' encoding='UTF-8'?>\n" + '<quota_class_set>' + '<metadata_items>10</metadata_items>' + '<injected_file_content_bytes>20' + '</injected_file_content_bytes>' + '<ram>50</ram>' + '<floating_ips>60</floating_ips>' + '<fixed_ips>-1</fixed_ips>' + '<instances>70</instances>' + '<injected_files>80</injected_files>' + '<cores>90</cores>' + '<security_groups>10</security_groups>' + '<security_group_rules>20</security_group_rules>' + '<key_pairs>100</key_pairs>' + '</quota_class_set>') + + result = self.deserializer.deserialize(intext)['body'] + self.assertEqual(result, exemplar) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_quotas.py b/nova/tests/unit/api/openstack/compute/contrib/test_quotas.py new file mode 100644 index 0000000000..33511b0cc3 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_quotas.py @@ -0,0 +1,648 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# 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 copy + +from lxml import etree +import mock +import webob + +from nova.api.openstack.compute.contrib import quotas as quotas_v2 +from nova.api.openstack.compute.plugins.v3 import quota_sets as quotas_v21 +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova import context as context_maker +from nova import exception +from nova import quota +from nova import test +from nova.tests.unit.api.openstack import fakes + + +def quota_set(id, include_server_group_quotas=True): + res = {'quota_set': {'id': id, 'metadata_items': 128, + 'ram': 51200, 'floating_ips': 10, 'fixed_ips': -1, + 'instances': 10, 'injected_files': 5, 'cores': 20, + 'injected_file_content_bytes': 10240, + 'security_groups': 10, 'security_group_rules': 20, + 'key_pairs': 100, 'injected_file_path_bytes': 255}} + if include_server_group_quotas: + res['quota_set']['server_groups'] = 10 + res['quota_set']['server_group_members'] = 10 + return res + + +class BaseQuotaSetsTest(test.TestCase): + + def _is_v20_api_test(self): + # NOTE(oomichi): If a test is for v2.0 API, this method returns + # True. Otherwise(v2.1 API test), returns False. + return (self.plugin == quotas_v2) + + def get_update_expected_response(self, base_body): + # NOTE(oomichi): "id" parameter is added to a response of + # "update quota" API since v2.1 API, because it makes the + # API consistent and it is not backwards incompatible change. + # This method adds "id" for an expected body of a response. + if self._is_v20_api_test(): + expected_body = base_body + else: + expected_body = copy.deepcopy(base_body) + expected_body['quota_set'].update({'id': 'update_me'}) + return expected_body + + def setup_mock_for_show(self): + if self._is_v20_api_test(): + self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True) + self.mox.ReplayAll() + + def setup_mock_for_update(self): + if self._is_v20_api_test(): + self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True) + self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True) + self.mox.ReplayAll() + + def get_delete_status_int(self, res): + if self._is_v20_api_test(): + return res.status_int + else: + # NOTE: on v2.1, http status code is set as wsgi_code of API + # method instead of status_int in a response object. + return self.controller.delete.wsgi_code + + +class QuotaSetsTestV21(BaseQuotaSetsTest): + plugin = quotas_v21 + validation_error = exception.ValidationError + include_server_group_quotas = True + + def setUp(self): + super(QuotaSetsTestV21, self).setUp() + self._setup_controller() + self.default_quotas = { + 'instances': 10, + 'cores': 20, + 'ram': 51200, + 'floating_ips': 10, + 'fixed_ips': -1, + 'metadata_items': 128, + 'injected_files': 5, + 'injected_file_path_bytes': 255, + 'injected_file_content_bytes': 10240, + 'security_groups': 10, + 'security_group_rules': 20, + 'key_pairs': 100, + } + if self.include_server_group_quotas: + self.default_quotas['server_groups'] = 10 + self.default_quotas['server_group_members'] = 10 + + def _setup_controller(self): + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.controller = self.plugin.QuotaSetsController(self.ext_mgr) + + def test_format_quota_set(self): + quota_set = self.controller._format_quota_set('1234', + self.default_quotas) + qs = quota_set['quota_set'] + + self.assertEqual(qs['id'], '1234') + self.assertEqual(qs['instances'], 10) + self.assertEqual(qs['cores'], 20) + self.assertEqual(qs['ram'], 51200) + self.assertEqual(qs['floating_ips'], 10) + self.assertEqual(qs['fixed_ips'], -1) + self.assertEqual(qs['metadata_items'], 128) + self.assertEqual(qs['injected_files'], 5) + self.assertEqual(qs['injected_file_path_bytes'], 255) + self.assertEqual(qs['injected_file_content_bytes'], 10240) + self.assertEqual(qs['security_groups'], 10) + self.assertEqual(qs['security_group_rules'], 20) + self.assertEqual(qs['key_pairs'], 100) + if self.include_server_group_quotas: + self.assertEqual(qs['server_groups'], 10) + self.assertEqual(qs['server_group_members'], 10) + + def test_quotas_defaults(self): + uri = '/v2/fake_tenant/os-quota-sets/fake_tenant/defaults' + + req = fakes.HTTPRequest.blank(uri) + res_dict = self.controller.defaults(req, 'fake_tenant') + self.default_quotas.update({'id': 'fake_tenant'}) + expected = {'quota_set': self.default_quotas} + + self.assertEqual(res_dict, expected) + + def test_quotas_show_as_admin(self): + self.setup_mock_for_show() + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234', + use_admin_context=True) + res_dict = self.controller.show(req, 1234) + + ref_quota_set = quota_set('1234', self.include_server_group_quotas) + self.assertEqual(res_dict, ref_quota_set) + + def test_quotas_show_as_unauthorized_user(self): + self.setup_mock_for_show() + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234') + self.assertRaises(webob.exc.HTTPForbidden, self.controller.show, + req, 1234) + + def test_quotas_update_as_admin(self): + self.setup_mock_for_update() + self.default_quotas.update({ + 'instances': 50, + 'cores': 50 + }) + body = {'quota_set': self.default_quotas} + expected_body = self.get_update_expected_response(body) + + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me', + use_admin_context=True) + res_dict = self.controller.update(req, 'update_me', body=body) + self.assertEqual(expected_body, res_dict) + + def test_quotas_update_zero_value_as_admin(self): + self.setup_mock_for_update() + body = {'quota_set': {'instances': 0, 'cores': 0, + 'ram': 0, 'floating_ips': 0, + 'metadata_items': 0, + 'injected_files': 0, + 'injected_file_content_bytes': 0, + 'injected_file_path_bytes': 0, + 'security_groups': 0, + 'security_group_rules': 0, + 'key_pairs': 100, 'fixed_ips': -1}} + if self.include_server_group_quotas: + body['quota_set']['server_groups'] = 10 + body['quota_set']['server_group_members'] = 10 + expected_body = self.get_update_expected_response(body) + + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me', + use_admin_context=True) + res_dict = self.controller.update(req, 'update_me', body=body) + self.assertEqual(expected_body, res_dict) + + def test_quotas_update_as_user(self): + self.setup_mock_for_update() + self.default_quotas.update({ + 'instances': 50, + 'cores': 50 + }) + body = {'quota_set': self.default_quotas} + + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me') + self.assertRaises(webob.exc.HTTPForbidden, self.controller.update, + req, 'update_me', body=body) + + def _quotas_update_bad_request_case(self, body): + self.setup_mock_for_update() + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me', + use_admin_context=True) + self.assertRaises(self.validation_error, self.controller.update, + req, 'update_me', body=body) + + def test_quotas_update_invalid_key(self): + body = {'quota_set': {'instances2': -2, 'cores': -2, + 'ram': -2, 'floating_ips': -2, + 'metadata_items': -2, 'injected_files': -2, + 'injected_file_content_bytes': -2}} + self._quotas_update_bad_request_case(body) + + def test_quotas_update_invalid_limit(self): + body = {'quota_set': {'instances': -2, 'cores': -2, + 'ram': -2, 'floating_ips': -2, 'fixed_ips': -2, + 'metadata_items': -2, 'injected_files': -2, + 'injected_file_content_bytes': -2}} + self._quotas_update_bad_request_case(body) + + def test_quotas_update_empty_body(self): + body = {} + self._quotas_update_bad_request_case(body) + + def test_quotas_update_invalid_value_non_int(self): + # when PUT non integer value + self.default_quotas.update({ + 'instances': 'test' + }) + body = {'quota_set': self.default_quotas} + self._quotas_update_bad_request_case(body) + + def test_quotas_update_invalid_value_with_float(self): + # when PUT non integer value + self.default_quotas.update({ + 'instances': 50.5 + }) + body = {'quota_set': self.default_quotas} + self._quotas_update_bad_request_case(body) + + def test_quotas_update_invalid_value_with_unicode(self): + # when PUT non integer value + self.default_quotas.update({ + 'instances': u'\u30aa\u30fc\u30d7\u30f3' + }) + body = {'quota_set': self.default_quotas} + self._quotas_update_bad_request_case(body) + + def test_quotas_delete_as_unauthorized_user(self): + if self._is_v20_api_test(): + self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True) + self.mox.ReplayAll() + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234') + self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete, + req, 1234) + + def test_quotas_delete_as_admin(self): + if self._is_v20_api_test(): + self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True) + context = context_maker.get_admin_context() + self.req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234') + self.req.environ['nova.context'] = context + self.mox.StubOutWithMock(quota.QUOTAS, + "destroy_all_by_project") + quota.QUOTAS.destroy_all_by_project(context, 1234) + self.mox.ReplayAll() + res = self.controller.delete(self.req, 1234) + self.mox.VerifyAll() + self.assertEqual(202, self.get_delete_status_int(res)) + + +class QuotaXMLSerializerTest(test.TestCase): + def setUp(self): + super(QuotaXMLSerializerTest, self).setUp() + self.serializer = quotas_v2.QuotaTemplate() + self.deserializer = wsgi.XMLDeserializer() + + def test_serializer(self): + exemplar = dict(quota_set=dict( + id='project_id', + metadata_items=10, + injected_file_path_bytes=255, + injected_file_content_bytes=20, + ram=50, + floating_ips=60, + fixed_ips=-1, + instances=70, + injected_files=80, + security_groups=10, + security_group_rules=20, + key_pairs=100, + cores=90)) + text = self.serializer.serialize(exemplar) + + tree = etree.fromstring(text) + + self.assertEqual('quota_set', tree.tag) + self.assertEqual('project_id', tree.get('id')) + self.assertEqual(len(exemplar['quota_set']) - 1, len(tree)) + for child in tree: + self.assertIn(child.tag, exemplar['quota_set']) + self.assertEqual(int(child.text), exemplar['quota_set'][child.tag]) + + def test_deserializer(self): + exemplar = dict(quota_set=dict( + metadata_items='10', + injected_file_content_bytes='20', + ram='50', + floating_ips='60', + fixed_ips='-1', + instances='70', + injected_files='80', + security_groups='10', + security_group_rules='20', + key_pairs='100', + cores='90')) + intext = ("<?xml version='1.0' encoding='UTF-8'?>\n" + '<quota_set>' + '<metadata_items>10</metadata_items>' + '<injected_file_content_bytes>20' + '</injected_file_content_bytes>' + '<ram>50</ram>' + '<floating_ips>60</floating_ips>' + '<fixed_ips>-1</fixed_ips>' + '<instances>70</instances>' + '<injected_files>80</injected_files>' + '<security_groups>10</security_groups>' + '<security_group_rules>20</security_group_rules>' + '<key_pairs>100</key_pairs>' + '<cores>90</cores>' + '</quota_set>') + + result = self.deserializer.deserialize(intext)['body'] + self.assertEqual(result, exemplar) + + +class ExtendedQuotasTestV21(BaseQuotaSetsTest): + plugin = quotas_v21 + + def setUp(self): + super(ExtendedQuotasTestV21, self).setUp() + self._setup_controller() + self.setup_mock_for_update() + + fake_quotas = {'ram': {'limit': 51200, + 'in_use': 12800, + 'reserved': 12800}, + 'cores': {'limit': 20, + 'in_use': 10, + 'reserved': 5}, + 'instances': {'limit': 100, + 'in_use': 0, + 'reserved': 0}} + + def _setup_controller(self): + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.controller = self.plugin.QuotaSetsController(self.ext_mgr) + + def fake_get_quotas(self, context, id, user_id=None, usages=False): + if usages: + return self.fake_quotas + else: + return dict((k, v['limit']) for k, v in self.fake_quotas.items()) + + def fake_get_settable_quotas(self, context, project_id, user_id=None): + return { + 'ram': {'minimum': self.fake_quotas['ram']['in_use'] + + self.fake_quotas['ram']['reserved'], + 'maximum': -1}, + 'cores': {'minimum': self.fake_quotas['cores']['in_use'] + + self.fake_quotas['cores']['reserved'], + 'maximum': -1}, + 'instances': {'minimum': self.fake_quotas['instances']['in_use'] + + self.fake_quotas['instances']['reserved'], + 'maximum': -1}, + } + + def test_quotas_update_exceed_in_used(self): + patcher = mock.patch.object(quota.QUOTAS, 'get_settable_quotas') + get_settable_quotas = patcher.start() + + body = {'quota_set': {'cores': 10}} + + get_settable_quotas.side_effect = self.fake_get_settable_quotas + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 'update_me', body=body) + mock.patch.stopall() + + def test_quotas_force_update_exceed_in_used(self): + patcher = mock.patch.object(quota.QUOTAS, 'get_settable_quotas') + get_settable_quotas = patcher.start() + patcher = mock.patch.object(self.plugin.QuotaSetsController, + '_get_quotas') + _get_quotas = patcher.start() + + body = {'quota_set': {'cores': 10, 'force': 'True'}} + + get_settable_quotas.side_effect = self.fake_get_settable_quotas + _get_quotas.side_effect = self.fake_get_quotas + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me', + use_admin_context=True) + self.controller.update(req, 'update_me', body=body) + mock.patch.stopall() + + +class UserQuotasTestV21(BaseQuotaSetsTest): + plugin = quotas_v21 + include_server_group_quotas = True + + def setUp(self): + super(UserQuotasTestV21, self).setUp() + self._setup_controller() + + def _setup_controller(self): + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.controller = self.plugin.QuotaSetsController(self.ext_mgr) + + def test_user_quotas_show_as_admin(self): + self.setup_mock_for_show() + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1', + use_admin_context=True) + res_dict = self.controller.show(req, 1234) + ref_quota_set = quota_set('1234', self.include_server_group_quotas) + self.assertEqual(res_dict, ref_quota_set) + + def test_user_quotas_show_as_unauthorized_user(self): + self.setup_mock_for_show() + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1') + self.assertRaises(webob.exc.HTTPForbidden, self.controller.show, + req, 1234) + + def test_user_quotas_update_as_admin(self): + self.setup_mock_for_update() + body = {'quota_set': {'instances': 10, 'cores': 20, + 'ram': 51200, 'floating_ips': 10, + 'fixed_ips': -1, 'metadata_items': 128, + 'injected_files': 5, + 'injected_file_content_bytes': 10240, + 'injected_file_path_bytes': 255, + 'security_groups': 10, + 'security_group_rules': 20, + 'key_pairs': 100}} + if self.include_server_group_quotas: + body['quota_set']['server_groups'] = 10 + body['quota_set']['server_group_members'] = 10 + + expected_body = self.get_update_expected_response(body) + + url = '/v2/fake4/os-quota-sets/update_me?user_id=1' + req = fakes.HTTPRequest.blank(url, use_admin_context=True) + res_dict = self.controller.update(req, 'update_me', body=body) + + self.assertEqual(expected_body, res_dict) + + def test_user_quotas_update_as_user(self): + self.setup_mock_for_update() + body = {'quota_set': {'instances': 10, 'cores': 20, + 'ram': 51200, 'floating_ips': 10, + 'fixed_ips': -1, 'metadata_items': 128, + 'injected_files': 5, + 'injected_file_content_bytes': 10240, + 'security_groups': 10, + 'security_group_rules': 20, + 'key_pairs': 100, + 'server_groups': 10, + 'server_group_members': 10}} + + url = '/v2/fake4/os-quota-sets/update_me?user_id=1' + req = fakes.HTTPRequest.blank(url) + self.assertRaises(webob.exc.HTTPForbidden, self.controller.update, + req, 'update_me', body=body) + + def test_user_quotas_update_exceed_project(self): + self.setup_mock_for_update() + body = {'quota_set': {'instances': 20}} + + url = '/v2/fake4/os-quota-sets/update_me?user_id=1' + req = fakes.HTTPRequest.blank(url, use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 'update_me', body=body) + + def test_user_quotas_delete_as_unauthorized_user(self): + self.setup_mock_for_update() + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1') + self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete, + req, 1234) + + def test_user_quotas_delete_as_admin(self): + if self._is_v20_api_test(): + self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True) + self.ext_mgr.is_loaded('os-user-quotas').AndReturn(True) + context = context_maker.get_admin_context() + url = '/v2/fake4/os-quota-sets/1234?user_id=1' + self.req = fakes.HTTPRequest.blank(url) + self.req.environ['nova.context'] = context + self.mox.StubOutWithMock(quota.QUOTAS, + "destroy_all_by_project_and_user") + quota.QUOTAS.destroy_all_by_project_and_user(context, 1234, '1') + self.mox.ReplayAll() + res = self.controller.delete(self.req, 1234) + self.mox.VerifyAll() + self.assertEqual(202, self.get_delete_status_int(res)) + + +class QuotaSetsTestV2(QuotaSetsTestV21): + plugin = quotas_v2 + validation_error = webob.exc.HTTPBadRequest + + def _setup_controller(self): + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.ext_mgr.is_loaded('os-server-group-quotas').MultipleTimes().\ + AndReturn(self.include_server_group_quotas) + self.mox.ReplayAll() + self.controller = self.plugin.QuotaSetsController(self.ext_mgr) + self.mox.ResetAll() + + # NOTE: The following tests are tricky and v2.1 API does not allow + # this kind of input by strong input validation. Just for test coverage, + # we keep them now. + def test_quotas_update_invalid_value_json_fromat_empty_string(self): + self.setup_mock_for_update() + self.default_quotas.update({ + 'instances': 50, + 'cores': 50 + }) + expected_resp = {'quota_set': self.default_quotas} + + # when PUT JSON format with empty string for quota + body = copy.deepcopy(expected_resp) + body['quota_set']['ram'] = '' + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me', + use_admin_context=True) + res_dict = self.controller.update(req, 'update_me', body) + self.assertEqual(res_dict, expected_resp) + + def test_quotas_update_invalid_value_xml_fromat_empty_string(self): + self.default_quotas.update({ + 'instances': 50, + 'cores': 50 + }) + expected_resp = {'quota_set': self.default_quotas} + + # when PUT XML format with empty string for quota + body = copy.deepcopy(expected_resp) + body['quota_set']['ram'] = {} + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me', + use_admin_context=True) + self.setup_mock_for_update() + res_dict = self.controller.update(req, 'update_me', body) + self.assertEqual(res_dict, expected_resp) + + # NOTE: os-extended-quotas and os-user-quotas are only for v2.0. + # On v2.1, these features are always enable. So we need the following + # tests only for v2.0. + def test_delete_quotas_when_extension_not_loaded(self): + self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(False) + self.mox.ReplayAll() + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, 1234) + + def test_delete_user_quotas_when_extension_not_loaded(self): + self.ext_mgr.is_loaded('os-extended-quotas').AndReturn(True) + self.ext_mgr.is_loaded('os-user-quotas').AndReturn(False) + self.mox.ReplayAll() + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/1234?user_id=1') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, 1234) + + +class QuotaSetsTestV2WithoutServerGroupQuotas(QuotaSetsTestV2): + include_server_group_quotas = False + + # NOTE: os-server-group-quotas is only for v2.0. On v2.1 this feature + # is always enabled, so this test is only needed for v2.0 + def test_quotas_update_without_server_group_quotas_extenstion(self): + self.setup_mock_for_update() + self.default_quotas.update({ + 'server_groups': 50, + 'sever_group_members': 50 + }) + body = {'quota_set': self.default_quotas} + + req = fakes.HTTPRequest.blank('/v2/fake4/os-quota-sets/update_me', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 'update_me', body=body) + + +class ExtendedQuotasTestV2(ExtendedQuotasTestV21): + plugin = quotas_v2 + + def _setup_controller(self): + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.ext_mgr.is_loaded('os-server-group-quotas').MultipleTimes().\ + AndReturn(False) + self.mox.ReplayAll() + self.controller = self.plugin.QuotaSetsController(self.ext_mgr) + self.mox.ResetAll() + + +class UserQuotasTestV2(UserQuotasTestV21): + plugin = quotas_v2 + + def _setup_controller(self): + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.ext_mgr.is_loaded('os-server-group-quotas').MultipleTimes().\ + AndReturn(self.include_server_group_quotas) + self.mox.ReplayAll() + self.controller = self.plugin.QuotaSetsController(self.ext_mgr) + self.mox.ResetAll() + + +class UserQuotasTestV2WithoutServerGroupQuotas(UserQuotasTestV2): + include_server_group_quotas = False + + # NOTE: os-server-group-quotas is only for v2.0. On v2.1 this feature + # is always enabled, so this test is only needed for v2.0 + def test_user_quotas_update_as_admin_without_sg_quota_extension(self): + self.setup_mock_for_update() + body = {'quota_set': {'instances': 10, 'cores': 20, + 'ram': 51200, 'floating_ips': 10, + 'fixed_ips': -1, 'metadata_items': 128, + 'injected_files': 5, + 'injected_file_content_bytes': 10240, + 'injected_file_path_bytes': 255, + 'security_groups': 10, + 'security_group_rules': 20, + 'key_pairs': 100, + 'server_groups': 100, + 'server_group_members': 200}} + + url = '/v2/fake4/os-quota-sets/update_me?user_id=1' + req = fakes.HTTPRequest.blank(url, use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 'update_me', body=body) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_rescue.py b/nova/tests/unit/api/openstack/compute/contrib/test_rescue.py new file mode 100644 index 0000000000..f8de7de291 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_rescue.py @@ -0,0 +1,270 @@ +# Copyright 2011 OpenStack Foundation +# +# 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 oslo.serialization import jsonutils +import webob + +from nova import compute +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + +CONF = cfg.CONF +CONF.import_opt('password_length', 'nova.utils') + + +def rescue(self, context, instance, rescue_password=None, + rescue_image_ref=None): + pass + + +def unrescue(self, context, instance): + pass + + +def fake_compute_get(*args, **kwargs): + uuid = '70f6db34-de8d-4fbd-aafb-4065bdfa6114' + return {'id': 1, 'uuid': uuid} + + +class RescueTestV21(test.NoDBTestCase): + _prefix = '/v2/fake' + + def setUp(self): + super(RescueTestV21, self).setUp() + + self.stubs.Set(compute.api.API, "get", fake_compute_get) + self.stubs.Set(compute.api.API, "rescue", rescue) + self.stubs.Set(compute.api.API, "unrescue", unrescue) + self.app = self._get_app() + + def _get_app(self): + return fakes.wsgi_app_v21(init_only=('servers', 'os-rescue')) + + def test_rescue_from_locked_server(self): + def fake_rescue_from_locked_server(self, context, + instance, rescue_password=None, rescue_image_ref=None): + raise exception.InstanceIsLocked(instance_uuid=instance['uuid']) + + self.stubs.Set(compute.api.API, + 'rescue', + fake_rescue_from_locked_server) + body = {"rescue": {"adminPass": "AABBCC112233"}} + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 409) + + def test_rescue_with_preset_password(self): + body = {"rescue": {"adminPass": "AABBCC112233"}} + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 200) + resp_json = jsonutils.loads(resp.body) + self.assertEqual("AABBCC112233", resp_json['adminPass']) + + def test_rescue_generates_password(self): + body = dict(rescue=None) + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 200) + resp_json = jsonutils.loads(resp.body) + self.assertEqual(CONF.password_length, len(resp_json['adminPass'])) + + def test_rescue_of_rescued_instance(self): + body = dict(rescue=None) + + def fake_rescue(*args, **kwargs): + raise exception.InstanceInvalidState('fake message') + + self.stubs.Set(compute.api.API, "rescue", fake_rescue) + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 409) + + def test_unrescue(self): + body = dict(unrescue=None) + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 202) + + def test_unrescue_from_locked_server(self): + def fake_unrescue_from_locked_server(self, context, + instance): + raise exception.InstanceIsLocked(instance_uuid=instance['uuid']) + + self.stubs.Set(compute.api.API, + 'unrescue', + fake_unrescue_from_locked_server) + + body = dict(unrescue=None) + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 409) + + def test_unrescue_of_active_instance(self): + body = dict(unrescue=None) + + def fake_unrescue(*args, **kwargs): + raise exception.InstanceInvalidState('fake message') + + self.stubs.Set(compute.api.API, "unrescue", fake_unrescue) + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 409) + + def test_rescue_raises_unrescuable(self): + body = dict(rescue=None) + + def fake_rescue(*args, **kwargs): + raise exception.InstanceNotRescuable('fake message') + + self.stubs.Set(compute.api.API, "rescue", fake_rescue) + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 400) + + @mock.patch('nova.compute.api.API.rescue') + def test_rescue_with_image_specified(self, mock_compute_api_rescue): + instance = fake_compute_get() + body = {"rescue": {"adminPass": "ABC123", + "rescue_image_ref": "img-id"}} + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 200) + resp_json = jsonutils.loads(resp.body) + self.assertEqual("ABC123", resp_json['adminPass']) + + mock_compute_api_rescue.assert_called_with(mock.ANY, instance, + rescue_password=u'ABC123', + rescue_image_ref=u'img-id') + + @mock.patch('nova.compute.api.API.rescue') + def test_rescue_without_image_specified(self, mock_compute_api_rescue): + instance = fake_compute_get() + body = {"rescue": {"adminPass": "ABC123"}} + + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 200) + resp_json = jsonutils.loads(resp.body) + self.assertEqual("ABC123", resp_json['adminPass']) + + mock_compute_api_rescue.assert_called_with(mock.ANY, instance, + rescue_password=u'ABC123', + rescue_image_ref=None) + + def test_rescue_with_none(self): + body = dict(rescue=None) + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(200, resp.status_int) + + def test_rescue_with_empty_dict(self): + body = dict(rescue=dict()) + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(200, resp.status_int) + + def test_rescue_disable_password(self): + self.flags(enable_instance_password=False) + body = dict(rescue=None) + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(200, resp.status_int) + resp_json = jsonutils.loads(resp.body) + self.assertNotIn('adminPass', resp_json) + + def test_rescue_with_invalid_property(self): + body = {"rescue": {"test": "test"}} + req = webob.Request.blank(self._prefix + '/servers/test_inst/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(self.app) + self.assertEqual(400, resp.status_int) + + +class RescueTestV20(RescueTestV21): + + def _get_app(self): + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=None) + return fakes.wsgi_app(init_only=('servers',)) + + def test_rescue_with_invalid_property(self): + # NOTE(cyeoh): input validation in original v2 code does not + # check for invalid properties. + pass + + def test_rescue_disable_password(self): + # NOTE(cyeoh): Original v2.0 code does not support disabling + # the admin password being returned through a conf setting + pass diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_scheduler_hints.py b/nova/tests/unit/api/openstack/compute/contrib/test_scheduler_hints.py new file mode 100644 index 0000000000..fba3a02eec --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_scheduler_hints.py @@ -0,0 +1,220 @@ +# Copyright 2011 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 datetime + +from oslo.config import cfg +from oslo.serialization import jsonutils + +from nova.api.openstack import compute +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import servers as servers_v21 +from nova.api.openstack.compute import servers as servers_v2 +from nova.api.openstack import extensions +import nova.compute.api +from nova.compute import flavors +from nova import db +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance +from nova.tests.unit.image import fake + + +UUID = fakes.FAKE_UUID + + +CONF = cfg.CONF + + +class SchedulerHintsTestCaseV21(test.TestCase): + + def setUp(self): + super(SchedulerHintsTestCaseV21, self).setUp() + self.fake_instance = fakes.stub_instance(1, uuid=UUID) + self._set_up_router() + + def _set_up_router(self): + self.app = compute.APIRouterV3(init_only=('servers', + 'os-scheduler-hints')) + + def _get_request(self): + return fakes.HTTPRequestV3.blank('/servers') + + def test_create_server_without_hints(self): + + def fake_create(*args, **kwargs): + self.assertEqual(kwargs['scheduler_hints'], {}) + return ([self.fake_instance], '') + + self.stubs.Set(nova.compute.api.API, 'create', fake_create) + + req = self._get_request() + req.method = 'POST' + req.content_type = 'application/json' + body = {'server': { + 'name': 'server_test', + 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175', + 'flavorRef': '1', + }} + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(202, res.status_int) + + def test_create_server_with_hints(self): + + def fake_create(*args, **kwargs): + self.assertEqual(kwargs['scheduler_hints'], {'a': 'b'}) + return ([self.fake_instance], '') + + self.stubs.Set(nova.compute.api.API, 'create', fake_create) + + req = self._get_request() + req.method = 'POST' + req.content_type = 'application/json' + body = { + 'server': { + 'name': 'server_test', + 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175', + 'flavorRef': '1', + }, + 'os:scheduler_hints': {'a': 'b'}, + } + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(202, res.status_int) + + def test_create_server_bad_hints(self): + req = self._get_request() + req.method = 'POST' + req.content_type = 'application/json' + body = { + 'server': { + 'name': 'server_test', + 'imageRef': 'cedef40a-ed67-4d10-800e-17455edce175', + 'flavorRef': '1', + }, + 'os:scheduler_hints': 'here', + } + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(400, res.status_int) + + +class SchedulerHintsTestCaseV2(SchedulerHintsTestCaseV21): + + def _set_up_router(self): + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Scheduler_hints']) + self.app = compute.APIRouter(init_only=('servers',)) + + def _get_request(self): + return fakes.HTTPRequest.blank('/fake/servers') + + +class ServersControllerCreateTestV21(test.TestCase): + + def setUp(self): + """Shared implementation for tests below that create instance.""" + super(ServersControllerCreateTestV21, self).setUp() + + self.instance_cache_num = 0 + self._set_up_controller() + + def instance_create(context, inst): + inst_type = flavors.get_flavor_by_flavor_id(3) + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + def_image_ref = 'http://localhost/images/%s' % image_uuid + self.instance_cache_num += 1 + instance = fake_instance.fake_db_instance(**{ + 'id': self.instance_cache_num, + 'display_name': inst['display_name'] or 'test', + 'uuid': fakes.FAKE_UUID, + 'instance_type': inst_type, + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': 'fead::1234', + 'image_ref': inst.get('image_ref', def_image_ref), + 'user_id': 'fake', + 'project_id': 'fake', + 'reservation_id': inst['reservation_id'], + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "progress": 0, + "fixed_ips": [], + "task_state": "", + "vm_state": "", + "root_device_name": inst.get('root_device_name', 'vda'), + }) + + return instance + + fake.stub_out_image_service(self.stubs) + self.stubs.Set(db, 'instance_create', instance_create) + + def _set_up_controller(self): + ext_info = plugins.LoadedExtensionInfo() + CONF.set_override('extensions_blacklist', 'os-scheduler-hints', + 'osapi_v3') + self.no_scheduler_hints_controller = servers_v21.ServersController( + extension_info=ext_info) + + def _verify_availability_zone(self, **kwargs): + self.assertNotIn('scheduler_hints', kwargs) + + def _get_request(self): + return fakes.HTTPRequestV3.blank('/servers') + + def _test_create_extra(self, params): + image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + server = dict(name='server_test', imageRef=image_uuid, flavorRef=2) + body = dict(server=server) + body.update(params) + req = self._get_request() + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + server = self.no_scheduler_hints_controller.create( + req, body=body).obj['server'] + + def test_create_instance_with_scheduler_hints_disabled(self): + hints = {'same_host': '48e6a9f6-30af-47e0-bc04-acaed113bb4e'} + params = {'OS-SCH-HNT:scheduler_hints': hints} + old_create = nova.compute.api.API.create + + def create(*args, **kwargs): + self._verify_availability_zone(**kwargs) + return old_create(*args, **kwargs) + + self.stubs.Set(nova.compute.api.API, 'create', create) + self._test_create_extra(params) + + +class ServersControllerCreateTestV2(ServersControllerCreateTestV21): + + def _set_up_controller(self): + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.no_scheduler_hints_controller = servers_v2.Controller( + self.ext_mgr) + + def _verify_availability_zone(self, **kwargs): + self.assertEqual(kwargs['scheduler_hints'], {}) + + def _get_request(self): + return fakes.HTTPRequest.blank('/fake/servers') diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_security_group_default_rules.py b/nova/tests/unit/api/openstack/compute/contrib/test_security_group_default_rules.py new file mode 100644 index 0000000000..a735f4722e --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_security_group_default_rules.py @@ -0,0 +1,515 @@ +# Copyright 2013 Metacloud, 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. + +from lxml import etree +from oslo.config import cfg +import webob + +from nova.api.openstack.compute.contrib import \ + security_group_default_rules as security_group_default_rules_v2 +from nova.api.openstack.compute.plugins.v3 import \ + security_group_default_rules as security_group_default_rules_v21 +from nova.api.openstack import wsgi +from nova import context +import nova.db +from nova import test +from nova.tests.unit.api.openstack import fakes + + +CONF = cfg.CONF + + +class AttrDict(dict): + def __getattr__(self, k): + return self[k] + + +def security_group_default_rule_template(**kwargs): + rule = kwargs.copy() + rule.setdefault('ip_protocol', 'TCP') + rule.setdefault('from_port', 22) + rule.setdefault('to_port', 22) + rule.setdefault('cidr', '10.10.10.0/24') + return rule + + +def security_group_default_rule_db(security_group_default_rule, id=None): + attrs = security_group_default_rule.copy() + if id is not None: + attrs['id'] = id + return AttrDict(attrs) + + +class TestSecurityGroupDefaultRulesNeutronV21(test.TestCase): + controller_cls = (security_group_default_rules_v21. + SecurityGroupDefaultRulesController) + + def setUp(self): + self.flags(security_group_api='neutron') + super(TestSecurityGroupDefaultRulesNeutronV21, self).setUp() + self.controller = self.controller_cls() + + def test_create_security_group_default_rule_not_implemented_neutron(self): + sgr = security_group_default_rule_template() + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPNotImplemented, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_security_group_default_rules_list_not_implemented_neturon(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPNotImplemented, self.controller.index, + req) + + def test_security_group_default_rules_show_not_implemented_neturon(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPNotImplemented, self.controller.show, + req, '602ed77c-a076-4f9b-a617-f93b847b62c5') + + def test_security_group_default_rules_delete_not_implemented_neturon(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPNotImplemented, self.controller.delete, + req, '602ed77c-a076-4f9b-a617-f93b847b62c5') + + +class TestSecurityGroupDefaultRulesNeutronV2(test.TestCase): + controller_cls = (security_group_default_rules_v2. + SecurityGroupDefaultRulesController) + + +class TestSecurityGroupDefaultRulesV21(test.TestCase): + controller_cls = (security_group_default_rules_v21. + SecurityGroupDefaultRulesController) + + def setUp(self): + super(TestSecurityGroupDefaultRulesV21, self).setUp() + self.controller = self.controller_cls() + + def test_create_security_group_default_rule(self): + sgr = security_group_default_rule_template() + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + sgr_dict = dict(security_group_default_rule=sgr) + res_dict = self.controller.create(req, sgr_dict) + security_group_default_rule = res_dict['security_group_default_rule'] + self.assertEqual(security_group_default_rule['ip_protocol'], + sgr['ip_protocol']) + self.assertEqual(security_group_default_rule['from_port'], + sgr['from_port']) + self.assertEqual(security_group_default_rule['to_port'], + sgr['to_port']) + self.assertEqual(security_group_default_rule['ip_range']['cidr'], + sgr['cidr']) + + def test_create_security_group_default_rule_with_no_to_port(self): + sgr = security_group_default_rule_template() + del sgr['to_port'] + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_create_security_group_default_rule_with_no_from_port(self): + sgr = security_group_default_rule_template() + del sgr['from_port'] + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_create_security_group_default_rule_with_no_ip_protocol(self): + sgr = security_group_default_rule_template() + del sgr['ip_protocol'] + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_create_security_group_default_rule_with_no_cidr(self): + sgr = security_group_default_rule_template() + del sgr['cidr'] + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + res_dict = self.controller.create(req, + {'security_group_default_rule': sgr}) + security_group_default_rule = res_dict['security_group_default_rule'] + self.assertNotEqual(security_group_default_rule['id'], 0) + self.assertEqual(security_group_default_rule['ip_range']['cidr'], + '0.0.0.0/0') + + def test_create_security_group_default_rule_with_blank_to_port(self): + sgr = security_group_default_rule_template(to_port='') + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_create_security_group_default_rule_with_blank_from_port(self): + sgr = security_group_default_rule_template(from_port='') + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_create_security_group_default_rule_with_blank_ip_protocol(self): + sgr = security_group_default_rule_template(ip_protocol='') + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_create_security_group_default_rule_with_blank_cidr(self): + sgr = security_group_default_rule_template(cidr='') + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + res_dict = self.controller.create(req, + {'security_group_default_rule': sgr}) + security_group_default_rule = res_dict['security_group_default_rule'] + self.assertNotEqual(security_group_default_rule['id'], 0) + self.assertEqual(security_group_default_rule['ip_range']['cidr'], + '0.0.0.0/0') + + def test_create_security_group_default_rule_non_numerical_to_port(self): + sgr = security_group_default_rule_template(to_port='invalid') + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_create_security_group_default_rule_non_numerical_from_port(self): + sgr = security_group_default_rule_template(from_port='invalid') + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_create_security_group_default_rule_invalid_ip_protocol(self): + sgr = security_group_default_rule_template(ip_protocol='invalid') + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_create_security_group_default_rule_invalid_cidr(self): + sgr = security_group_default_rule_template(cidr='10.10.2222.0/24') + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_create_security_group_default_rule_invalid_to_port(self): + sgr = security_group_default_rule_template(to_port='666666') + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_create_security_group_default_rule_invalid_from_port(self): + sgr = security_group_default_rule_template(from_port='666666') + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_create_security_group_default_rule_with_no_body(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, None) + + def test_create_duplicate_security_group_default_rule(self): + sgr = security_group_default_rule_template() + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.controller.create(req, {'security_group_default_rule': sgr}) + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_default_rule': sgr}) + + def test_security_group_default_rules_list(self): + self.test_create_security_group_default_rule() + rules = [dict(id=1, + ip_protocol='TCP', + from_port=22, + to_port=22, + ip_range=dict(cidr='10.10.10.0/24'))] + expected = {'security_group_default_rules': rules} + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + res_dict = self.controller.index(req) + self.assertEqual(res_dict, expected) + + def test_default_security_group_default_rule_show(self): + sgr = security_group_default_rule_template(id=1) + + self.test_create_security_group_default_rule() + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + res_dict = self.controller.show(req, '1') + + security_group_default_rule = res_dict['security_group_default_rule'] + + self.assertEqual(security_group_default_rule['ip_protocol'], + sgr['ip_protocol']) + self.assertEqual(security_group_default_rule['to_port'], + sgr['to_port']) + self.assertEqual(security_group_default_rule['from_port'], + sgr['from_port']) + self.assertEqual(security_group_default_rule['ip_range']['cidr'], + sgr['cidr']) + + def test_delete_security_group_default_rule(self): + sgr = security_group_default_rule_template(id=1) + + self.test_create_security_group_default_rule() + + self.called = False + + def security_group_default_rule_destroy(context, id): + self.called = True + + def return_security_group_default_rule(context, id): + self.assertEqual(sgr['id'], id) + return security_group_default_rule_db(sgr) + + self.stubs.Set(nova.db, 'security_group_default_rule_destroy', + security_group_default_rule_destroy) + self.stubs.Set(nova.db, 'security_group_default_rule_get', + return_security_group_default_rule) + + req = fakes.HTTPRequest.blank( + '/v2/fake/os-security-group-default-rules', use_admin_context=True) + self.controller.delete(req, '1') + + self.assertTrue(self.called) + + def test_security_group_ensure_default(self): + sgr = security_group_default_rule_template(id=1) + self.test_create_security_group_default_rule() + + ctxt = context.get_admin_context() + + setattr(ctxt, 'project_id', 'new_project_id') + + sg = nova.db.security_group_ensure_default(ctxt) + rules = nova.db.security_group_rule_get_by_security_group(ctxt, sg.id) + security_group_rule = rules[0] + self.assertEqual(sgr['id'], security_group_rule.id) + self.assertEqual(sgr['ip_protocol'], security_group_rule.protocol) + self.assertEqual(sgr['from_port'], security_group_rule.from_port) + self.assertEqual(sgr['to_port'], security_group_rule.to_port) + self.assertEqual(sgr['cidr'], security_group_rule.cidr) + + +class TestSecurityGroupDefaultRulesV2(test.TestCase): + controller_cls = (security_group_default_rules_v2. + SecurityGroupDefaultRulesController) + + +class TestSecurityGroupDefaultRulesXMLDeserializer(test.TestCase): + def setUp(self): + super(TestSecurityGroupDefaultRulesXMLDeserializer, self).setUp() + deserializer = security_group_default_rules_v2.\ + SecurityGroupDefaultRulesXMLDeserializer() + self.deserializer = deserializer + + def test_create_request(self): + serial_request = """ +<security_group_default_rule> + <from_port>22</from_port> + <to_port>22</to_port> + <ip_protocol>TCP</ip_protocol> + <cidr>10.10.10.0/24</cidr> +</security_group_default_rule>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group_default_rule": { + "from_port": "22", + "to_port": "22", + "ip_protocol": "TCP", + "cidr": "10.10.10.0/24" + }, + } + self.assertEqual(request['body'], expected) + + def test_create_no_to_port_request(self): + serial_request = """ +<security_group_default_rule> + <from_port>22</from_port> + <ip_protocol>TCP</ip_protocol> + <cidr>10.10.10.0/24</cidr> +</security_group_default_rule>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group_default_rule": { + "from_port": "22", + "ip_protocol": "TCP", + "cidr": "10.10.10.0/24" + }, + } + self.assertEqual(request['body'], expected) + + def test_create_no_from_port_request(self): + serial_request = """ +<security_group_default_rule> + <to_port>22</to_port> + <ip_protocol>TCP</ip_protocol> + <cidr>10.10.10.0/24</cidr> +</security_group_default_rule>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group_default_rule": { + "to_port": "22", + "ip_protocol": "TCP", + "cidr": "10.10.10.0/24" + }, + } + self.assertEqual(request['body'], expected) + + def test_create_no_ip_protocol_request(self): + serial_request = """ +<security_group_default_rule> + <from_port>22</from_port> + <to_port>22</to_port> + <cidr>10.10.10.0/24</cidr> +</security_group_default_rule>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group_default_rule": { + "from_port": "22", + "to_port": "22", + "cidr": "10.10.10.0/24" + }, + } + self.assertEqual(request['body'], expected) + + def test_create_no_cidr_request(self): + serial_request = """ +<security_group_default_rule> + <from_port>22</from_port> + <to_port>22</to_port> + <ip_protocol>TCP</ip_protocol> +</security_group_default_rule>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group_default_rule": { + "from_port": "22", + "to_port": "22", + "ip_protocol": "TCP", + }, + } + self.assertEqual(request['body'], expected) + + +class TestSecurityGroupDefaultRuleXMLSerializer(test.TestCase): + def setUp(self): + super(TestSecurityGroupDefaultRuleXMLSerializer, self).setUp() + self.namespace = wsgi.XMLNS_V11 + self.rule_serializer =\ + security_group_default_rules_v2.SecurityGroupDefaultRuleTemplate() + self.index_serializer =\ + security_group_default_rules_v2.SecurityGroupDefaultRulesTemplate() + + def _tag(self, elem): + tagname = elem.tag + self.assertEqual(tagname[0], '{') + tmp = tagname.partition('}') + namespace = tmp[0][1:] + self.assertEqual(namespace, self.namespace) + return tmp[2] + + def _verify_security_group_default_rule(self, raw_rule, tree): + self.assertEqual(raw_rule['id'], tree.get('id')) + + seen = set() + expected = set(['ip_protocol', 'from_port', 'to_port', 'ip_range', + 'ip_range/cidr']) + + for child in tree: + child_tag = self._tag(child) + seen.add(child_tag) + if child_tag == 'ip_range': + for gr_child in child: + gr_child_tag = self._tag(gr_child) + self.assertIn(gr_child_tag, raw_rule[child_tag]) + seen.add('%s/%s' % (child_tag, gr_child_tag)) + self.assertEqual(gr_child.text, + raw_rule[child_tag][gr_child_tag]) + else: + self.assertEqual(child.text, raw_rule[child_tag]) + self.assertEqual(seen, expected) + + def test_rule_serializer(self): + raw_rule = dict(id='123', + ip_protocol='TCP', + from_port='22', + to_port='22', + ip_range=dict(cidr='10.10.10.0/24')) + rule = dict(security_group_default_rule=raw_rule) + text = self.rule_serializer.serialize(rule) + + tree = etree.fromstring(text) + + self.assertEqual('security_group_default_rule', self._tag(tree)) + self._verify_security_group_default_rule(raw_rule, tree) + + def test_index_serializer(self): + rules = [dict(id='123', + ip_protocol='TCP', + from_port='22', + to_port='22', + ip_range=dict(cidr='10.10.10.0/24')), + dict(id='234', + ip_protocol='UDP', + from_port='23456', + to_port='234567', + ip_range=dict(cidr='10.12.0.0/18')), + dict(id='345', + ip_protocol='tcp', + from_port='3456', + to_port='4567', + ip_range=dict(cidr='192.168.1.0/32'))] + + rules_dict = dict(security_group_default_rules=rules) + + text = self.index_serializer.serialize(rules_dict) + + tree = etree.fromstring(text) + self.assertEqual('security_group_default_rules', self._tag(tree)) + self.assertEqual(len(rules), len(tree)) + for idx, child in enumerate(tree): + self._verify_security_group_default_rule(rules[idx], child) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_security_groups.py b/nova/tests/unit/api/openstack/compute/contrib/test_security_groups.py new file mode 100644 index 0000000000..d1620b6a28 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_security_groups.py @@ -0,0 +1,1767 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2012 Justin Santa Barbara +# +# 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. + +from lxml import etree +import mock +import mox +from oslo.config import cfg +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import security_groups as secgroups_v2 +from nova.api.openstack.compute.plugins.v3 import security_groups as \ + secgroups_v21 +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova.compute import power_state +from nova import context as context_maker +import nova.db +from nova import exception +from nova import objects +from nova.objects import instance as instance_obj +from nova import quota +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance +from nova.tests.unit import utils + +CONF = cfg.CONF +FAKE_UUID1 = 'a47ae74e-ab08-447f-8eee-ffd43fc46c16' +FAKE_UUID2 = 'c6e6430a-6563-4efa-9542-5e93c9e97d18' + + +class AttrDict(dict): + def __getattr__(self, k): + return self[k] + + +def security_group_template(**kwargs): + sg = kwargs.copy() + sg.setdefault('tenant_id', '123') + sg.setdefault('name', 'test') + sg.setdefault('description', 'test-description') + return sg + + +def security_group_db(security_group, id=None): + attrs = security_group.copy() + if 'tenant_id' in attrs: + attrs['project_id'] = attrs.pop('tenant_id') + if id is not None: + attrs['id'] = id + attrs.setdefault('rules', []) + attrs.setdefault('instances', []) + return AttrDict(attrs) + + +def security_group_rule_template(**kwargs): + rule = kwargs.copy() + rule.setdefault('ip_protocol', 'tcp') + rule.setdefault('from_port', 22) + rule.setdefault('to_port', 22) + rule.setdefault('parent_group_id', 2) + return rule + + +def security_group_rule_db(rule, id=None): + attrs = rule.copy() + if 'ip_protocol' in attrs: + attrs['protocol'] = attrs.pop('ip_protocol') + return AttrDict(attrs) + + +def return_server(context, server_id, + columns_to_join=None, use_slave=False): + return fake_instance.fake_db_instance( + **{'id': int(server_id), + 'power_state': 0x01, + 'host': "localhost", + 'uuid': FAKE_UUID1, + 'name': 'asdf'}) + + +def return_server_by_uuid(context, server_uuid, + columns_to_join=None, + use_slave=False): + return fake_instance.fake_db_instance( + **{'id': 1, + 'power_state': 0x01, + 'host': "localhost", + 'uuid': server_uuid, + 'name': 'asdf'}) + + +def return_non_running_server(context, server_id, columns_to_join=None): + return fake_instance.fake_db_instance( + **{'id': server_id, 'power_state': power_state.SHUTDOWN, + 'uuid': FAKE_UUID1, 'host': "localhost", 'name': 'asdf'}) + + +def return_security_group_by_name(context, project_id, group_name): + return {'id': 1, 'name': group_name, + "instances": [{'id': 1, 'uuid': FAKE_UUID1}]} + + +def return_security_group_without_instances(context, project_id, group_name): + return {'id': 1, 'name': group_name} + + +def return_server_nonexistent(context, server_id, columns_to_join=None): + raise exception.InstanceNotFound(instance_id=server_id) + + +class TestSecurityGroupsV21(test.TestCase): + secgrp_ctl_cls = secgroups_v21.SecurityGroupController + server_secgrp_ctl_cls = secgroups_v21.ServerSecurityGroupController + secgrp_act_ctl_cls = secgroups_v21.SecurityGroupActionController + + def setUp(self): + super(TestSecurityGroupsV21, self).setUp() + + self.controller = self.secgrp_ctl_cls() + self.server_controller = self.server_secgrp_ctl_cls() + self.manager = self.secgrp_act_ctl_cls() + + # This needs to be done here to set fake_id because the derived + # class needs to be called first if it wants to set + # 'security_group_api' and this setUp method needs to be called. + if self.controller.security_group_api.id_is_uuid: + self.fake_id = '11111111-1111-1111-1111-111111111111' + else: + self.fake_id = '11111111' + + def _assert_no_security_groups_reserved(self, context): + """Check that no reservations are leaked during tests.""" + result = quota.QUOTAS.get_project_quotas(context, context.project_id) + self.assertEqual(result['security_groups']['reserved'], 0) + + def _assert_security_groups_in_use(self, project_id, user_id, in_use): + context = context_maker.get_admin_context() + result = quota.QUOTAS.get_user_quotas(context, project_id, user_id) + self.assertEqual(result['security_groups']['in_use'], in_use) + + def test_create_security_group(self): + sg = security_group_template() + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + res_dict = self.controller.create(req, {'security_group': sg}) + self.assertEqual(res_dict['security_group']['name'], 'test') + self.assertEqual(res_dict['security_group']['description'], + 'test-description') + + def test_create_security_group_with_no_name(self): + sg = security_group_template() + del sg['name'] + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, sg) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_with_no_description(self): + sg = security_group_template() + del sg['description'] + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group': sg}) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_with_empty_description(self): + sg = security_group_template() + sg['description'] = "" + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + try: + self.controller.create(req, {'security_group': sg}) + self.fail('Should have raised BadRequest exception') + except webob.exc.HTTPBadRequest as exc: + self.assertEqual('description has a minimum character requirement' + ' of 1.', exc.explanation) + except exception.InvalidInput as exc: + self.fail('Should have raised BadRequest exception instead of') + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_with_blank_name(self): + sg = security_group_template(name='') + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group': sg}) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_with_whitespace_name(self): + sg = security_group_template(name=' ') + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group': sg}) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_with_blank_description(self): + sg = security_group_template(description='') + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group': sg}) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_with_whitespace_description(self): + sg = security_group_template(description=' ') + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group': sg}) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_with_duplicate_name(self): + sg = security_group_template() + + # FIXME: Stub out _get instead of creating twice + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.controller.create(req, {'security_group': sg}) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group': sg}) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_with_no_body(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, None) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_with_no_security_group(self): + body = {'no-securityGroup': None} + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_above_255_characters_name(self): + sg = security_group_template(name='1234567890' * 26) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group': sg}) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_above_255_characters_description(self): + sg = security_group_template(description='1234567890' * 26) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group': sg}) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_non_string_name(self): + sg = security_group_template(name=12) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group': sg}) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_non_string_description(self): + sg = security_group_template(description=12) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group': sg}) + + self._assert_no_security_groups_reserved(req.environ['nova.context']) + + def test_create_security_group_quota_limit(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + for num in range(1, CONF.quota_security_groups): + name = 'test%s' % num + sg = security_group_template(name=name) + res_dict = self.controller.create(req, {'security_group': sg}) + self.assertEqual(res_dict['security_group']['name'], name) + + sg = security_group_template() + self.assertRaises(webob.exc.HTTPForbidden, self.controller.create, + req, {'security_group': sg}) + + def test_get_security_group_list(self): + groups = [] + for i, name in enumerate(['default', 'test']): + sg = security_group_template(id=i + 1, + name=name, + description=name + '-desc', + rules=[]) + groups.append(sg) + expected = {'security_groups': groups} + + def return_security_groups(context, project_id): + return [security_group_db(sg) for sg in groups] + + self.stubs.Set(nova.db, 'security_group_get_by_project', + return_security_groups) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + res_dict = self.controller.index(req) + + self.assertEqual(res_dict, expected) + + def test_get_security_group_list_missing_group_id_rule(self): + groups = [] + rule1 = security_group_rule_template(cidr='10.2.3.124/24', + parent_group_id=1, + group_id={}, id=88, + protocol='TCP') + rule2 = security_group_rule_template(cidr='10.2.3.125/24', + parent_group_id=1, + id=99, protocol=88, + group_id='HAS_BEEN_DELETED') + sg = security_group_template(id=1, + name='test', + description='test-desc', + rules=[rule1, rule2]) + + groups.append(sg) + # An expected rule here needs to be created as the api returns + # different attributes on the rule for a response than what was + # passed in. For example: + # "cidr": "0.0.0.0/0" ->"ip_range": {"cidr": "0.0.0.0/0"} + expected_rule = security_group_rule_template( + ip_range={'cidr': '10.2.3.124/24'}, parent_group_id=1, + group={}, id=88, ip_protocol='TCP') + expected = security_group_template(id=1, + name='test', + description='test-desc', + rules=[expected_rule]) + + expected = {'security_groups': [expected]} + + def return_security_groups(context, project, search_opts): + return [security_group_db(sg) for sg in groups] + + self.stubs.Set(self.controller.security_group_api, 'list', + return_security_groups) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + res_dict = self.controller.index(req) + + self.assertEqual(res_dict, expected) + + def test_get_security_group_list_all_tenants(self): + all_groups = [] + tenant_groups = [] + + for i, name in enumerate(['default', 'test']): + sg = security_group_template(id=i + 1, + name=name, + description=name + '-desc', + rules=[]) + all_groups.append(sg) + if name == 'default': + tenant_groups.append(sg) + + all = {'security_groups': all_groups} + tenant_specific = {'security_groups': tenant_groups} + + def return_all_security_groups(context): + return [security_group_db(sg) for sg in all_groups] + + self.stubs.Set(nova.db, 'security_group_get_all', + return_all_security_groups) + + def return_tenant_security_groups(context, project_id): + return [security_group_db(sg) for sg in tenant_groups] + + self.stubs.Set(nova.db, 'security_group_get_by_project', + return_tenant_security_groups) + + path = '/v2/fake/os-security-groups' + + req = fakes.HTTPRequest.blank(path, use_admin_context=True) + res_dict = self.controller.index(req) + self.assertEqual(res_dict, tenant_specific) + + req = fakes.HTTPRequest.blank('%s?all_tenants=1' % path, + use_admin_context=True) + res_dict = self.controller.index(req) + self.assertEqual(res_dict, all) + + def test_get_security_group_by_instance(self): + groups = [] + for i, name in enumerate(['default', 'test']): + sg = security_group_template(id=i + 1, + name=name, + description=name + '-desc', + rules=[]) + groups.append(sg) + expected = {'security_groups': groups} + + def return_instance(context, server_id, + columns_to_join=None, use_slave=False): + self.assertEqual(server_id, FAKE_UUID1) + return return_server_by_uuid(context, server_id) + + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_instance) + + def return_security_groups(context, instance_uuid): + self.assertEqual(instance_uuid, FAKE_UUID1) + return [security_group_db(sg) for sg in groups] + + self.stubs.Set(nova.db, 'security_group_get_by_instance', + return_security_groups) + + req = fakes.HTTPRequest.blank('/v2/%s/servers/%s/os-security-groups' % + ('fake', FAKE_UUID1)) + res_dict = self.server_controller.index(req, FAKE_UUID1) + + self.assertEqual(res_dict, expected) + + @mock.patch('nova.db.instance_get_by_uuid') + @mock.patch('nova.db.security_group_get_by_instance', return_value=[]) + def test_get_security_group_empty_for_instance(self, mock_sec_group, + mock_db_get_ins): + expected = {'security_groups': []} + + def return_instance(context, server_id, + columns_to_join=None, use_slave=False): + self.assertEqual(server_id, FAKE_UUID1) + return return_server_by_uuid(context, server_id) + mock_db_get_ins.side_effect = return_instance + req = fakes.HTTPRequest.blank('/v2/%s/servers/%s/os-security-groups' % + ('fake', FAKE_UUID1)) + res_dict = self.server_controller.index(req, FAKE_UUID1) + self.assertEqual(expected, res_dict) + mock_sec_group.assert_called_once_with(req.environ['nova.context'], + FAKE_UUID1) + + def test_get_security_group_by_instance_non_existing(self): + self.stubs.Set(nova.db, 'instance_get', return_server_nonexistent) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_nonexistent) + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/os-security-groups') + self.assertRaises(webob.exc.HTTPNotFound, + self.server_controller.index, req, '1') + + def test_get_security_group_by_instance_invalid_id(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/servers/invalid/os-security-groups') + self.assertRaises(webob.exc.HTTPNotFound, + self.server_controller.index, req, 'invalid') + + def test_get_security_group_by_id(self): + sg = security_group_template(id=2, rules=[]) + + def return_security_group(context, group_id): + self.assertEqual(sg['id'], group_id) + return security_group_db(sg) + + self.stubs.Set(nova.db, 'security_group_get', + return_security_group) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/2') + res_dict = self.controller.show(req, '2') + + expected = {'security_group': sg} + self.assertEqual(res_dict, expected) + + def test_get_security_group_by_invalid_id(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/invalid') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete, + req, 'invalid') + + def test_get_security_group_by_non_existing_id(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/%s' % + self.fake_id) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, self.fake_id) + + def test_update_security_group(self): + sg = security_group_template(id=2, rules=[]) + sg_update = security_group_template(id=2, rules=[], + name='update_name', description='update_desc') + + def return_security_group(context, group_id): + self.assertEqual(sg['id'], group_id) + return security_group_db(sg) + + def return_update_security_group(context, group_id, values, + columns_to_join=None): + self.assertEqual(sg_update['id'], group_id) + self.assertEqual(sg_update['name'], values['name']) + self.assertEqual(sg_update['description'], values['description']) + return security_group_db(sg_update) + + self.stubs.Set(nova.db, 'security_group_update', + return_update_security_group) + self.stubs.Set(nova.db, 'security_group_get', + return_security_group) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/2') + res_dict = self.controller.update(req, '2', + {'security_group': sg_update}) + + expected = {'security_group': sg_update} + self.assertEqual(res_dict, expected) + + def test_update_security_group_name_to_default(self): + sg = security_group_template(id=2, rules=[], name='default') + + def return_security_group(context, group_id): + self.assertEqual(sg['id'], group_id) + return security_group_db(sg) + + self.stubs.Set(nova.db, 'security_group_get', + return_security_group) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/2') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, '2', {'security_group': sg}) + + def test_update_default_security_group_fail(self): + sg = security_group_template() + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/1') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, '1', {'security_group': sg}) + + def test_delete_security_group_by_id(self): + sg = security_group_template(id=1, project_id='fake_project', + user_id='fake_user', rules=[]) + + self.called = False + + def security_group_destroy(context, id): + self.called = True + + def return_security_group(context, group_id): + self.assertEqual(sg['id'], group_id) + return security_group_db(sg) + + self.stubs.Set(nova.db, 'security_group_destroy', + security_group_destroy) + self.stubs.Set(nova.db, 'security_group_get', + return_security_group) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/1') + self.controller.delete(req, '1') + + self.assertTrue(self.called) + + def test_delete_security_group_by_admin(self): + sg = security_group_template(id=2, rules=[]) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups') + self.controller.create(req, {'security_group': sg}) + context = req.environ['nova.context'] + + # Ensure quota usage for security group is correct. + self._assert_security_groups_in_use(context.project_id, + context.user_id, 2) + + # Delete the security group by admin. + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/2', + use_admin_context=True) + self.controller.delete(req, '2') + + # Ensure quota for security group in use is released. + self._assert_security_groups_in_use(context.project_id, + context.user_id, 1) + + def test_delete_security_group_by_invalid_id(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/invalid') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete, + req, 'invalid') + + def test_delete_security_group_by_non_existing_id(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/%s' + % self.fake_id) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, self.fake_id) + + def test_delete_security_group_in_use(self): + sg = security_group_template(id=1, rules=[]) + + def security_group_in_use(context, id): + return True + + def return_security_group(context, group_id): + self.assertEqual(sg['id'], group_id) + return security_group_db(sg) + + self.stubs.Set(nova.db, 'security_group_in_use', + security_group_in_use) + self.stubs.Set(nova.db, 'security_group_get', + return_security_group) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-groups/1') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete, + req, '1') + + def test_associate_by_non_existing_security_group_name(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.assertEqual(return_server(None, '1'), + nova.db.instance_get(None, '1')) + body = dict(addSecurityGroup=dict(name='non-existing')) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPNotFound, + self.manager._addSecurityGroup, req, '1', body) + + def test_associate_by_invalid_server_id(self): + body = dict(addSecurityGroup=dict(name='test')) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/invalid/action') + self.assertRaises(webob.exc.HTTPNotFound, + self.manager._addSecurityGroup, req, 'invalid', body) + + def test_associate_without_body(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + body = dict(addSecurityGroup=None) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._addSecurityGroup, req, '1', body) + + def test_associate_no_security_group_name(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + body = dict(addSecurityGroup=dict()) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._addSecurityGroup, req, '1', body) + + def test_associate_security_group_name_with_whitespaces(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + body = dict(addSecurityGroup=dict(name=" ")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._addSecurityGroup, req, '1', body) + + def test_associate_non_existing_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_server_nonexistent) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_nonexistent) + body = dict(addSecurityGroup=dict(name="test")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPNotFound, + self.manager._addSecurityGroup, req, '1', body) + + def test_associate_non_running_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_non_running_server) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_non_running_server) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_without_instances) + body = dict(addSecurityGroup=dict(name="test")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.manager._addSecurityGroup(req, '1', body) + + def test_associate_already_associated_security_group_to_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_by_uuid) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_by_name) + body = dict(addSecurityGroup=dict(name="test")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._addSecurityGroup, req, '1', body) + + def test_associate(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_by_uuid) + self.mox.StubOutWithMock(nova.db, 'instance_add_security_group') + nova.db.instance_add_security_group(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_without_instances) + self.mox.ReplayAll() + + body = dict(addSecurityGroup=dict(name="test")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.manager._addSecurityGroup(req, '1', body) + + def test_disassociate_by_non_existing_security_group_name(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.assertEqual(return_server(None, '1'), + nova.db.instance_get(None, '1')) + body = dict(removeSecurityGroup=dict(name='non-existing')) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPNotFound, + self.manager._removeSecurityGroup, req, '1', body) + + def test_disassociate_by_invalid_server_id(self): + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_by_name) + body = dict(removeSecurityGroup=dict(name='test')) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/invalid/action') + self.assertRaises(webob.exc.HTTPNotFound, + self.manager._removeSecurityGroup, req, 'invalid', + body) + + def test_disassociate_without_body(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + body = dict(removeSecurityGroup=None) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._removeSecurityGroup, req, '1', body) + + def test_disassociate_no_security_group_name(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + body = dict(removeSecurityGroup=dict()) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._removeSecurityGroup, req, '1', body) + + def test_disassociate_security_group_name_with_whitespaces(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + body = dict(removeSecurityGroup=dict(name=" ")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._removeSecurityGroup, req, '1', body) + + def test_disassociate_non_existing_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_server_nonexistent) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_by_name) + body = dict(removeSecurityGroup=dict(name="test")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPNotFound, + self.manager._removeSecurityGroup, req, '1', body) + + def test_disassociate_non_running_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_non_running_server) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_non_running_server) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_by_name) + body = dict(removeSecurityGroup=dict(name="test")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.manager._removeSecurityGroup(req, '1', body) + + def test_disassociate_already_associated_security_group_to_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_by_uuid) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_without_instances) + body = dict(removeSecurityGroup=dict(name="test")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.assertRaises(webob.exc.HTTPBadRequest, + self.manager._removeSecurityGroup, req, '1', body) + + def test_disassociate(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_by_uuid) + self.mox.StubOutWithMock(nova.db, 'instance_remove_security_group') + nova.db.instance_remove_security_group(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_by_name) + self.mox.ReplayAll() + + body = dict(removeSecurityGroup=dict(name="test")) + + req = fakes.HTTPRequest.blank('/v2/fake/servers/1/action') + self.manager._removeSecurityGroup(req, '1', body) + + +class TestSecurityGroupsV2(TestSecurityGroupsV21): + secgrp_ctl_cls = secgroups_v2.SecurityGroupController + server_secgrp_ctl_cls = secgroups_v2.ServerSecurityGroupController + secgrp_act_ctl_cls = secgroups_v2.SecurityGroupActionController + + +class TestSecurityGroupRulesV21(test.TestCase): + secgrp_ctl_cls = secgroups_v21.SecurityGroupRulesController + + def setUp(self): + super(TestSecurityGroupRulesV21, self).setUp() + + self.controller = self.secgrp_ctl_cls() + if self.controller.security_group_api.id_is_uuid: + id1 = '11111111-1111-1111-1111-111111111111' + id2 = '22222222-2222-2222-2222-222222222222' + self.invalid_id = '33333333-3333-3333-3333-333333333333' + else: + id1 = 1 + id2 = 2 + self.invalid_id = '33333333' + + self.sg1 = security_group_template(id=id1) + self.sg2 = security_group_template( + id=id2, name='authorize_revoke', + description='authorize-revoke testing') + + db1 = security_group_db(self.sg1) + db2 = security_group_db(self.sg2) + + def return_security_group(context, group_id, columns_to_join=None): + if group_id == db1['id']: + return db1 + if group_id == db2['id']: + return db2 + raise exception.SecurityGroupNotFound(security_group_id=group_id) + + self.stubs.Set(nova.db, 'security_group_get', + return_security_group) + + self.parent_security_group = db2 + + def test_create_by_cidr(self): + rule = security_group_rule_template(cidr='10.2.3.124/24', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + security_group_rule = res_dict['security_group_rule'] + self.assertNotEqual(security_group_rule['id'], 0) + self.assertEqual(security_group_rule['parent_group_id'], + self.sg2['id']) + self.assertEqual(security_group_rule['ip_range']['cidr'], + "10.2.3.124/24") + + def test_create_by_group_id(self): + rule = security_group_rule_template(group_id=self.sg1['id'], + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + + security_group_rule = res_dict['security_group_rule'] + self.assertNotEqual(security_group_rule['id'], 0) + self.assertEqual(security_group_rule['parent_group_id'], + self.sg2['id']) + + def test_create_by_same_group_id(self): + rule1 = security_group_rule_template(group_id=self.sg1['id'], + from_port=80, to_port=80, + parent_group_id=self.sg2['id']) + self.parent_security_group['rules'] = [security_group_rule_db(rule1)] + + rule2 = security_group_rule_template(group_id=self.sg1['id'], + from_port=81, to_port=81, + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule2}) + + security_group_rule = res_dict['security_group_rule'] + self.assertNotEqual(security_group_rule['id'], 0) + self.assertEqual(security_group_rule['parent_group_id'], + self.sg2['id']) + self.assertEqual(security_group_rule['from_port'], 81) + self.assertEqual(security_group_rule['to_port'], 81) + + def test_create_none_value_from_to_port(self): + rule = {'parent_group_id': self.sg1['id'], + 'group_id': self.sg1['id']} + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + security_group_rule = res_dict['security_group_rule'] + self.assertIsNone(security_group_rule['from_port']) + self.assertIsNone(security_group_rule['to_port']) + self.assertEqual(security_group_rule['group']['name'], 'test') + self.assertEqual(security_group_rule['parent_group_id'], + self.sg1['id']) + + def test_create_none_value_from_to_port_icmp(self): + rule = {'parent_group_id': self.sg1['id'], + 'group_id': self.sg1['id'], + 'ip_protocol': 'ICMP'} + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + security_group_rule = res_dict['security_group_rule'] + self.assertEqual(security_group_rule['ip_protocol'], 'ICMP') + self.assertEqual(security_group_rule['from_port'], -1) + self.assertEqual(security_group_rule['to_port'], -1) + self.assertEqual(security_group_rule['group']['name'], 'test') + self.assertEqual(security_group_rule['parent_group_id'], + self.sg1['id']) + + def test_create_none_value_from_to_port_tcp(self): + rule = {'parent_group_id': self.sg1['id'], + 'group_id': self.sg1['id'], + 'ip_protocol': 'TCP'} + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + security_group_rule = res_dict['security_group_rule'] + self.assertEqual(security_group_rule['ip_protocol'], 'TCP') + self.assertEqual(security_group_rule['from_port'], 1) + self.assertEqual(security_group_rule['to_port'], 65535) + self.assertEqual(security_group_rule['group']['name'], 'test') + self.assertEqual(security_group_rule['parent_group_id'], + self.sg1['id']) + + def test_create_by_invalid_cidr_json(self): + rule = security_group_rule_template( + ip_protocol="tcp", + from_port=22, + to_port=22, + parent_group_id=self.sg2['id'], + cidr="10.2.3.124/2433") + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_by_invalid_tcp_port_json(self): + rule = security_group_rule_template( + ip_protocol="tcp", + from_port=75534, + to_port=22, + parent_group_id=self.sg2['id'], + cidr="10.2.3.124/24") + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_by_invalid_icmp_port_json(self): + rule = security_group_rule_template( + ip_protocol="icmp", + from_port=1, + to_port=256, + parent_group_id=self.sg2['id'], + cidr="10.2.3.124/24") + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_add_existing_rules_by_cidr(self): + rule = security_group_rule_template(cidr='10.0.0.0/24', + parent_group_id=self.sg2['id']) + + self.parent_security_group['rules'] = [security_group_rule_db(rule)] + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_add_existing_rules_by_group_id(self): + rule = security_group_rule_template(group_id=1) + + self.parent_security_group['rules'] = [security_group_rule_db(rule)] + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_no_body(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, None) + + def test_create_with_no_security_group_rule_in_body(self): + rules = {'test': 'test'} + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, rules) + + def test_create_with_invalid_parent_group_id(self): + rule = security_group_rule_template(parent_group_id='invalid') + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_non_existing_parent_group_id(self): + rule = security_group_rule_template(group_id=None, + parent_group_id=self.invalid_id) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_non_existing_group_id(self): + rule = security_group_rule_template(group_id='invalid', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_invalid_protocol(self): + rule = security_group_rule_template(ip_protocol='invalid-protocol', + cidr='10.2.2.0/24', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_no_protocol(self): + rule = security_group_rule_template(cidr='10.2.2.0/24', + parent_group_id=self.sg2['id']) + del rule['ip_protocol'] + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_invalid_from_port(self): + rule = security_group_rule_template(from_port='666666', + cidr='10.2.2.0/24', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_invalid_to_port(self): + rule = security_group_rule_template(to_port='666666', + cidr='10.2.2.0/24', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_non_numerical_from_port(self): + rule = security_group_rule_template(from_port='invalid', + cidr='10.2.2.0/24', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_non_numerical_to_port(self): + rule = security_group_rule_template(to_port='invalid', + cidr='10.2.2.0/24', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_no_from_port(self): + rule = security_group_rule_template(cidr='10.2.2.0/24', + parent_group_id=self.sg2['id']) + del rule['from_port'] + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_no_to_port(self): + rule = security_group_rule_template(cidr='10.2.2.0/24', + parent_group_id=self.sg2['id']) + del rule['to_port'] + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_invalid_cidr(self): + rule = security_group_rule_template(cidr='10.2.2222.0/24', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_no_cidr_group(self): + rule = security_group_rule_template(parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + + security_group_rule = res_dict['security_group_rule'] + self.assertNotEqual(security_group_rule['id'], 0) + self.assertEqual(security_group_rule['parent_group_id'], + self.parent_security_group['id']) + self.assertEqual(security_group_rule['ip_range']['cidr'], + "0.0.0.0/0") + + def test_create_with_invalid_group_id(self): + rule = security_group_rule_template(group_id='invalid', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_empty_group_id(self): + rule = security_group_rule_template(group_id='', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_nonexist_group_id(self): + rule = security_group_rule_template(group_id=self.invalid_id, + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_with_same_group_parent_id_and_group_id(self): + rule = security_group_rule_template(group_id=self.sg1['id'], + parent_group_id=self.sg1['id']) + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + security_group_rule = res_dict['security_group_rule'] + self.assertNotEqual(security_group_rule['id'], 0) + self.assertEqual(security_group_rule['parent_group_id'], + self.sg1['id']) + self.assertEqual(security_group_rule['group']['name'], + self.sg1['name']) + + def _test_create_with_no_ports_and_no_group(self, proto): + rule = {'ip_protocol': proto, 'parent_group_id': self.sg2['id']} + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + def _test_create_with_no_ports(self, proto): + rule = {'ip_protocol': proto, 'parent_group_id': self.sg2['id'], + 'group_id': self.sg1['id']} + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + security_group_rule = res_dict['security_group_rule'] + expected_rule = { + 'from_port': 1, 'group': {'tenant_id': '123', 'name': 'test'}, + 'ip_protocol': proto, 'to_port': 65535, 'parent_group_id': + self.sg2['id'], 'ip_range': {}, 'id': security_group_rule['id'] + } + if proto == 'icmp': + expected_rule['to_port'] = -1 + expected_rule['from_port'] = -1 + self.assertEqual(expected_rule, security_group_rule) + + def test_create_with_no_ports_icmp(self): + self._test_create_with_no_ports_and_no_group('icmp') + self._test_create_with_no_ports('icmp') + + def test_create_with_no_ports_tcp(self): + self._test_create_with_no_ports_and_no_group('tcp') + self._test_create_with_no_ports('tcp') + + def test_create_with_no_ports_udp(self): + self._test_create_with_no_ports_and_no_group('udp') + self._test_create_with_no_ports('udp') + + def _test_create_with_ports(self, proto, from_port, to_port): + rule = { + 'ip_protocol': proto, 'from_port': from_port, 'to_port': to_port, + 'parent_group_id': self.sg2['id'], 'group_id': self.sg1['id'] + } + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + + security_group_rule = res_dict['security_group_rule'] + expected_rule = { + 'from_port': from_port, + 'group': {'tenant_id': '123', 'name': 'test'}, + 'ip_protocol': proto, 'to_port': to_port, 'parent_group_id': + self.sg2['id'], 'ip_range': {}, 'id': security_group_rule['id'] + } + self.assertEqual(proto, security_group_rule['ip_protocol']) + self.assertEqual(from_port, security_group_rule['from_port']) + self.assertEqual(to_port, security_group_rule['to_port']) + self.assertEqual(expected_rule, security_group_rule) + + def test_create_with_ports_icmp(self): + self._test_create_with_ports('icmp', 0, 1) + self._test_create_with_ports('icmp', 0, 0) + self._test_create_with_ports('icmp', 1, 0) + + def test_create_with_ports_tcp(self): + self._test_create_with_ports('tcp', 1, 1) + self._test_create_with_ports('tcp', 1, 65535) + self._test_create_with_ports('tcp', 65535, 65535) + + def test_create_with_ports_udp(self): + self._test_create_with_ports('udp', 1, 1) + self._test_create_with_ports('udp', 1, 65535) + self._test_create_with_ports('udp', 65535, 65535) + + def test_delete(self): + rule = security_group_rule_template(id=self.sg2['id'], + parent_group_id=self.sg2['id']) + + def security_group_rule_get(context, id): + return security_group_rule_db(rule) + + def security_group_rule_destroy(context, id): + pass + + self.stubs.Set(nova.db, 'security_group_rule_get', + security_group_rule_get) + self.stubs.Set(nova.db, 'security_group_rule_destroy', + security_group_rule_destroy) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules/%s' + % self.sg2['id']) + self.controller.delete(req, self.sg2['id']) + + def test_delete_invalid_rule_id(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules' + + '/invalid') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete, + req, 'invalid') + + def test_delete_non_existing_rule_id(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules/%s' + % self.invalid_id) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, self.invalid_id) + + def test_create_rule_quota_limit(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + for num in range(100, 100 + CONF.quota_security_group_rules): + rule = { + 'ip_protocol': 'tcp', 'from_port': num, + 'to_port': num, 'parent_group_id': self.sg2['id'], + 'group_id': self.sg1['id'] + } + self.controller.create(req, {'security_group_rule': rule}) + + rule = { + 'ip_protocol': 'tcp', 'from_port': '121', 'to_port': '121', + 'parent_group_id': self.sg2['id'], 'group_id': self.sg1['id'] + } + self.assertRaises(webob.exc.HTTPForbidden, self.controller.create, + req, {'security_group_rule': rule}) + + def test_create_rule_cidr_allow_all(self): + rule = security_group_rule_template(cidr='0.0.0.0/0', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + + security_group_rule = res_dict['security_group_rule'] + self.assertNotEqual(security_group_rule['id'], 0) + self.assertEqual(security_group_rule['parent_group_id'], + self.parent_security_group['id']) + self.assertEqual(security_group_rule['ip_range']['cidr'], + "0.0.0.0/0") + + def test_create_rule_cidr_ipv6_allow_all(self): + rule = security_group_rule_template(cidr='::/0', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + + security_group_rule = res_dict['security_group_rule'] + self.assertNotEqual(security_group_rule['id'], 0) + self.assertEqual(security_group_rule['parent_group_id'], + self.parent_security_group['id']) + self.assertEqual(security_group_rule['ip_range']['cidr'], + "::/0") + + def test_create_rule_cidr_allow_some(self): + rule = security_group_rule_template(cidr='15.0.0.0/8', + parent_group_id=self.sg2['id']) + + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + res_dict = self.controller.create(req, {'security_group_rule': rule}) + + security_group_rule = res_dict['security_group_rule'] + self.assertNotEqual(security_group_rule['id'], 0) + self.assertEqual(security_group_rule['parent_group_id'], + self.parent_security_group['id']) + self.assertEqual(security_group_rule['ip_range']['cidr'], + "15.0.0.0/8") + + def test_create_rule_cidr_bad_netmask(self): + rule = security_group_rule_template(cidr='15.0.0.0/0') + req = fakes.HTTPRequest.blank('/v2/fake/os-security-group-rules') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'security_group_rule': rule}) + + +class TestSecurityGroupRulesV2(TestSecurityGroupRulesV21): + secgrp_ctl_cls = secgroups_v2.SecurityGroupRulesController + + +class TestSecurityGroupRulesXMLDeserializer(test.TestCase): + + def setUp(self): + super(TestSecurityGroupRulesXMLDeserializer, self).setUp() + self.deserializer = secgroups_v2.SecurityGroupRulesXMLDeserializer() + + def test_create_request(self): + serial_request = """ +<security_group_rule> + <parent_group_id>12</parent_group_id> + <from_port>22</from_port> + <to_port>22</to_port> + <group_id></group_id> + <ip_protocol>tcp</ip_protocol> + <cidr>10.0.0.0/24</cidr> +</security_group_rule>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group_rule": { + "parent_group_id": "12", + "from_port": "22", + "to_port": "22", + "ip_protocol": "tcp", + "group_id": "", + "cidr": "10.0.0.0/24", + }, + } + self.assertEqual(request['body'], expected) + + def test_create_no_protocol_request(self): + serial_request = """ +<security_group_rule> + <parent_group_id>12</parent_group_id> + <from_port>22</from_port> + <to_port>22</to_port> + <group_id></group_id> + <cidr>10.0.0.0/24</cidr> +</security_group_rule>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group_rule": { + "parent_group_id": "12", + "from_port": "22", + "to_port": "22", + "group_id": "", + "cidr": "10.0.0.0/24", + }, + } + self.assertEqual(request['body'], expected) + + def test_corrupt_xml(self): + """Should throw a 400 error on corrupt xml.""" + self.assertRaises( + exception.MalformedRequestBody, + self.deserializer.deserialize, + utils.killer_xml_body()) + + +class TestSecurityGroupXMLDeserializer(test.TestCase): + + def setUp(self): + super(TestSecurityGroupXMLDeserializer, self).setUp() + self.deserializer = secgroups_v2.SecurityGroupXMLDeserializer() + + def test_create_request(self): + serial_request = """ +<security_group name="test"> + <description>test</description> +</security_group>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group": { + "name": "test", + "description": "test", + }, + } + self.assertEqual(request['body'], expected) + + def test_create_no_description_request(self): + serial_request = """ +<security_group name="test"> +</security_group>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group": { + "name": "test", + }, + } + self.assertEqual(request['body'], expected) + + def test_create_no_name_request(self): + serial_request = """ +<security_group> +<description>test</description> +</security_group>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "security_group": { + "description": "test", + }, + } + self.assertEqual(request['body'], expected) + + def test_corrupt_xml(self): + """Should throw a 400 error on corrupt xml.""" + self.assertRaises( + exception.MalformedRequestBody, + self.deserializer.deserialize, + utils.killer_xml_body()) + + +class TestSecurityGroupXMLSerializer(test.TestCase): + def setUp(self): + super(TestSecurityGroupXMLSerializer, self).setUp() + self.namespace = wsgi.XMLNS_V11 + self.rule_serializer = secgroups_v2.SecurityGroupRuleTemplate() + self.index_serializer = secgroups_v2.SecurityGroupsTemplate() + self.default_serializer = secgroups_v2.SecurityGroupTemplate() + + def _tag(self, elem): + tagname = elem.tag + self.assertEqual(tagname[0], '{') + tmp = tagname.partition('}') + namespace = tmp[0][1:] + self.assertEqual(namespace, self.namespace) + return tmp[2] + + def _verify_security_group_rule(self, raw_rule, tree): + self.assertEqual(raw_rule['id'], tree.get('id')) + self.assertEqual(raw_rule['parent_group_id'], + tree.get('parent_group_id')) + + seen = set() + expected = set(['ip_protocol', 'from_port', 'to_port', + 'group', 'group/name', 'group/tenant_id', + 'ip_range', 'ip_range/cidr']) + + for child in tree: + child_tag = self._tag(child) + self.assertIn(child_tag, raw_rule) + seen.add(child_tag) + if child_tag in ('group', 'ip_range'): + for gr_child in child: + gr_child_tag = self._tag(gr_child) + self.assertIn(gr_child_tag, raw_rule[child_tag]) + seen.add('%s/%s' % (child_tag, gr_child_tag)) + self.assertEqual(gr_child.text, + raw_rule[child_tag][gr_child_tag]) + else: + self.assertEqual(child.text, raw_rule[child_tag]) + self.assertEqual(seen, expected) + + def _verify_security_group(self, raw_group, tree): + rules = raw_group['rules'] + self.assertEqual('security_group', self._tag(tree)) + self.assertEqual(raw_group['id'], tree.get('id')) + self.assertEqual(raw_group['tenant_id'], tree.get('tenant_id')) + self.assertEqual(raw_group['name'], tree.get('name')) + self.assertEqual(2, len(tree)) + for child in tree: + child_tag = self._tag(child) + if child_tag == 'rules': + self.assertEqual(2, len(child)) + for idx, gr_child in enumerate(child): + self.assertEqual(self._tag(gr_child), 'rule') + self._verify_security_group_rule(rules[idx], gr_child) + else: + self.assertEqual('description', child_tag) + self.assertEqual(raw_group['description'], child.text) + + def test_rule_serializer(self): + raw_rule = dict( + id='123', + parent_group_id='456', + ip_protocol='tcp', + from_port='789', + to_port='987', + group=dict(name='group', tenant_id='tenant'), + ip_range=dict(cidr='10.0.0.0/8')) + rule = dict(security_group_rule=raw_rule) + text = self.rule_serializer.serialize(rule) + + tree = etree.fromstring(text) + + self.assertEqual('security_group_rule', self._tag(tree)) + self._verify_security_group_rule(raw_rule, tree) + + def test_group_serializer(self): + rules = [dict( + id='123', + parent_group_id='456', + ip_protocol='tcp', + from_port='789', + to_port='987', + group=dict(name='group1', tenant_id='tenant1'), + ip_range=dict(cidr='10.55.44.0/24')), + dict( + id='654', + parent_group_id='321', + ip_protocol='udp', + from_port='234', + to_port='567', + group=dict(name='group2', tenant_id='tenant2'), + ip_range=dict(cidr='10.44.55.0/24'))] + raw_group = dict( + id='890', + description='description', + name='name', + tenant_id='tenant', + rules=rules) + sg_group = dict(security_group=raw_group) + text = self.default_serializer.serialize(sg_group) + + tree = etree.fromstring(text) + + self._verify_security_group(raw_group, tree) + + def test_groups_serializer(self): + rules = [dict( + id='123', + parent_group_id='1234', + ip_protocol='tcp', + from_port='12345', + to_port='123456', + group=dict(name='group1', tenant_id='tenant1'), + ip_range=dict(cidr='10.123.0.0/24')), + dict( + id='234', + parent_group_id='2345', + ip_protocol='udp', + from_port='23456', + to_port='234567', + group=dict(name='group2', tenant_id='tenant2'), + ip_range=dict(cidr='10.234.0.0/24')), + dict( + id='345', + parent_group_id='3456', + ip_protocol='tcp', + from_port='34567', + to_port='345678', + group=dict(name='group3', tenant_id='tenant3'), + ip_range=dict(cidr='10.345.0.0/24')), + dict( + id='456', + parent_group_id='4567', + ip_protocol='udp', + from_port='45678', + to_port='456789', + group=dict(name='group4', tenant_id='tenant4'), + ip_range=dict(cidr='10.456.0.0/24'))] + groups = [dict( + id='567', + description='description1', + name='name1', + tenant_id='tenant1', + rules=rules[0:2]), + dict( + id='678', + description='description2', + name='name2', + tenant_id='tenant2', + rules=rules[2:4])] + sg_groups = dict(security_groups=groups) + text = self.index_serializer.serialize(sg_groups) + + tree = etree.fromstring(text) + + self.assertEqual('security_groups', self._tag(tree)) + self.assertEqual(len(groups), len(tree)) + for idx, child in enumerate(tree): + self._verify_security_group(groups[idx], child) + + +UUID1 = '00000000-0000-0000-0000-000000000001' +UUID2 = '00000000-0000-0000-0000-000000000002' +UUID3 = '00000000-0000-0000-0000-000000000003' + + +def fake_compute_get_all(*args, **kwargs): + base = {'id': 1, 'description': 'foo', 'user_id': 'bar', + 'project_id': 'baz', 'deleted': False, 'deleted_at': None, + 'updated_at': None, 'created_at': None} + db_list = [ + fakes.stub_instance( + 1, uuid=UUID1, + security_groups=[dict(base, **{'name': 'fake-0-0'}), + dict(base, **{'name': 'fake-0-1'})]), + fakes.stub_instance( + 2, uuid=UUID2, + security_groups=[dict(base, **{'name': 'fake-1-0'}), + dict(base, **{'name': 'fake-1-1'})]) + ] + + return instance_obj._make_instance_list(args[1], + objects.InstanceList(), + db_list, + ['metadata', 'system_metadata', + 'security_groups', 'info_cache']) + + +def fake_compute_get(*args, **kwargs): + inst = fakes.stub_instance(1, uuid=UUID3, + security_groups=[{'name': 'fake-2-0'}, + {'name': 'fake-2-1'}]) + return fake_instance.fake_instance_obj(args[1], + expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS, **inst) + + +def fake_compute_create(*args, **kwargs): + return ([fake_compute_get(*args, **kwargs)], '') + + +def fake_get_instances_security_groups_bindings(inst, context, servers): + groups = {UUID1: [{'name': 'fake-0-0'}, {'name': 'fake-0-1'}], + UUID2: [{'name': 'fake-1-0'}, {'name': 'fake-1-1'}], + UUID3: [{'name': 'fake-2-0'}, {'name': 'fake-2-1'}]} + result = {} + for server in servers: + result[server['id']] = groups.get(server['id']) + return result + + +class SecurityGroupsOutputTestV21(test.TestCase): + base_url = '/v2/fake/servers' + content_type = 'application/json' + + def setUp(self): + super(SecurityGroupsOutputTestV21, self).setUp() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all) + self.stubs.Set(compute.api.API, 'create', fake_compute_create) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Security_groups']) + self.app = self._setup_app() + + def _setup_app(self): + return fakes.wsgi_app_v21(init_only=('os-security-groups', 'servers')) + + def _make_request(self, url, body=None): + req = webob.Request.blank(url) + if body: + req.method = 'POST' + req.body = self._encode_body(body) + req.content_type = self.content_type + req.headers['Accept'] = self.content_type + res = req.get_response(self.app) + return res + + def _encode_body(self, body): + return jsonutils.dumps(body) + + def _get_server(self, body): + return jsonutils.loads(body).get('server') + + def _get_servers(self, body): + return jsonutils.loads(body).get('servers') + + def _get_groups(self, server): + return server.get('security_groups') + + def test_create(self): + image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + server = dict(name='server_test', imageRef=image_uuid, flavorRef=2) + res = self._make_request(self.base_url, {'server': server}) + self.assertEqual(res.status_int, 202) + server = self._get_server(res.body) + for i, group in enumerate(self._get_groups(server)): + name = 'fake-2-%s' % i + self.assertEqual(group.get('name'), name) + + def test_show(self): + url = self.base_url + '/' + UUID3 + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + server = self._get_server(res.body) + for i, group in enumerate(self._get_groups(server)): + name = 'fake-2-%s' % i + self.assertEqual(group.get('name'), name) + + def test_detail(self): + url = self.base_url + '/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + for i, server in enumerate(self._get_servers(res.body)): + for j, group in enumerate(self._get_groups(server)): + name = 'fake-%s-%s' % (i, j) + self.assertEqual(group.get('name'), name) + + def test_no_instance_passthrough_404(self): + + def fake_compute_get(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + url = self.base_url + '/70f6db34-de8d-4fbd-aafb-4065bdfa6115' + res = self._make_request(url) + + self.assertEqual(res.status_int, 404) + + +class SecurityGroupsOutputTestV2(SecurityGroupsOutputTestV21): + + def _setup_app(self): + return fakes.wsgi_app(init_only=('servers',)) + + +class SecurityGroupsOutputXmlTest(SecurityGroupsOutputTestV2): + content_type = 'application/xml' + + class MinimalCreateServerTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('server', selector='server') + root.set('name') + root.set('id') + root.set('imageRef') + root.set('flavorRef') + return xmlutil.MasterTemplate(root, 1, + nsmap={None: xmlutil.XMLNS_V11}) + + def _encode_body(self, body): + serializer = self.MinimalCreateServerTemplate() + return serializer.serialize(body) + + def _get_server(self, body): + return etree.XML(body) + + def _get_servers(self, body): + return etree.XML(body).getchildren() + + def _get_groups(self, server): + # NOTE(vish): we are adding security groups without an extension + # namespace so we don't break people using the existing + # functionality, but that means we need to use find with + # the existing server namespace. + namespace = server.nsmap[None] + return server.find('{%s}security_groups' % namespace).getchildren() diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_diagnostics.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_diagnostics.py new file mode 100644 index 0000000000..535a1afa15 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_diagnostics.py @@ -0,0 +1,132 @@ +# Copyright 2011 Eldar Nugaev +# 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. +from lxml import etree +import mock +from oslo.serialization import jsonutils + +from nova.api.openstack import compute +from nova.api.openstack.compute.contrib import server_diagnostics +from nova.api.openstack import wsgi +from nova.compute import api as compute_api +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + + +UUID = 'abc' + + +def fake_get_diagnostics(self, _context, instance_uuid): + return {'data': 'Some diagnostic info'} + + +def fake_instance_get(self, _context, instance_uuid, want_objects=False, + expected_attrs=None): + if instance_uuid != UUID: + raise Exception("Invalid UUID") + return {'uuid': instance_uuid} + + +class ServerDiagnosticsTestV21(test.NoDBTestCase): + + def _setup_router(self): + self.router = compute.APIRouterV3(init_only=('servers', + 'os-server-diagnostics')) + + def _get_request(self): + return fakes.HTTPRequestV3.blank( + '/servers/%s/diagnostics' % UUID) + + def setUp(self): + super(ServerDiagnosticsTestV21, self).setUp() + self._setup_router() + + @mock.patch.object(compute_api.API, 'get_diagnostics', + fake_get_diagnostics) + @mock.patch.object(compute_api.API, 'get', + fake_instance_get) + def test_get_diagnostics(self): + req = self._get_request() + res = req.get_response(self.router) + output = jsonutils.loads(res.body) + self.assertEqual(output, {'data': 'Some diagnostic info'}) + + @mock.patch.object(compute_api.API, 'get_diagnostics', + fake_get_diagnostics) + @mock.patch.object(compute_api.API, 'get', + side_effect=exception.InstanceNotFound(instance_id=UUID)) + def test_get_diagnostics_with_non_existed_instance(self, mock_get): + req = self._get_request() + res = req.get_response(self.router) + self.assertEqual(res.status_int, 404) + + @mock.patch.object(compute_api.API, 'get_diagnostics', + side_effect=exception.InstanceInvalidState('fake message')) + @mock.patch.object(compute_api.API, 'get', fake_instance_get) + def test_get_diagnostics_raise_conflict_on_invalid_state(self, + mock_get_diagnostics): + req = self._get_request() + res = req.get_response(self.router) + self.assertEqual(409, res.status_int) + + @mock.patch.object(compute_api.API, 'get_diagnostics', + side_effect=NotImplementedError) + @mock.patch.object(compute_api.API, 'get', fake_instance_get) + def test_get_diagnostics_raise_no_notimplementederror(self, + mock_get_diagnostics): + req = self._get_request() + res = req.get_response(self.router) + self.assertEqual(501, res.status_int) + + +class ServerDiagnosticsTestV2(ServerDiagnosticsTestV21): + + def _setup_router(self): + self.flags(verbose=True, + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Server_diagnostics']) + + self.router = compute.APIRouter(init_only=('servers', 'diagnostics')) + + def _get_request(self): + return fakes.HTTPRequest.blank( + '/fake/servers/%s/diagnostics' % UUID) + + +class TestServerDiagnosticsXMLSerializer(test.NoDBTestCase): + namespace = wsgi.XMLNS_V11 + + def _tag(self, elem): + tagname = elem.tag + self.assertEqual(tagname[0], '{') + tmp = tagname.partition('}') + namespace = tmp[0][1:] + self.assertEqual(namespace, self.namespace) + return tmp[2] + + def test_index_serializer(self): + serializer = server_diagnostics.ServerDiagnosticsTemplate() + exemplar = dict(diag1='foo', diag2='bar') + text = serializer.serialize(exemplar) + + tree = etree.fromstring(text) + + self.assertEqual('diagnostics', self._tag(tree)) + self.assertEqual(len(tree), len(exemplar)) + for child in tree: + tag = self._tag(child) + self.assertIn(tag, exemplar) + self.assertEqual(child.text, exemplar[tag]) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_external_events.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_external_events.py new file mode 100644 index 0000000000..61801ba648 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_external_events.py @@ -0,0 +1,158 @@ +# Copyright 2014 Red Hat, 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.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import server_external_events +from nova import context +from nova import exception +from nova import objects +from nova import test + +fake_instances = { + '00000000-0000-0000-0000-000000000001': objects.Instance( + uuid='00000000-0000-0000-0000-000000000001', host='host1'), + '00000000-0000-0000-0000-000000000002': objects.Instance( + uuid='00000000-0000-0000-0000-000000000002', host='host1'), + '00000000-0000-0000-0000-000000000003': objects.Instance( + uuid='00000000-0000-0000-0000-000000000003', host='host2'), + '00000000-0000-0000-0000-000000000004': objects.Instance( + uuid='00000000-0000-0000-0000-000000000004', host=None), +} +fake_instance_uuids = sorted(fake_instances.keys()) +MISSING_UUID = '00000000-0000-0000-0000-000000000005' + + +@classmethod +def fake_get_by_uuid(cls, context, uuid): + try: + return fake_instances[uuid] + except KeyError: + raise exception.InstanceNotFound(instance_id=uuid) + + +@mock.patch('nova.objects.instance.Instance.get_by_uuid', fake_get_by_uuid) +class ServerExternalEventsTest(test.NoDBTestCase): + def setUp(self): + super(ServerExternalEventsTest, self).setUp() + self.api = server_external_events.ServerExternalEventsController() + self.context = context.get_admin_context() + self.event_1 = {'name': 'network-vif-plugged', + 'tag': 'foo', + 'server_uuid': fake_instance_uuids[0]} + self.event_2 = {'name': 'network-changed', + 'server_uuid': fake_instance_uuids[1]} + self.default_body = {'events': [self.event_1, self.event_2]} + self.resp_event_1 = dict(self.event_1) + self.resp_event_1['code'] = 200 + self.resp_event_1['status'] = 'completed' + self.resp_event_2 = dict(self.event_2) + self.resp_event_2['code'] = 200 + self.resp_event_2['status'] = 'completed' + self.default_resp_body = {'events': [self.resp_event_1, + self.resp_event_2]} + + def _create_req(self, body): + req = webob.Request.blank('/v2/fake/os-server-external-events') + req.method = 'POST' + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + req.body = jsonutils.dumps(body) + return req + + def _assert_call(self, req, body, expected_uuids, expected_events): + with mock.patch.object(self.api.compute_api, + 'external_instance_event') as api_method: + response = self.api.create(req, body) + + result = response.obj + code = response._code + + self.assertEqual(1, api_method.call_count) + for inst in api_method.call_args_list[0][0][1]: + expected_uuids.remove(inst.uuid) + self.assertEqual([], expected_uuids) + for event in api_method.call_args_list[0][0][2]: + expected_events.remove(event.name) + self.assertEqual([], expected_events) + return result, code + + def test_create(self): + req = self._create_req(self.default_body) + result, code = self._assert_call(req, self.default_body, + fake_instance_uuids[:2], + ['network-vif-plugged', + 'network-changed']) + self.assertEqual(self.default_resp_body, result) + self.assertEqual(200, code) + + def test_create_one_bad_instance(self): + body = self.default_body + body['events'][1]['server_uuid'] = MISSING_UUID + req = self._create_req(body) + result, code = self._assert_call(req, body, [fake_instance_uuids[0]], + ['network-vif-plugged']) + self.assertEqual('failed', result['events'][1]['status']) + self.assertEqual(200, result['events'][0]['code']) + self.assertEqual(404, result['events'][1]['code']) + self.assertEqual(207, code) + + def test_create_event_instance_has_no_host(self): + body = self.default_body + body['events'][0]['server_uuid'] = fake_instance_uuids[-1] + req = self._create_req(body) + # the instance without host should not be passed to the compute layer + result, code = self._assert_call(req, body, + [fake_instance_uuids[1]], + ['network-changed']) + self.assertEqual(422, result['events'][0]['code']) + self.assertEqual('failed', result['events'][0]['status']) + self.assertEqual(200, result['events'][1]['code']) + self.assertEqual(207, code) + + def test_create_no_good_instances(self): + body = self.default_body + body['events'][0]['server_uuid'] = MISSING_UUID + body['events'][1]['server_uuid'] = MISSING_UUID + req = self._create_req(body) + self.assertRaises(webob.exc.HTTPNotFound, + self.api.create, req, body) + + def test_create_bad_status(self): + body = self.default_body + body['events'][1]['status'] = 'foo' + req = self._create_req(body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.api.create, req, body) + + def test_create_extra_gorp(self): + body = self.default_body + body['events'][0]['foobar'] = 'bad stuff' + req = self._create_req(body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.api.create, req, body) + + def test_create_bad_events(self): + body = {'events': 'foo'} + req = self._create_req(body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.api.create, req, body) + + def test_create_bad_body(self): + body = {'foo': 'bar'} + req = self._create_req(body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.api.create, req, body) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_group_quotas.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_group_quotas.py new file mode 100644 index 0000000000..9e756cf157 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_group_quotas.py @@ -0,0 +1,188 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P +# 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. + +from oslo.config import cfg +import webob + +from nova.api.openstack.compute.contrib import server_groups +from nova.api.openstack.compute.plugins.v3 import server_groups as sg_v3 +from nova.api.openstack import extensions +from nova import context +import nova.db +from nova.openstack.common import uuidutils +from nova import quota +from nova import test +from nova.tests.unit.api.openstack import fakes + +CONF = cfg.CONF + + +class AttrDict(dict): + def __getattr__(self, k): + return self[k] + + +def server_group_template(**kwargs): + sgroup = kwargs.copy() + sgroup.setdefault('name', 'test') + return sgroup + + +def server_group_db(sg): + attrs = sg.copy() + if 'id' in attrs: + attrs['uuid'] = attrs.pop('id') + if 'policies' in attrs: + policies = attrs.pop('policies') + attrs['policies'] = policies + else: + attrs['policies'] = [] + if 'members' in attrs: + members = attrs.pop('members') + attrs['members'] = members + else: + attrs['members'] = [] + if 'metadata' in attrs: + attrs['metadetails'] = attrs.pop('metadata') + else: + attrs['metadetails'] = {} + attrs['deleted'] = 0 + attrs['deleted_at'] = None + attrs['created_at'] = None + attrs['updated_at'] = None + if 'user_id' not in attrs: + attrs['user_id'] = 'user_id' + if 'project_id' not in attrs: + attrs['project_id'] = 'project_id' + attrs['id'] = 7 + + return AttrDict(attrs) + + +class ServerGroupQuotasTestV21(test.TestCase): + + def setUp(self): + super(ServerGroupQuotasTestV21, self).setUp() + self._setup_controller() + self.app = self._get_app() + + def _setup_controller(self): + self.controller = sg_v3.ServerGroupController() + + def _get_app(self): + return fakes.wsgi_app_v21(init_only=('os-server-groups',)) + + def _get_url(self): + return '/v2/fake' + + def _setup_quotas(self): + pass + + def _assert_server_groups_in_use(self, project_id, user_id, in_use): + ctxt = context.get_admin_context() + result = quota.QUOTAS.get_user_quotas(ctxt, project_id, user_id) + self.assertEqual(result['server_groups']['in_use'], in_use) + + def test_create_server_group_normal(self): + self._setup_quotas() + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups') + sgroup = server_group_template() + policies = ['anti-affinity'] + sgroup['policies'] = policies + res_dict = self.controller.create(req, {'server_group': sgroup}) + self.assertEqual(res_dict['server_group']['name'], 'test') + self.assertTrue(uuidutils.is_uuid_like(res_dict['server_group']['id'])) + self.assertEqual(res_dict['server_group']['policies'], policies) + + def test_create_server_group_quota_limit(self): + self._setup_quotas() + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups') + sgroup = server_group_template() + policies = ['anti-affinity'] + sgroup['policies'] = policies + # Start by creating as many server groups as we're allowed to. + for i in range(CONF.quota_server_groups): + self.controller.create(req, {'server_group': sgroup}) + + # Then, creating a server group should fail. + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.create, + req, {'server_group': sgroup}) + + def test_delete_server_group_by_admin(self): + self._setup_quotas() + sgroup = server_group_template() + policies = ['anti-affinity'] + sgroup['policies'] = policies + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups') + res = self.controller.create(req, {'server_group': sgroup}) + sg_id = res['server_group']['id'] + context = req.environ['nova.context'] + + self._assert_server_groups_in_use(context.project_id, + context.user_id, 1) + + # Delete the server group we've just created. + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups/%s' % sg_id, + use_admin_context=True) + self.controller.delete(req, sg_id) + + # Make sure the quota in use has been released. + self._assert_server_groups_in_use(context.project_id, + context.user_id, 0) + + def test_delete_server_group_by_id(self): + self._setup_quotas() + sg = server_group_template(id='123') + self.called = False + + def server_group_delete(context, id): + self.called = True + + def return_server_group(context, group_id): + self.assertEqual(sg['id'], group_id) + return server_group_db(sg) + + self.stubs.Set(nova.db, 'instance_group_delete', + server_group_delete) + self.stubs.Set(nova.db, 'instance_group_get', + return_server_group) + + req = fakes.HTTPRequest.blank('/v2/fake/os-server-groups/123') + resp = self.controller.delete(req, '123') + self.assertTrue(self.called) + + # NOTE: on v2.1, http status code is set as wsgi_code of API + # method instead of status_int in a response object. + if isinstance(self.controller, sg_v3.ServerGroupController): + status_int = self.controller.delete.wsgi_code + else: + status_int = resp.status_int + self.assertEqual(204, status_int) + + +class ServerGroupQuotasTestV2(ServerGroupQuotasTestV21): + + def _setup_controller(self): + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.controller = server_groups.ServerGroupController(self.ext_mgr) + + def _setup_quotas(self): + self.ext_mgr.is_loaded('os-server-group-quotas').MultipleTimes()\ + .AndReturn(True) + self.mox.ReplayAll() + + def _get_app(self): + return fakes.wsgi_app(init_only=('os-server-groups',)) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_groups.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_groups.py new file mode 100644 index 0000000000..7dd2675c9e --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_groups.py @@ -0,0 +1,521 @@ +# Copyright (c) 2014 Cisco Systems, Inc. +# 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. + +from lxml import etree +import webob + +from nova.api.openstack.compute.contrib import server_groups +from nova.api.openstack.compute.plugins.v3 import server_groups as sg_v3 +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova import context +import nova.db +from nova import exception +from nova import objects +from nova.openstack.common import uuidutils +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import utils + +FAKE_UUID1 = 'a47ae74e-ab08-447f-8eee-ffd43fc46c16' +FAKE_UUID2 = 'c6e6430a-6563-4efa-9542-5e93c9e97d18' +FAKE_UUID3 = 'b8713410-9ba3-e913-901b-13410ca90121' + + +class AttrDict(dict): + def __getattr__(self, k): + return self[k] + + +def server_group_template(**kwargs): + sgroup = kwargs.copy() + sgroup.setdefault('name', 'test') + return sgroup + + +def server_group_resp_template(**kwargs): + sgroup = kwargs.copy() + sgroup.setdefault('name', 'test') + sgroup.setdefault('policies', []) + sgroup.setdefault('members', []) + return sgroup + + +def server_group_db(sg): + attrs = sg.copy() + if 'id' in attrs: + attrs['uuid'] = attrs.pop('id') + if 'policies' in attrs: + policies = attrs.pop('policies') + attrs['policies'] = policies + else: + attrs['policies'] = [] + if 'members' in attrs: + members = attrs.pop('members') + attrs['members'] = members + else: + attrs['members'] = [] + attrs['deleted'] = 0 + attrs['deleted_at'] = None + attrs['created_at'] = None + attrs['updated_at'] = None + if 'user_id' not in attrs: + attrs['user_id'] = 'user_id' + if 'project_id' not in attrs: + attrs['project_id'] = 'project_id' + attrs['id'] = 7 + + return AttrDict(attrs) + + +class ServerGroupTestV21(test.TestCase): + + def setUp(self): + super(ServerGroupTestV21, self).setUp() + self._setup_controller() + self.app = self._get_app() + + def _setup_controller(self): + self.controller = sg_v3.ServerGroupController() + + def _get_app(self): + return fakes.wsgi_app_v21(init_only=('os-server-groups',)) + + def _get_url(self): + return '/v2/fake' + + def test_create_server_group_with_no_policies(self): + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + sgroup = server_group_template() + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + def test_create_server_group_normal(self): + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + sgroup = server_group_template() + policies = ['anti-affinity'] + sgroup['policies'] = policies + res_dict = self.controller.create(req, {'server_group': sgroup}) + self.assertEqual(res_dict['server_group']['name'], 'test') + self.assertTrue(uuidutils.is_uuid_like(res_dict['server_group']['id'])) + self.assertEqual(res_dict['server_group']['policies'], policies) + + def _create_instance(self, context): + instance = objects.Instance(image_ref=1, node='node1', + reservation_id='a', host='host1', project_id='fake', + vm_state='fake', system_metadata={'key': 'value'}) + instance.create(context) + return instance + + def _create_instance_group(self, context, members): + ig = objects.InstanceGroup(name='fake_name', + user_id='fake_user', project_id='fake', + members=members) + ig.create(context) + return ig.uuid + + def _create_groups_and_instances(self, ctx): + instances = [self._create_instance(ctx), self._create_instance(ctx)] + members = [instance.uuid for instance in instances] + ig_uuid = self._create_instance_group(ctx, members) + return (ig_uuid, instances, members) + + def test_display_members(self): + ctx = context.RequestContext('fake_user', 'fake') + (ig_uuid, instances, members) = self._create_groups_and_instances(ctx) + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + res_dict = self.controller.show(req, ig_uuid) + result_members = res_dict['server_group']['members'] + self.assertEqual(2, len(result_members)) + for member in members: + self.assertIn(member, result_members) + + def test_display_active_members_only(self): + ctx = context.RequestContext('fake_user', 'fake') + (ig_uuid, instances, members) = self._create_groups_and_instances(ctx) + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + + # delete an instance + instances[1].destroy(ctx) + # check that the instance does not exist + self.assertRaises(exception.InstanceNotFound, + objects.Instance.get_by_uuid, + ctx, instances[1].uuid) + res_dict = self.controller.show(req, ig_uuid) + result_members = res_dict['server_group']['members'] + # check that only the active instance is displayed + self.assertEqual(1, len(result_members)) + self.assertIn(instances[0].uuid, result_members) + + def test_create_server_group_with_illegal_name(self): + # blank name + sgroup = server_group_template(name='', policies=['test_policy']) + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + # name with length 256 + sgroup = server_group_template(name='1234567890' * 26, + policies=['test_policy']) + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + # non-string name + sgroup = server_group_template(name=12, policies=['test_policy']) + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + # name with leading spaces + sgroup = server_group_template(name=' leading spaces', + policies=['test_policy']) + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + # name with trailing spaces + sgroup = server_group_template(name='trailing space ', + policies=['test_policy']) + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + # name with all spaces + sgroup = server_group_template(name=' ', + policies=['test_policy']) + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + def test_create_server_group_with_illegal_policies(self): + # blank policy + sgroup = server_group_template(name='fake-name', policies='') + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + # policy as integer + sgroup = server_group_template(name='fake-name', policies=7) + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + # policy as string + sgroup = server_group_template(name='fake-name', policies='invalid') + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + # policy as None + sgroup = server_group_template(name='fake-name', policies=None) + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + def test_create_server_group_conflicting_policies(self): + sgroup = server_group_template() + policies = ['anti-affinity', 'affinity'] + sgroup['policies'] = policies + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + def test_create_server_group_with_duplicate_policies(self): + sgroup = server_group_template() + policies = ['affinity', 'affinity'] + sgroup['policies'] = policies + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + def test_create_server_group_not_supported(self): + sgroup = server_group_template() + policies = ['storage-affinity', 'anti-affinity', 'rack-affinity'] + sgroup['policies'] = policies + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, {'server_group': sgroup}) + + def test_create_server_group_with_no_body(self): + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, None) + + def test_create_server_group_with_no_server_group(self): + body = {'no-instanceGroup': None} + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, body) + + def test_list_server_group_by_tenant(self): + groups = [] + policies = ['anti-affinity'] + members = [] + metadata = {} # always empty + names = ['default-x', 'test'] + sg1 = server_group_resp_template(id=str(1345), + name=names[0], + policies=policies, + members=members, + metadata=metadata) + sg2 = server_group_resp_template(id=str(891), + name=names[1], + policies=policies, + members=members, + metadata=metadata) + groups = [sg1, sg2] + expected = {'server_groups': groups} + + def return_server_groups(context, project_id): + return [server_group_db(sg) for sg in groups] + + self.stubs.Set(nova.db, 'instance_group_get_all_by_project_id', + return_server_groups) + + req = fakes.HTTPRequest.blank(self._get_url() + '/os-server-groups') + res_dict = self.controller.index(req) + self.assertEqual(res_dict, expected) + + def test_list_server_group_all(self): + all_groups = [] + tenant_groups = [] + policies = ['anti-affinity'] + members = [] + metadata = {} # always empty + names = ['default-x', 'test'] + sg1 = server_group_resp_template(id=str(1345), + name=names[0], + policies=[], + members=members, + metadata=metadata) + sg2 = server_group_resp_template(id=str(891), + name=names[1], + policies=policies, + members=members, + metadata={}) + tenant_groups = [sg2] + all_groups = [sg1, sg2] + + all = {'server_groups': all_groups} + tenant_specific = {'server_groups': tenant_groups} + + def return_all_server_groups(context): + return [server_group_db(sg) for sg in all_groups] + + self.stubs.Set(nova.db, 'instance_group_get_all', + return_all_server_groups) + + def return_tenant_server_groups(context, project_id): + return [server_group_db(sg) for sg in tenant_groups] + + self.stubs.Set(nova.db, 'instance_group_get_all_by_project_id', + return_tenant_server_groups) + + path = self._get_url() + '/os-server-groups?all_projects=True' + + req = fakes.HTTPRequest.blank(path, use_admin_context=True) + res_dict = self.controller.index(req) + self.assertEqual(res_dict, all) + req = fakes.HTTPRequest.blank(path) + res_dict = self.controller.index(req) + self.assertEqual(res_dict, tenant_specific) + + def test_delete_server_group_by_id(self): + sg = server_group_template(id='123') + + self.called = False + + def server_group_delete(context, id): + self.called = True + + def return_server_group(context, group_id): + self.assertEqual(sg['id'], group_id) + return server_group_db(sg) + + self.stubs.Set(nova.db, 'instance_group_delete', + server_group_delete) + self.stubs.Set(nova.db, 'instance_group_get', + return_server_group) + + req = fakes.HTTPRequest.blank(self._get_url() + + '/os-server-groups/123') + resp = self.controller.delete(req, '123') + self.assertTrue(self.called) + + # NOTE: on v2.1, http status code is set as wsgi_code of API + # method instead of status_int in a response object. + if isinstance(self.controller, sg_v3.ServerGroupController): + status_int = self.controller.delete.wsgi_code + else: + status_int = resp.status_int + self.assertEqual(204, status_int) + + def test_delete_non_existing_server_group(self): + req = fakes.HTTPRequest.blank(self._get_url() + + '/os-server-groups/invalid') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, 'invalid') + + +class ServerGroupTestV2(ServerGroupTestV21): + + def _setup_controller(self): + ext_mgr = extensions.ExtensionManager() + ext_mgr.extensions = {} + self.controller = server_groups.ServerGroupController(ext_mgr) + + def _get_app(self): + return fakes.wsgi_app(init_only=('os-server-groups',)) + + +class TestServerGroupXMLDeserializer(test.TestCase): + + def setUp(self): + super(TestServerGroupXMLDeserializer, self).setUp() + self.deserializer = server_groups.ServerGroupXMLDeserializer() + + def test_create_request(self): + serial_request = """ +<server_group name="test"> +</server_group>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server_group": { + "name": "test", + "policies": [] + }, + } + self.assertEqual(request['body'], expected) + + def test_update_request(self): + serial_request = """ +<server_group name="test"> +<policies> +<policy>policy-1</policy> +<policy>policy-2</policy> +</policies> +</server_group>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server_group": { + "name": 'test', + "policies": ['policy-1', 'policy-2'] + }, + } + self.assertEqual(request['body'], expected) + + def test_create_request_no_name(self): + serial_request = """ +<server_group> +</server_group>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server_group": { + "policies": [] + }, + } + self.assertEqual(request['body'], expected) + + def test_corrupt_xml(self): + """Should throw a 400 error on corrupt xml.""" + self.assertRaises( + exception.MalformedRequestBody, + self.deserializer.deserialize, + utils.killer_xml_body()) + + +class TestServerGroupXMLSerializer(test.TestCase): + def setUp(self): + super(TestServerGroupXMLSerializer, self).setUp() + self.namespace = wsgi.XMLNS_V11 + self.index_serializer = server_groups.ServerGroupsTemplate() + self.default_serializer = server_groups.ServerGroupTemplate() + + def _tag(self, elem): + tagname = elem.tag + self.assertEqual(tagname[0], '{') + tmp = tagname.partition('}') + namespace = tmp[0][1:] + self.assertEqual(namespace, self.namespace) + return tmp[2] + + def _verify_server_group(self, raw_group, tree): + policies = raw_group['policies'] + members = raw_group['members'] + self.assertEqual('server_group', self._tag(tree)) + self.assertEqual(raw_group['id'], tree.get('id')) + self.assertEqual(raw_group['name'], tree.get('name')) + self.assertEqual(3, len(tree)) + for child in tree: + child_tag = self._tag(child) + if child_tag == 'policies': + self.assertEqual(len(policies), len(child)) + for idx, gr_child in enumerate(child): + self.assertEqual(self._tag(gr_child), 'policy') + self.assertEqual(policies[idx], + gr_child.text) + elif child_tag == 'members': + self.assertEqual(len(members), len(child)) + for idx, gr_child in enumerate(child): + self.assertEqual(self._tag(gr_child), 'member') + self.assertEqual(members[idx], + gr_child.text) + elif child_tag == 'metadata': + self.assertEqual(0, len(child)) + + def _verify_server_group_brief(self, raw_group, tree): + self.assertEqual('server_group', self._tag(tree)) + self.assertEqual(raw_group['id'], tree.get('id')) + self.assertEqual(raw_group['name'], tree.get('name')) + + def test_group_serializer(self): + policies = ["policy-1", "policy-2"] + members = ["1", "2"] + raw_group = dict( + id='890', + name='name', + policies=policies, + members=members) + sg_group = dict(server_group=raw_group) + text = self.default_serializer.serialize(sg_group) + + tree = etree.fromstring(text) + + self._verify_server_group(raw_group, tree) + + def test_groups_serializer(self): + policies = ["policy-1", "policy-2", + "policy-3"] + members = ["1", "2", "3"] + groups = [dict( + id='890', + name='test', + policies=policies[0:2], + members=members[0:2]), + dict( + id='123', + name='default', + policies=policies[2:], + members=members[2:])] + sg_groups = dict(server_groups=groups) + text = self.index_serializer.serialize(sg_groups) + + tree = etree.fromstring(text) + + self.assertEqual('server_groups', self._tag(tree)) + self.assertEqual(len(groups), len(tree)) + for idx, child in enumerate(tree): + self._verify_server_group_brief(groups[idx], child) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_password.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_password.py new file mode 100644 index 0000000000..d29b0480f3 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_password.py @@ -0,0 +1,94 @@ +# Copyright 2012 Nebula, Inc. +# 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. + +from lxml import etree +from oslo.config import cfg +from oslo.serialization import jsonutils +import webob + +from nova.api.metadata import password +from nova import compute +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + + +CONF = cfg.CONF +CONF.import_opt('osapi_compute_ext_list', 'nova.api.openstack.compute.contrib') + + +class ServerPasswordTest(test.TestCase): + content_type = 'application/json' + + def setUp(self): + super(ServerPasswordTest, self).setUp() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set( + compute.api.API, 'get', + lambda self, ctxt, *a, **kw: + fake_instance.fake_instance_obj( + ctxt, + system_metadata={}, + expected_attrs=['system_metadata'])) + self.password = 'fakepass' + + def fake_extract_password(instance): + return self.password + + def fake_convert_password(context, password): + self.password = password + return {} + + self.stubs.Set(password, 'extract_password', fake_extract_password) + self.stubs.Set(password, 'convert_password', fake_convert_password) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Server_password']) + + def _make_request(self, url, method='GET'): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + req.method = method + res = req.get_response( + fakes.wsgi_app(init_only=('servers', 'os-server-password'))) + return res + + def _get_pass(self, body): + return jsonutils.loads(body).get('password') + + def test_get_password(self): + url = '/v2/fake/servers/fake/os-server-password' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + self.assertEqual(self._get_pass(res.body), 'fakepass') + + def test_reset_password(self): + url = '/v2/fake/servers/fake/os-server-password' + res = self._make_request(url, 'DELETE') + self.assertEqual(res.status_int, 204) + + res = self._make_request(url) + self.assertEqual(res.status_int, 200) + self.assertEqual(self._get_pass(res.body), '') + + +class ServerPasswordXmlTest(ServerPasswordTest): + content_type = 'application/xml' + + def _get_pass(self, body): + # NOTE(vish): first element is password + return etree.XML(body).text or '' diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_start_stop.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_start_stop.py new file mode 100644 index 0000000000..6be2a52b86 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_start_stop.py @@ -0,0 +1,183 @@ +# Copyright (c) 2012 Midokura Japan K.K. +# +# 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 mox +import webob + +from nova.api.openstack.compute.contrib import server_start_stop \ + as server_v2 +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import servers \ + as server_v21 +from nova.compute import api as compute_api +from nova import db +from nova import exception +from nova.openstack.common import policy as common_policy +from nova import policy +from nova import test +from nova.tests.unit.api.openstack import fakes + + +def fake_instance_get(context, instance_id, + columns_to_join=None, use_slave=False): + result = fakes.stub_instance(id=1, uuid=instance_id) + result['created_at'] = None + result['deleted_at'] = None + result['updated_at'] = None + result['deleted'] = 0 + result['info_cache'] = {'network_info': '[]', + 'instance_uuid': result['uuid']} + return result + + +def fake_start_stop_not_ready(self, context, instance): + raise exception.InstanceNotReady(instance_id=instance["uuid"]) + + +def fake_start_stop_locked_server(self, context, instance): + raise exception.InstanceIsLocked(instance_uuid=instance['uuid']) + + +def fake_start_stop_invalid_state(self, context, instance): + raise exception.InstanceIsLocked(instance_uuid=instance['uuid']) + + +class ServerStartStopTestV21(test.TestCase): + start_policy = "compute:v3:servers:start" + stop_policy = "compute:v3:servers:stop" + + def setUp(self): + super(ServerStartStopTestV21, self).setUp() + self._setup_controller() + + def _setup_controller(self): + ext_info = plugins.LoadedExtensionInfo() + self.controller = server_v21.ServersController( + extension_info=ext_info) + + def test_start(self): + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) + self.mox.StubOutWithMock(compute_api.API, 'start') + compute_api.API.start(mox.IgnoreArg(), mox.IgnoreArg()) + self.mox.ReplayAll() + + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + body = dict(start="") + self.controller._start_server(req, 'test_inst', body) + + def test_start_policy_failed(self): + rules = { + self.start_policy: + common_policy.parse_rule("project_id:non_fake") + } + policy.set_rules(rules) + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + body = dict(start="") + exc = self.assertRaises(exception.PolicyNotAuthorized, + self.controller._start_server, + req, 'test_inst', body) + self.assertIn(self.start_policy, exc.format_message()) + + def test_start_not_ready(self): + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) + self.stubs.Set(compute_api.API, 'start', fake_start_stop_not_ready) + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + body = dict(start="") + self.assertRaises(webob.exc.HTTPConflict, + self.controller._start_server, req, 'test_inst', body) + + def test_start_locked_server(self): + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) + self.stubs.Set(compute_api.API, 'start', fake_start_stop_locked_server) + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + body = dict(start="") + self.assertRaises(webob.exc.HTTPConflict, + self.controller._start_server, req, 'test_inst', body) + + def test_start_invalid_state(self): + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) + self.stubs.Set(compute_api.API, 'start', fake_start_stop_invalid_state) + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + body = dict(start="") + self.assertRaises(webob.exc.HTTPConflict, + self.controller._start_server, req, 'test_inst', body) + + def test_stop(self): + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) + self.mox.StubOutWithMock(compute_api.API, 'stop') + compute_api.API.stop(mox.IgnoreArg(), mox.IgnoreArg()) + self.mox.ReplayAll() + + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + body = dict(stop="") + self.controller._stop_server(req, 'test_inst', body) + + def test_stop_policy_failed(self): + rules = { + self.stop_policy: + common_policy.parse_rule("project_id:non_fake") + } + policy.set_rules(rules) + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + body = dict(stop="") + exc = self.assertRaises(exception.PolicyNotAuthorized, + self.controller._stop_server, + req, 'test_inst', body) + self.assertIn(self.stop_policy, exc.format_message()) + + def test_stop_not_ready(self): + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) + self.stubs.Set(compute_api.API, 'stop', fake_start_stop_not_ready) + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + body = dict(stop="") + self.assertRaises(webob.exc.HTTPConflict, + self.controller._stop_server, req, 'test_inst', body) + + def test_stop_locked_server(self): + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) + self.stubs.Set(compute_api.API, 'stop', fake_start_stop_locked_server) + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + body = dict(stop="") + self.assertRaises(webob.exc.HTTPConflict, + self.controller._stop_server, req, 'test_inst', body) + + def test_stop_invalid_state(self): + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) + self.stubs.Set(compute_api.API, 'stop', fake_start_stop_invalid_state) + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + body = dict(start="") + self.assertRaises(webob.exc.HTTPConflict, + self.controller._stop_server, req, 'test_inst', body) + + def test_start_with_bogus_id(self): + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + body = dict(start="") + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._start_server, req, 'test_inst', body) + + def test_stop_with_bogus_id(self): + req = fakes.HTTPRequest.blank('/v2/fake/servers/test_inst/action') + body = dict(stop="") + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._stop_server, req, 'test_inst', body) + + +class ServerStartStopTestV2(ServerStartStopTestV21): + start_policy = "compute:start" + stop_policy = "compute:stop" + + def _setup_controller(self): + self.controller = server_v2.ServerStartStopActionController() diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_server_usage.py b/nova/tests/unit/api/openstack/compute/contrib/test_server_usage.py new file mode 100644 index 0000000000..ee0d9a0ef4 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_server_usage.py @@ -0,0 +1,159 @@ +# Copyright 2013 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 datetime + +from lxml import etree +from oslo.serialization import jsonutils +from oslo.utils import timeutils + +from nova.api.openstack.compute.contrib import server_usage +from nova import compute +from nova import db +from nova import exception +from nova import objects +from nova.objects import instance as instance_obj +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + +UUID1 = '00000000-0000-0000-0000-000000000001' +UUID2 = '00000000-0000-0000-0000-000000000002' +UUID3 = '00000000-0000-0000-0000-000000000003' + +DATE1 = datetime.datetime(year=2013, month=4, day=5, hour=12) +DATE2 = datetime.datetime(year=2013, month=4, day=5, hour=13) +DATE3 = datetime.datetime(year=2013, month=4, day=5, hour=14) + + +def fake_compute_get(*args, **kwargs): + inst = fakes.stub_instance(1, uuid=UUID3, launched_at=DATE1, + terminated_at=DATE2) + return fake_instance.fake_instance_obj(args[1], **inst) + + +def fake_compute_get_all(*args, **kwargs): + db_list = [ + fakes.stub_instance(2, uuid=UUID1, launched_at=DATE2, + terminated_at=DATE3), + fakes.stub_instance(3, uuid=UUID2, launched_at=DATE1, + terminated_at=DATE3), + ] + fields = instance_obj.INSTANCE_DEFAULT_FIELDS + return instance_obj._make_instance_list(args[1], + objects.InstanceList(), + db_list, fields) + + +class ServerUsageTestV21(test.TestCase): + content_type = 'application/json' + prefix = 'OS-SRV-USG:' + _prefix = "/v2/fake" + + def setUp(self): + super(ServerUsageTestV21, self).setUp() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Server_usage']) + return_server = fakes.fake_instance_get() + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + def _make_request(self, url): + req = fakes.HTTPRequest.blank(url) + req.accept = self.content_type + res = req.get_response(self._get_app()) + return res + + def _get_app(self): + return fakes.wsgi_app_v21(init_only=('servers', 'os-server-usage')) + + def _get_server(self, body): + return jsonutils.loads(body).get('server') + + def _get_servers(self, body): + return jsonutils.loads(body).get('servers') + + def assertServerUsage(self, server, launched_at, terminated_at): + resp_launched_at = timeutils.parse_isotime( + server.get('%slaunched_at' % self.prefix)) + self.assertEqual(timeutils.normalize_time(resp_launched_at), + launched_at) + resp_terminated_at = timeutils.parse_isotime( + server.get('%sterminated_at' % self.prefix)) + self.assertEqual(timeutils.normalize_time(resp_terminated_at), + terminated_at) + + def test_show(self): + url = self._prefix + ('/servers/%s' % UUID3) + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + now = timeutils.utcnow() + timeutils.set_time_override(now) + self.assertServerUsage(self._get_server(res.body), + launched_at=DATE1, + terminated_at=DATE2) + + def test_detail(self): + url = self._prefix + '/servers/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + servers = self._get_servers(res.body) + self.assertServerUsage(servers[0], + launched_at=DATE2, + terminated_at=DATE3) + self.assertServerUsage(servers[1], + launched_at=DATE1, + terminated_at=DATE3) + + def test_no_instance_passthrough_404(self): + + def fake_compute_get(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + url = self._prefix + '/servers/70f6db34-de8d-4fbd-aafb-4065bdfa6115' + res = self._make_request(url) + + self.assertEqual(res.status_int, 404) + + +class ServerUsageTestV20(ServerUsageTestV21): + + def setUp(self): + super(ServerUsageTestV20, self).setUp() + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Server_usage']) + + def _get_app(self): + return fakes.wsgi_app(init_only=('servers',)) + + +class ServerUsageXmlTest(ServerUsageTestV20): + content_type = 'application/xml' + prefix = '{%s}' % server_usage.Server_usage.namespace + + def _get_server(self, body): + return etree.XML(body) + + def _get_servers(self, body): + return etree.XML(body).getchildren() diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_services.py b/nova/tests/unit/api/openstack/compute/contrib/test_services.py new file mode 100644 index 0000000000..87297c567b --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_services.py @@ -0,0 +1,576 @@ +# Copyright 2012 IBM Corp. +# +# 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 calendar +import datetime + +import iso8601 +import mock +from oslo.utils import timeutils +import webob.exc + +from nova.api.openstack.compute.contrib import services +from nova.api.openstack import extensions +from nova import availability_zones +from nova.compute import cells_api +from nova import context +from nova import db +from nova import exception +from nova.servicegroup.drivers import db as db_driver +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit.objects import test_service + + +fake_services_list = [ + dict(test_service.fake_service, + binary='nova-scheduler', + host='host1', + id=1, + disabled=True, + topic='scheduler', + updated_at=datetime.datetime(2012, 10, 29, 13, 42, 2), + created_at=datetime.datetime(2012, 9, 18, 2, 46, 27), + disabled_reason='test1'), + dict(test_service.fake_service, + binary='nova-compute', + host='host1', + id=2, + disabled=True, + topic='compute', + updated_at=datetime.datetime(2012, 10, 29, 13, 42, 5), + created_at=datetime.datetime(2012, 9, 18, 2, 46, 27), + disabled_reason='test2'), + dict(test_service.fake_service, + binary='nova-scheduler', + host='host2', + id=3, + disabled=False, + topic='scheduler', + updated_at=datetime.datetime(2012, 9, 19, 6, 55, 34), + created_at=datetime.datetime(2012, 9, 18, 2, 46, 28), + disabled_reason=None), + dict(test_service.fake_service, + binary='nova-compute', + host='host2', + id=4, + disabled=True, + topic='compute', + updated_at=datetime.datetime(2012, 9, 18, 8, 3, 38), + created_at=datetime.datetime(2012, 9, 18, 2, 46, 28), + disabled_reason='test4'), + ] + + +class FakeRequest(object): + environ = {"nova.context": context.get_admin_context()} + GET = {} + + +class FakeRequestWithService(object): + environ = {"nova.context": context.get_admin_context()} + GET = {"binary": "nova-compute"} + + +class FakeRequestWithHost(object): + environ = {"nova.context": context.get_admin_context()} + GET = {"host": "host1"} + + +class FakeRequestWithHostService(object): + environ = {"nova.context": context.get_admin_context()} + GET = {"host": "host1", "binary": "nova-compute"} + + +def fake_service_get_all(services): + def service_get_all(context, filters=None, set_zones=False): + if set_zones or 'availability_zone' in filters: + return availability_zones.set_availability_zones(context, + services) + return services + return service_get_all + + +def fake_db_api_service_get_all(context, disabled=None): + return fake_services_list + + +def fake_db_service_get_by_host_binary(services): + def service_get_by_host_binary(context, host, binary): + for service in services: + if service['host'] == host and service['binary'] == binary: + return service + raise exception.HostBinaryNotFound(host=host, binary=binary) + return service_get_by_host_binary + + +def fake_service_get_by_host_binary(context, host, binary): + fake = fake_db_service_get_by_host_binary(fake_services_list) + return fake(context, host, binary) + + +def _service_get_by_id(services, value): + for service in services: + if service['id'] == value: + return service + return None + + +def fake_db_service_update(services): + def service_update(context, service_id, values): + service = _service_get_by_id(services, service_id) + if service is None: + raise exception.ServiceNotFound(service_id=service_id) + return service + return service_update + + +def fake_service_update(context, service_id, values): + fake = fake_db_service_update(fake_services_list) + return fake(context, service_id, values) + + +def fake_utcnow(): + return datetime.datetime(2012, 10, 29, 13, 42, 11) + + +fake_utcnow.override_time = None + + +def fake_utcnow_ts(): + d = fake_utcnow() + return calendar.timegm(d.utctimetuple()) + + +class ServicesTest(test.TestCase): + + def setUp(self): + super(ServicesTest, self).setUp() + + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = services.ServiceController(self.ext_mgr) + + self.stubs.Set(timeutils, "utcnow", fake_utcnow) + self.stubs.Set(timeutils, "utcnow_ts", fake_utcnow_ts) + + self.stubs.Set(self.controller.host_api, "service_get_all", + fake_service_get_all(fake_services_list)) + + self.stubs.Set(db, "service_get_by_args", + fake_db_service_get_by_host_binary(fake_services_list)) + self.stubs.Set(db, "service_update", + fake_db_service_update(fake_services_list)) + + def test_services_list(self): + req = FakeRequest() + res_dict = self.controller.index(req) + + response = {'services': [ + {'binary': 'nova-scheduler', + 'host': 'host1', + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)}, + {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}, + {'binary': 'nova-scheduler', + 'host': 'host2', + 'zone': 'internal', + 'status': 'enabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34)}, + {'binary': 'nova-compute', + 'host': 'host2', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]} + self.assertEqual(res_dict, response) + + def test_services_list_with_host(self): + req = FakeRequestWithHost() + res_dict = self.controller.index(req) + + response = {'services': [ + {'binary': 'nova-scheduler', + 'host': 'host1', + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)}, + {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}]} + self.assertEqual(res_dict, response) + + def test_services_list_with_service(self): + req = FakeRequestWithService() + res_dict = self.controller.index(req) + + response = {'services': [ + {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}, + {'binary': 'nova-compute', + 'host': 'host2', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]} + self.assertEqual(res_dict, response) + + def test_services_list_with_host_service(self): + req = FakeRequestWithHostService() + res_dict = self.controller.index(req) + + response = {'services': [ + {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}]} + self.assertEqual(res_dict, response) + + def test_services_detail(self): + self.ext_mgr.extensions['os-extended-services'] = True + req = FakeRequest() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-scheduler', + 'host': 'host1', + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + 'disabled_reason': 'test1'}, + {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), + 'disabled_reason': 'test2'}, + {'binary': 'nova-scheduler', + 'host': 'host2', + 'zone': 'internal', + 'status': 'enabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34), + 'disabled_reason': None}, + {'binary': 'nova-compute', + 'host': 'host2', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38), + 'disabled_reason': 'test4'}]} + self.assertEqual(res_dict, response) + + def test_service_detail_with_host(self): + self.ext_mgr.extensions['os-extended-services'] = True + req = FakeRequestWithHost() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-scheduler', + 'host': 'host1', + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + 'disabled_reason': 'test1'}, + {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), + 'disabled_reason': 'test2'}]} + self.assertEqual(res_dict, response) + + def test_service_detail_with_service(self): + self.ext_mgr.extensions['os-extended-services'] = True + req = FakeRequestWithService() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), + 'disabled_reason': 'test2'}, + {'binary': 'nova-compute', + 'host': 'host2', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38), + 'disabled_reason': 'test4'}]} + self.assertEqual(res_dict, response) + + def test_service_detail_with_host_service(self): + self.ext_mgr.extensions['os-extended-services'] = True + req = FakeRequestWithHostService() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), + 'disabled_reason': 'test2'}]} + self.assertEqual(res_dict, response) + + def test_services_detail_with_delete_extension(self): + self.ext_mgr.extensions['os-extended-services-delete'] = True + req = FakeRequest() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-scheduler', + 'host': 'host1', + 'id': 1, + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2)}, + {'binary': 'nova-compute', + 'host': 'host1', + 'id': 2, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5)}, + {'binary': 'nova-scheduler', + 'host': 'host2', + 'id': 3, + 'zone': 'internal', + 'status': 'enabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34)}, + {'binary': 'nova-compute', + 'host': 'host2', + 'id': 4, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38)}]} + self.assertEqual(res_dict, response) + + def test_services_enable(self): + def _service_update(context, service_id, values): + self.assertIsNone(values['disabled_reason']) + return dict(test_service.fake_service, id=service_id, **values) + + self.stubs.Set(db, "service_update", _service_update) + + body = {'host': 'host1', 'binary': 'nova-compute'} + req = fakes.HTTPRequest.blank('/v2/fake/os-services/enable') + + res_dict = self.controller.update(req, "enable", body) + self.assertEqual(res_dict['service']['status'], 'enabled') + self.assertNotIn('disabled_reason', res_dict['service']) + + def test_services_enable_with_invalid_host(self): + body = {'host': 'invalid', 'binary': 'nova-compute'} + req = fakes.HTTPRequest.blank('/v2/fake/os-services/enable') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, + req, + "enable", + body) + + def test_services_enable_with_invalid_binary(self): + body = {'host': 'host1', 'binary': 'invalid'} + req = fakes.HTTPRequest.blank('/v2/fake/os-services/enable') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, + req, + "enable", + body) + + # This test is just to verify that the servicegroup API gets used when + # calling this API. + def test_services_with_exception(self): + def dummy_is_up(self, dummy): + raise KeyError() + + self.stubs.Set(db_driver.DbDriver, 'is_up', dummy_is_up) + req = FakeRequestWithHostService() + self.assertRaises(KeyError, self.controller.index, req) + + def test_services_disable(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-services/disable') + body = {'host': 'host1', 'binary': 'nova-compute'} + res_dict = self.controller.update(req, "disable", body) + + self.assertEqual(res_dict['service']['status'], 'disabled') + self.assertNotIn('disabled_reason', res_dict['service']) + + def test_services_disable_with_invalid_host(self): + body = {'host': 'invalid', 'binary': 'nova-compute'} + req = fakes.HTTPRequest.blank('/v2/fake/os-services/disable') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, + req, + "disable", + body) + + def test_services_disable_with_invalid_binary(self): + body = {'host': 'host1', 'binary': 'invalid'} + req = fakes.HTTPRequestV3.blank('/v2/fake/os-services/disable') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, + req, + "disable", + body) + + def test_services_disable_log_reason(self): + self.ext_mgr.extensions['os-extended-services'] = True + req = \ + fakes.HTTPRequest.blank('v2/fakes/os-services/disable-log-reason') + body = {'host': 'host1', + 'binary': 'nova-compute', + 'disabled_reason': 'test-reason', + } + res_dict = self.controller.update(req, "disable-log-reason", body) + + self.assertEqual(res_dict['service']['status'], 'disabled') + self.assertEqual(res_dict['service']['disabled_reason'], 'test-reason') + + def test_mandatory_reason_field(self): + self.ext_mgr.extensions['os-extended-services'] = True + req = \ + fakes.HTTPRequest.blank('v2/fakes/os-services/disable-log-reason') + body = {'host': 'host1', + 'binary': 'nova-compute', + } + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, "disable-log-reason", body) + + def test_invalid_reason_field(self): + reason = ' ' + self.assertFalse(self.controller._is_valid_as_reason(reason)) + reason = 'a' * 256 + self.assertFalse(self.controller._is_valid_as_reason(reason)) + reason = 'it\'s a valid reason.' + self.assertTrue(self.controller._is_valid_as_reason(reason)) + + def test_services_delete(self): + self.ext_mgr.extensions['os-extended-services-delete'] = True + + request = fakes.HTTPRequest.blank('/v2/fakes/os-services/1', + use_admin_context=True) + request.method = 'DELETE' + + with mock.patch.object(self.controller.host_api, + 'service_delete') as service_delete: + self.controller.delete(request, '1') + service_delete.assert_called_once_with( + request.environ['nova.context'], '1') + self.assertEqual(self.controller.delete.wsgi_code, 204) + + def test_services_delete_not_found(self): + self.ext_mgr.extensions['os-extended-services-delete'] = True + + request = fakes.HTTPRequest.blank('/v2/fakes/os-services/abc', + use_admin_context=True) + request.method = 'DELETE' + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, request, 'abc') + + def test_services_delete_not_enabled(self): + request = fakes.HTTPRequest.blank('/v2/fakes/os-services/300', + use_admin_context=True) + request.method = 'DELETE' + self.assertRaises(webob.exc.HTTPMethodNotAllowed, + self.controller.delete, request, '300') + + +class ServicesCellsTest(test.TestCase): + def setUp(self): + super(ServicesCellsTest, self).setUp() + + host_api = cells_api.HostAPI() + + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = services.ServiceController(self.ext_mgr) + self.controller.host_api = host_api + + self.stubs.Set(timeutils, "utcnow", fake_utcnow) + self.stubs.Set(timeutils, "utcnow_ts", fake_utcnow_ts) + + services_list = [] + for service in fake_services_list: + service = service.copy() + service['id'] = 'cell1@%d' % service['id'] + services_list.append(service) + + self.stubs.Set(host_api.cells_rpcapi, "service_get_all", + fake_service_get_all(services_list)) + + def test_services_detail(self): + self.ext_mgr.extensions['os-extended-services-delete'] = True + req = FakeRequest() + res_dict = self.controller.index(req) + utc = iso8601.iso8601.Utc() + response = {'services': [ + {'id': 'cell1@1', + 'binary': 'nova-scheduler', + 'host': 'host1', + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2, + tzinfo=utc)}, + {'id': 'cell1@2', + 'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5, + tzinfo=utc)}, + {'id': 'cell1@3', + 'binary': 'nova-scheduler', + 'host': 'host2', + 'zone': 'internal', + 'status': 'enabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34, + tzinfo=utc)}, + {'id': 'cell1@4', + 'binary': 'nova-compute', + 'host': 'host2', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38, + tzinfo=utc)}]} + self.assertEqual(res_dict, response) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_shelve.py b/nova/tests/unit/api/openstack/compute/contrib/test_shelve.py new file mode 100644 index 0000000000..df1c6fc449 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_shelve.py @@ -0,0 +1,148 @@ +# 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 uuid + +import webob + +from nova.api.openstack.compute.contrib import shelve as shelve_v2 +from nova.api.openstack.compute.plugins.v3 import shelve as shelve_v21 +from nova.compute import api as compute_api +from nova import db +from nova import exception +from nova.openstack.common import policy as common_policy +from nova import policy +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + + +def fake_instance_get_by_uuid(context, instance_id, + columns_to_join=None, use_slave=False): + return fake_instance.fake_db_instance( + **{'name': 'fake', 'project_id': '%s_unequal' % context.project_id}) + + +def fake_auth_context(context): + return True + + +class ShelvePolicyTestV21(test.NoDBTestCase): + plugin = shelve_v21 + prefix = 'v3:os-shelve:' + offload = 'shelve_offload' + + def setUp(self): + super(ShelvePolicyTestV21, self).setUp() + self.controller = self.plugin.ShelveController() + + def _fake_request(self): + return fakes.HTTPRequestV3.blank('/servers/12/os-shelve') + + def test_shelve_restricted_by_role(self): + rules = {'compute_extension:%sshelve' % self.prefix: + common_policy.parse_rule('role:admin')} + policy.set_rules(rules) + + req = self._fake_request() + self.assertRaises(exception.Forbidden, self.controller._shelve, + req, str(uuid.uuid4()), {}) + + def test_shelve_allowed(self): + rules = {'compute:get': common_policy.parse_rule(''), + 'compute_extension:%sshelve' % self.prefix: + common_policy.parse_rule('')} + policy.set_rules(rules) + + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + req = self._fake_request() + self.assertRaises(exception.Forbidden, self.controller._shelve, + req, str(uuid.uuid4()), {}) + + def test_shelve_locked_server(self): + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + self.stubs.Set(self.plugin, 'auth_shelve', fake_auth_context) + self.stubs.Set(compute_api.API, 'shelve', + fakes.fake_actions_to_locked_server) + req = self._fake_request() + self.assertRaises(webob.exc.HTTPConflict, self.controller._shelve, + req, str(uuid.uuid4()), {}) + + def test_unshelve_restricted_by_role(self): + rules = {'compute_extension:%sunshelve' % self.prefix: + common_policy.parse_rule('role:admin')} + policy.set_rules(rules) + + req = self._fake_request() + self.assertRaises(exception.Forbidden, self.controller._unshelve, + req, str(uuid.uuid4()), {}) + + def test_unshelve_allowed(self): + rules = {'compute:get': common_policy.parse_rule(''), + 'compute_extension:%sunshelve' % self.prefix: + common_policy.parse_rule('')} + policy.set_rules(rules) + + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + req = self._fake_request() + self.assertRaises(exception.Forbidden, self.controller._unshelve, + req, str(uuid.uuid4()), {}) + + def test_unshelve_locked_server(self): + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + self.stubs.Set(self.plugin, 'auth_unshelve', fake_auth_context) + self.stubs.Set(compute_api.API, 'unshelve', + fakes.fake_actions_to_locked_server) + req = self._fake_request() + self.assertRaises(webob.exc.HTTPConflict, self.controller._unshelve, + req, str(uuid.uuid4()), {}) + + def test_shelve_offload_restricted_by_role(self): + rules = {'compute_extension:%s%s' % (self.prefix, self.offload): + common_policy.parse_rule('role:admin')} + policy.set_rules(rules) + + req = self._fake_request() + self.assertRaises(exception.Forbidden, + self.controller._shelve_offload, req, str(uuid.uuid4()), {}) + + def test_shelve_offload_allowed(self): + rules = {'compute:get': common_policy.parse_rule(''), + 'compute_extension:%s%s' % (self.prefix, self.offload): + common_policy.parse_rule('')} + policy.set_rules(rules) + + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + req = self._fake_request() + self.assertRaises(exception.Forbidden, + self.controller._shelve_offload, req, str(uuid.uuid4()), {}) + + def test_shelve_offload_locked_server(self): + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + self.stubs.Set(self.plugin, 'auth_shelve_offload', fake_auth_context) + self.stubs.Set(compute_api.API, 'shelve_offload', + fakes.fake_actions_to_locked_server) + req = self._fake_request() + self.assertRaises(webob.exc.HTTPConflict, + self.controller._shelve_offload, + req, str(uuid.uuid4()), {}) + + +class ShelvePolicyTestV2(ShelvePolicyTestV21): + plugin = shelve_v2 + prefix = '' + offload = 'shelveOffload' + + def _fake_request(self): + return fakes.HTTPRequest.blank('/v2/123/servers/12/os-shelve') diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_simple_tenant_usage.py b/nova/tests/unit/api/openstack/compute/contrib/test_simple_tenant_usage.py new file mode 100644 index 0000000000..9639b886ae --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_simple_tenant_usage.py @@ -0,0 +1,539 @@ +# Copyright 2011 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 datetime + +from lxml import etree +import mock +from oslo.serialization import jsonutils +from oslo.utils import timeutils +import webob + +from nova.api.openstack.compute.contrib import simple_tenant_usage as \ + simple_tenant_usage_v2 +from nova.api.openstack.compute.plugins.v3 import simple_tenant_usage as \ + simple_tenant_usage_v21 +from nova.compute import flavors +from nova.compute import vm_states +from nova import context +from nova import db +from nova import exception +from nova import objects +from nova.openstack.common import policy as common_policy +from nova import policy +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova import utils + +SERVERS = 5 +TENANTS = 2 +HOURS = 24 +ROOT_GB = 10 +EPHEMERAL_GB = 20 +MEMORY_MB = 1024 +VCPUS = 2 +NOW = timeutils.utcnow() +START = NOW - datetime.timedelta(hours=HOURS) +STOP = NOW + + +FAKE_INST_TYPE = {'id': 1, + 'vcpus': VCPUS, + 'root_gb': ROOT_GB, + 'ephemeral_gb': EPHEMERAL_GB, + 'memory_mb': MEMORY_MB, + 'name': 'fakeflavor', + 'flavorid': 'foo', + 'rxtx_factor': 1.0, + 'vcpu_weight': 1, + 'swap': 0, + 'created_at': None, + 'updated_at': None, + 'deleted_at': None, + 'deleted': 0, + 'disabled': False, + 'is_public': True, + 'extra_specs': {'foo': 'bar'}} + + +def get_fake_db_instance(start, end, instance_id, tenant_id, + vm_state=vm_states.ACTIVE): + sys_meta = utils.dict_to_metadata( + flavors.save_flavor_info({}, FAKE_INST_TYPE)) + # NOTE(mriedem): We use fakes.stub_instance since it sets the fields + # needed on the db instance for converting it to an object, but we still + # need to override system_metadata to use our fake flavor. + inst = fakes.stub_instance( + id=instance_id, + uuid='00000000-0000-0000-0000-00000000000000%02d' % instance_id, + image_ref='1', + project_id=tenant_id, + user_id='fakeuser', + display_name='name', + flavor_id=FAKE_INST_TYPE['id'], + launched_at=start, + terminated_at=end, + vm_state=vm_state, + memory_mb=MEMORY_MB, + vcpus=VCPUS, + root_gb=ROOT_GB, + ephemeral_gb=EPHEMERAL_GB,) + inst['system_metadata'] = sys_meta + return inst + + +def fake_instance_get_active_by_window_joined(context, begin, end, + project_id, host): + return [get_fake_db_instance(START, + STOP, + x, + "faketenant_%s" % (x / SERVERS)) + for x in xrange(TENANTS * SERVERS)] + + +@mock.patch.object(db, 'instance_get_active_by_window_joined', + fake_instance_get_active_by_window_joined) +class SimpleTenantUsageTestV21(test.TestCase): + url = '/v2/faketenant_0/os-simple-tenant-usage' + alt_url = '/v2/faketenant_1/os-simple-tenant-usage' + policy_rule_prefix = "compute_extension:v3:os-simple-tenant-usage" + + def setUp(self): + super(SimpleTenantUsageTestV21, self).setUp() + self.admin_context = context.RequestContext('fakeadmin_0', + 'faketenant_0', + is_admin=True) + self.user_context = context.RequestContext('fakeadmin_0', + 'faketenant_0', + is_admin=False) + self.alt_user_context = context.RequestContext('fakeadmin_0', + 'faketenant_1', + is_admin=False) + + def _get_wsgi_app(self, context): + return fakes.wsgi_app_v21(fake_auth_context=context, + init_only=('servers', + 'os-simple-tenant-usage')) + + def _test_verify_index(self, start, stop): + req = webob.Request.blank( + self.url + '?start=%s&end=%s' % + (start.isoformat(), stop.isoformat())) + req.method = "GET" + req.headers["content-type"] = "application/json" + + res = req.get_response(self._get_wsgi_app(self.admin_context)) + + self.assertEqual(res.status_int, 200) + res_dict = jsonutils.loads(res.body) + usages = res_dict['tenant_usages'] + for i in xrange(TENANTS): + self.assertEqual(int(usages[i]['total_hours']), + SERVERS * HOURS) + self.assertEqual(int(usages[i]['total_local_gb_usage']), + SERVERS * (ROOT_GB + EPHEMERAL_GB) * HOURS) + self.assertEqual(int(usages[i]['total_memory_mb_usage']), + SERVERS * MEMORY_MB * HOURS) + self.assertEqual(int(usages[i]['total_vcpus_usage']), + SERVERS * VCPUS * HOURS) + self.assertFalse(usages[i].get('server_usages')) + + def test_verify_index(self): + self._test_verify_index(START, STOP) + + def test_verify_index_future_end_time(self): + future = NOW + datetime.timedelta(hours=HOURS) + self._test_verify_index(START, future) + + def test_verify_show(self): + self._test_verify_show(START, STOP) + + def test_verify_show_future_end_time(self): + future = NOW + datetime.timedelta(hours=HOURS) + self._test_verify_show(START, future) + + def _get_tenant_usages(self, detailed=''): + req = webob.Request.blank( + self.url + '?detailed=%s&start=%s&end=%s' % + (detailed, START.isoformat(), STOP.isoformat())) + req.method = "GET" + req.headers["content-type"] = "application/json" + + res = req.get_response(self._get_wsgi_app(self.admin_context)) + self.assertEqual(res.status_int, 200) + res_dict = jsonutils.loads(res.body) + return res_dict['tenant_usages'] + + def test_verify_detailed_index(self): + usages = self._get_tenant_usages('1') + for i in xrange(TENANTS): + servers = usages[i]['server_usages'] + for j in xrange(SERVERS): + self.assertEqual(int(servers[j]['hours']), HOURS) + + def test_verify_simple_index(self): + usages = self._get_tenant_usages(detailed='0') + for i in xrange(TENANTS): + self.assertIsNone(usages[i].get('server_usages')) + + def test_verify_simple_index_empty_param(self): + # NOTE(lzyeval): 'detailed=&start=..&end=..' + usages = self._get_tenant_usages() + for i in xrange(TENANTS): + self.assertIsNone(usages[i].get('server_usages')) + + def _test_verify_show(self, start, stop): + tenant_id = 0 + req = webob.Request.blank( + self.url + '/faketenant_%s?start=%s&end=%s' % + (tenant_id, start.isoformat(), stop.isoformat())) + req.method = "GET" + req.headers["content-type"] = "application/json" + + res = req.get_response(self._get_wsgi_app(self.user_context)) + self.assertEqual(res.status_int, 200) + res_dict = jsonutils.loads(res.body) + + usage = res_dict['tenant_usage'] + servers = usage['server_usages'] + self.assertEqual(len(usage['server_usages']), SERVERS) + uuids = ['00000000-0000-0000-0000-00000000000000%02d' % + (x + (tenant_id * SERVERS)) for x in xrange(SERVERS)] + for j in xrange(SERVERS): + delta = STOP - START + uptime = delta.days * 24 * 3600 + delta.seconds + self.assertEqual(int(servers[j]['uptime']), uptime) + self.assertEqual(int(servers[j]['hours']), HOURS) + self.assertIn(servers[j]['instance_id'], uuids) + + def test_verify_show_cannot_view_other_tenant(self): + req = webob.Request.blank( + self.alt_url + '/faketenant_0?start=%s&end=%s' % + (START.isoformat(), STOP.isoformat())) + req.method = "GET" + req.headers["content-type"] = "application/json" + + rules = { + self.policy_rule_prefix + ":show": + common_policy.parse_rule([ + ["role:admin"], ["project_id:%(project_id)s"] + ]) + } + policy.set_rules(rules) + + try: + res = req.get_response(self._get_wsgi_app(self.alt_user_context)) + self.assertEqual(res.status_int, 403) + finally: + policy.reset() + + def test_get_tenants_usage_with_bad_start_date(self): + future = NOW + datetime.timedelta(hours=HOURS) + tenant_id = 0 + req = webob.Request.blank( + self.url + '/' + 'faketenant_%s?start=%s&end=%s' % + (tenant_id, future.isoformat(), NOW.isoformat())) + req.method = "GET" + req.headers["content-type"] = "application/json" + + res = req.get_response(self._get_wsgi_app(self.user_context)) + self.assertEqual(res.status_int, 400) + + def test_get_tenants_usage_with_invalid_start_date(self): + tenant_id = 0 + req = webob.Request.blank( + self.url + '/' + 'faketenant_%s?start=%s&end=%s' % + (tenant_id, "xxxx", NOW.isoformat())) + req.method = "GET" + req.headers["content-type"] = "application/json" + + res = req.get_response(self._get_wsgi_app(self.user_context)) + self.assertEqual(res.status_int, 400) + + def _test_get_tenants_usage_with_one_date(self, date_url_param): + req = webob.Request.blank( + self.url + '/' + 'faketenant_0?%s' % date_url_param) + req.method = "GET" + req.headers["content-type"] = "application/json" + res = req.get_response(self._get_wsgi_app(self.user_context)) + self.assertEqual(200, res.status_int) + + def test_get_tenants_usage_with_no_start_date(self): + self._test_get_tenants_usage_with_one_date( + 'end=%s' % (NOW + datetime.timedelta(5)).isoformat()) + + def test_get_tenants_usage_with_no_end_date(self): + self._test_get_tenants_usage_with_one_date( + 'start=%s' % (NOW - datetime.timedelta(5)).isoformat()) + + +class SimpleTenantUsageTestV2(SimpleTenantUsageTestV21): + policy_rule_prefix = "compute_extension:simple_tenant_usage" + + def _get_wsgi_app(self, context): + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Simple_tenant_usage']) + return fakes.wsgi_app(fake_auth_context=context, + init_only=('os-simple-tenant-usage', )) + + +class SimpleTenantUsageSerializerTest(test.TestCase): + def _verify_server_usage(self, raw_usage, tree): + self.assertEqual('server_usage', tree.tag) + + # Figure out what fields we expect + not_seen = set(raw_usage.keys()) + + for child in tree: + self.assertIn(child.tag, not_seen) + not_seen.remove(child.tag) + self.assertEqual(str(raw_usage[child.tag]), child.text) + + self.assertEqual(len(not_seen), 0) + + def _verify_tenant_usage(self, raw_usage, tree): + self.assertEqual('tenant_usage', tree.tag) + + # Figure out what fields we expect + not_seen = set(raw_usage.keys()) + + for child in tree: + self.assertIn(child.tag, not_seen) + not_seen.remove(child.tag) + if child.tag == 'server_usages': + for idx, gr_child in enumerate(child): + self._verify_server_usage(raw_usage['server_usages'][idx], + gr_child) + else: + self.assertEqual(str(raw_usage[child.tag]), child.text) + + self.assertEqual(len(not_seen), 0) + + def test_serializer_show(self): + serializer = simple_tenant_usage_v2.SimpleTenantUsageTemplate() + today = timeutils.utcnow() + yesterday = today - datetime.timedelta(days=1) + raw_usage = dict( + tenant_id='tenant', + total_local_gb_usage=789, + total_vcpus_usage=456, + total_memory_mb_usage=123, + total_hours=24, + start=yesterday, + stop=today, + server_usages=[dict( + instance_id='00000000-0000-0000-0000-0000000000000000', + name='test', + hours=24, + memory_mb=1024, + local_gb=50, + vcpus=1, + tenant_id='tenant', + flavor='m1.small', + started_at=yesterday, + ended_at=today, + state='terminated', + uptime=86400), + dict( + instance_id='00000000-0000-0000-0000-0000000000000002', + name='test2', + hours=12, + memory_mb=512, + local_gb=25, + vcpus=2, + tenant_id='tenant', + flavor='m1.tiny', + started_at=yesterday, + ended_at=today, + state='terminated', + uptime=43200), + ], + ) + tenant_usage = dict(tenant_usage=raw_usage) + text = serializer.serialize(tenant_usage) + + tree = etree.fromstring(text) + + self._verify_tenant_usage(raw_usage, tree) + + def test_serializer_index(self): + serializer = simple_tenant_usage_v2.SimpleTenantUsagesTemplate() + today = timeutils.utcnow() + yesterday = today - datetime.timedelta(days=1) + raw_usages = [dict( + tenant_id='tenant1', + total_local_gb_usage=1024, + total_vcpus_usage=23, + total_memory_mb_usage=512, + total_hours=24, + start=yesterday, + stop=today, + server_usages=[dict( + instance_id='00000000-0000-0000-0000-0000000000000001', + name='test1', + hours=24, + memory_mb=1024, + local_gb=50, + vcpus=2, + tenant_id='tenant1', + flavor='m1.small', + started_at=yesterday, + ended_at=today, + state='terminated', + uptime=86400), + dict( + instance_id='00000000-0000-0000-0000-0000000000000002', + name='test2', + hours=42, + memory_mb=4201, + local_gb=25, + vcpus=1, + tenant_id='tenant1', + flavor='m1.tiny', + started_at=today, + ended_at=yesterday, + state='terminated', + uptime=43200), + ], + ), + dict( + tenant_id='tenant2', + total_local_gb_usage=512, + total_vcpus_usage=32, + total_memory_mb_usage=1024, + total_hours=42, + start=today, + stop=yesterday, + server_usages=[dict( + instance_id='00000000-0000-0000-0000-0000000000000003', + name='test3', + hours=24, + memory_mb=1024, + local_gb=50, + vcpus=2, + tenant_id='tenant2', + flavor='m1.small', + started_at=yesterday, + ended_at=today, + state='terminated', + uptime=86400), + dict( + instance_id='00000000-0000-0000-0000-0000000000000002', + name='test2', + hours=42, + memory_mb=4201, + local_gb=25, + vcpus=1, + tenant_id='tenant4', + flavor='m1.tiny', + started_at=today, + ended_at=yesterday, + state='terminated', + uptime=43200), + ], + ), + ] + tenant_usages = dict(tenant_usages=raw_usages) + text = serializer.serialize(tenant_usages) + + tree = etree.fromstring(text) + + self.assertEqual('tenant_usages', tree.tag) + self.assertEqual(len(raw_usages), len(tree)) + for idx, child in enumerate(tree): + self._verify_tenant_usage(raw_usages[idx], child) + + +class SimpleTenantUsageControllerTestV21(test.TestCase): + controller = simple_tenant_usage_v21.SimpleTenantUsageController() + + def setUp(self): + super(SimpleTenantUsageControllerTestV21, self).setUp() + + self.context = context.RequestContext('fakeuser', 'fake-project') + + self.baseinst = get_fake_db_instance(START, STOP, instance_id=1, + tenant_id=self.context.project_id, + vm_state=vm_states.DELETED) + # convert the fake instance dict to an object + self.inst_obj = objects.Instance._from_db_object( + self.context, objects.Instance(), self.baseinst) + + def test_get_flavor_from_sys_meta(self): + # Non-deleted instances get their type information from their + # system_metadata + with mock.patch.object(db, 'instance_get_by_uuid', + return_value=self.baseinst): + flavor = self.controller._get_flavor(self.context, + self.inst_obj, {}) + self.assertEqual(objects.Flavor, type(flavor)) + self.assertEqual(FAKE_INST_TYPE['id'], flavor.id) + + def test_get_flavor_from_non_deleted_with_id_fails(self): + # If an instance is not deleted and missing type information from + # system_metadata, then that's a bug + self.inst_obj.system_metadata = {} + self.assertRaises(KeyError, + self.controller._get_flavor, self.context, + self.inst_obj, {}) + + def test_get_flavor_from_deleted_with_id(self): + # Deleted instances may not have type info in system_metadata, + # so verify that they get their type from a lookup of their + # instance_type_id + self.inst_obj.system_metadata = {} + self.inst_obj.deleted = 1 + flavor = self.controller._get_flavor(self.context, self.inst_obj, {}) + self.assertEqual(objects.Flavor, type(flavor)) + self.assertEqual(FAKE_INST_TYPE['id'], flavor.id) + + def test_get_flavor_from_deleted_with_id_of_deleted(self): + # Verify the legacy behavior of instance_type_id pointing to a + # missing type being non-fatal + self.inst_obj.system_metadata = {} + self.inst_obj.deleted = 1 + self.inst_obj.instance_type_id = 99 + flavor = self.controller._get_flavor(self.context, self.inst_obj, {}) + self.assertIsNone(flavor) + + +class SimpleTenantUsageControllerTestV2(SimpleTenantUsageControllerTestV21): + controller = simple_tenant_usage_v2.SimpleTenantUsageController() + + +class SimpleTenantUsageUtilsV21(test.NoDBTestCase): + simple_tenant_usage = simple_tenant_usage_v21 + + def test_valid_string(self): + dt = self.simple_tenant_usage.parse_strtime( + "2014-02-21T13:47:20.824060", "%Y-%m-%dT%H:%M:%S.%f") + self.assertEqual(datetime.datetime( + microsecond=824060, second=20, minute=47, hour=13, + day=21, month=2, year=2014), dt) + + def test_invalid_string(self): + self.assertRaises(exception.InvalidStrTime, + self.simple_tenant_usage.parse_strtime, + "2014-02-21 13:47:20.824060", + "%Y-%m-%dT%H:%M:%S.%f") + + +class SimpleTenantUsageUtilsV2(SimpleTenantUsageUtilsV21): + simple_tenant_usage = simple_tenant_usage_v2 diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_snapshots.py b/nova/tests/unit/api/openstack/compute/contrib/test_snapshots.py new file mode 100644 index 0000000000..74bb1948e6 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_snapshots.py @@ -0,0 +1,209 @@ +# Copyright 2011 Denali Systems, Inc. +# 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. + +from lxml import etree +from oslo.serialization import jsonutils +from oslo.utils import timeutils +import webob + +from nova.api.openstack.compute.contrib import volumes +from nova import context +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.volume import cinder + + +class SnapshotApiTest(test.NoDBTestCase): + def setUp(self): + super(SnapshotApiTest, self).setUp() + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + self.stubs.Set(cinder.API, "create_snapshot", + fakes.stub_snapshot_create) + self.stubs.Set(cinder.API, "create_snapshot_force", + fakes.stub_snapshot_create) + self.stubs.Set(cinder.API, "delete_snapshot", + fakes.stub_snapshot_delete) + self.stubs.Set(cinder.API, "get_snapshot", fakes.stub_snapshot_get) + self.stubs.Set(cinder.API, "get_all_snapshots", + fakes.stub_snapshot_get_all) + self.stubs.Set(cinder.API, "get", fakes.stub_volume_get) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Volumes']) + + self.context = context.get_admin_context() + self.app = fakes.wsgi_app(init_only=('os-snapshots',)) + + def test_snapshot_create(self): + snapshot = {"volume_id": 12, + "force": False, + "display_name": "Snapshot Test Name", + "display_description": "Snapshot Test Desc"} + body = dict(snapshot=snapshot) + req = webob.Request.blank('/v2/fake/os-snapshots') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 200) + resp_dict = jsonutils.loads(resp.body) + self.assertIn('snapshot', resp_dict) + self.assertEqual(resp_dict['snapshot']['displayName'], + snapshot['display_name']) + self.assertEqual(resp_dict['snapshot']['displayDescription'], + snapshot['display_description']) + self.assertEqual(resp_dict['snapshot']['volumeId'], + snapshot['volume_id']) + + def test_snapshot_create_force(self): + snapshot = {"volume_id": 12, + "force": True, + "display_name": "Snapshot Test Name", + "display_description": "Snapshot Test Desc"} + body = dict(snapshot=snapshot) + req = webob.Request.blank('/v2/fake/os-snapshots') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 200) + + resp_dict = jsonutils.loads(resp.body) + self.assertIn('snapshot', resp_dict) + self.assertEqual(resp_dict['snapshot']['displayName'], + snapshot['display_name']) + self.assertEqual(resp_dict['snapshot']['displayDescription'], + snapshot['display_description']) + self.assertEqual(resp_dict['snapshot']['volumeId'], + snapshot['volume_id']) + + # Test invalid force paramter + snapshot = {"volume_id": 12, + "force": '**&&^^%%$$##@@'} + body = dict(snapshot=snapshot) + req = webob.Request.blank('/v2/fake/os-snapshots') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 400) + + def test_snapshot_delete(self): + snapshot_id = 123 + req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id) + req.method = 'DELETE' + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 202) + + def test_snapshot_delete_invalid_id(self): + snapshot_id = -1 + req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id) + req.method = 'DELETE' + + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 404) + + def test_snapshot_show(self): + snapshot_id = 123 + req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id) + req.method = 'GET' + resp = req.get_response(self.app) + + self.assertEqual(resp.status_int, 200) + resp_dict = jsonutils.loads(resp.body) + self.assertIn('snapshot', resp_dict) + self.assertEqual(resp_dict['snapshot']['id'], str(snapshot_id)) + + def test_snapshot_show_invalid_id(self): + snapshot_id = -1 + req = webob.Request.blank('/v2/fake/os-snapshots/%d' % snapshot_id) + req.method = 'GET' + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 404) + + def test_snapshot_detail(self): + req = webob.Request.blank('/v2/fake/os-snapshots/detail') + req.method = 'GET' + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 200) + + resp_dict = jsonutils.loads(resp.body) + self.assertIn('snapshots', resp_dict) + resp_snapshots = resp_dict['snapshots'] + self.assertEqual(len(resp_snapshots), 3) + + resp_snapshot = resp_snapshots.pop() + self.assertEqual(resp_snapshot['id'], 102) + + +class SnapshotSerializerTest(test.NoDBTestCase): + def _verify_snapshot(self, snap, tree): + self.assertEqual(tree.tag, 'snapshot') + + for attr in ('id', 'status', 'size', 'createdAt', + 'displayName', 'displayDescription', 'volumeId'): + self.assertEqual(str(snap[attr]), tree.get(attr)) + + def test_snapshot_show_create_serializer(self): + serializer = volumes.SnapshotTemplate() + raw_snapshot = dict( + id='snap_id', + status='snap_status', + size=1024, + createdAt=timeutils.utcnow(), + displayName='snap_name', + displayDescription='snap_desc', + volumeId='vol_id', + ) + text = serializer.serialize(dict(snapshot=raw_snapshot)) + + tree = etree.fromstring(text) + + self._verify_snapshot(raw_snapshot, tree) + + def test_snapshot_index_detail_serializer(self): + serializer = volumes.SnapshotsTemplate() + raw_snapshots = [dict( + id='snap1_id', + status='snap1_status', + size=1024, + createdAt=timeutils.utcnow(), + displayName='snap1_name', + displayDescription='snap1_desc', + volumeId='vol1_id', + ), + dict( + id='snap2_id', + status='snap2_status', + size=1024, + createdAt=timeutils.utcnow(), + displayName='snap2_name', + displayDescription='snap2_desc', + volumeId='vol2_id', + )] + text = serializer.serialize(dict(snapshots=raw_snapshots)) + + tree = etree.fromstring(text) + + self.assertEqual('snapshots', tree.tag) + self.assertEqual(len(raw_snapshots), len(tree)) + for idx, child in enumerate(tree): + self._verify_snapshot(raw_snapshots[idx], child) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_tenant_networks.py b/nova/tests/unit/api/openstack/compute/contrib/test_tenant_networks.py new file mode 100644 index 0000000000..30d4da6ba1 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_tenant_networks.py @@ -0,0 +1,76 @@ +# Copyright 2014 IBM Corp. +# +# 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 +import webob + +from nova.api.openstack.compute.contrib import os_tenant_networks as networks +from nova.api.openstack.compute.plugins.v3 import tenant_networks \ + as networks_v21 +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + + +class TenantNetworksTestV21(test.NoDBTestCase): + ctrlr = networks_v21.TenantNetworkController + + def setUp(self): + super(TenantNetworksTestV21, self).setUp() + self.controller = self.ctrlr() + self.flags(enable_network_quota=True) + + @mock.patch('nova.network.api.API.delete', + side_effect=exception.NetworkInUse(network_id=1)) + def test_network_delete_in_use(self, mock_delete): + req = fakes.HTTPRequest.blank('/v2/1234/os-tenant-networks/1') + + self.assertRaises(webob.exc.HTTPConflict, + self.controller.delete, req, 1) + + @mock.patch('nova.quota.QUOTAS.reserve') + @mock.patch('nova.quota.QUOTAS.rollback') + @mock.patch('nova.network.api.API.delete') + def _test_network_delete_exception(self, ex, expex, delete_mock, + rollback_mock, reserve_mock): + req = fakes.HTTPRequest.blank('/v2/1234/os-tenant-networks') + ctxt = req.environ['nova.context'] + + reserve_mock.return_value = 'rv' + delete_mock.side_effect = ex + + self.assertRaises(expex, self.controller.delete, req, 1) + + delete_mock.assert_called_once_with(ctxt, 1) + rollback_mock.assert_called_once_with(ctxt, 'rv') + reserve_mock.assert_called_once_with(ctxt, networks=-1) + + def test_network_delete_exception_network_not_found(self): + ex = exception.NetworkNotFound(network_id=1) + expex = webob.exc.HTTPNotFound + self._test_network_delete_exception(ex, expex) + + def test_network_delete_exception_policy_failed(self): + ex = exception.PolicyNotAuthorized(action='dummy') + expex = webob.exc.HTTPForbidden + self._test_network_delete_exception(ex, expex) + + def test_network_delete_exception_network_in_use(self): + ex = exception.NetworkInUse(network_id=1) + expex = webob.exc.HTTPConflict + self._test_network_delete_exception(ex, expex) + + +class TenantNetworksTestV2(TenantNetworksTestV21): + ctrlr = networks.NetworkController diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_used_limits.py b/nova/tests/unit/api/openstack/compute/contrib/test_used_limits.py new file mode 100644 index 0000000000..ee2b0d703b --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_used_limits.py @@ -0,0 +1,306 @@ +# 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. + +from nova.api.openstack.compute.contrib import used_limits as used_limits_v2 +from nova.api.openstack.compute import limits +from nova.api.openstack.compute.plugins.v3 import used_limits as \ + used_limits_v21 +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +import nova.context +from nova import exception +from nova import quota +from nova import test + + +class FakeRequest(object): + def __init__(self, context, reserved=False): + self.environ = {'nova.context': context} + self.reserved = reserved + self.GET = {'reserved': 1} if reserved else {} + + +class UsedLimitsTestCaseV21(test.NoDBTestCase): + used_limit_extension = "compute_extension:v3:os-used-limits:used_limits" + include_server_group_quotas = True + + def setUp(self): + """Run before each test.""" + super(UsedLimitsTestCaseV21, self).setUp() + self._set_up_controller() + self.fake_context = nova.context.RequestContext('fake', 'fake') + + def _set_up_controller(self): + self.ext_mgr = None + self.controller = used_limits_v21.UsedLimitsController() + self.mox.StubOutWithMock(used_limits_v21, 'authorize') + self.authorize = used_limits_v21.authorize + + def _do_test_used_limits(self, reserved): + fake_req = FakeRequest(self.fake_context, reserved=reserved) + obj = { + "limits": { + "rate": [], + "absolute": {}, + }, + } + res = wsgi.ResponseObject(obj) + quota_map = { + 'totalRAMUsed': 'ram', + 'totalCoresUsed': 'cores', + 'totalInstancesUsed': 'instances', + 'totalFloatingIpsUsed': 'floating_ips', + 'totalSecurityGroupsUsed': 'security_groups', + 'totalServerGroupsUsed': 'server_groups', + } + limits = {} + expected_abs_limits = [] + for display_name, q in quota_map.iteritems(): + limits[q] = {'limit': len(display_name), + 'in_use': len(display_name) / 2, + 'reserved': len(display_name) / 3} + if (self.include_server_group_quotas or + display_name != 'totalServerGroupsUsed'): + expected_abs_limits.append(display_name) + + def stub_get_project_quotas(context, project_id, usages=True): + return limits + + self.stubs.Set(quota.QUOTAS, "get_project_quotas", + stub_get_project_quotas) + if self.ext_mgr is not None: + self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False) + self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn( + self.include_server_group_quotas) + self.mox.ReplayAll() + + self.controller.index(fake_req, res) + abs_limits = res.obj['limits']['absolute'] + for limit in expected_abs_limits: + value = abs_limits[limit] + r = limits[quota_map[limit]]['reserved'] if reserved else 0 + self.assertEqual(value, + limits[quota_map[limit]]['in_use'] + r) + + def test_used_limits_basic(self): + self._do_test_used_limits(False) + + def test_used_limits_with_reserved(self): + self._do_test_used_limits(True) + + def test_admin_can_fetch_limits_for_a_given_tenant_id(self): + project_id = "123456" + user_id = "A1234" + tenant_id = 'abcd' + self.fake_context.project_id = project_id + self.fake_context.user_id = user_id + obj = { + "limits": { + "rate": [], + "absolute": {}, + }, + } + target = { + "project_id": tenant_id, + "user_id": user_id + } + fake_req = FakeRequest(self.fake_context) + fake_req.GET = {'tenant_id': tenant_id} + if self.ext_mgr is not None: + self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(True) + self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn( + self.include_server_group_quotas) + self.authorize(self.fake_context, target=target) + self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas') + quota.QUOTAS.get_project_quotas(self.fake_context, '%s' % tenant_id, + usages=True).AndReturn({}) + self.mox.ReplayAll() + res = wsgi.ResponseObject(obj) + self.controller.index(fake_req, res) + + def test_admin_can_fetch_used_limits_for_own_project(self): + project_id = "123456" + user_id = "A1234" + self.fake_context.project_id = project_id + self.fake_context.user_id = user_id + obj = { + "limits": { + "rate": [], + "absolute": {}, + }, + } + fake_req = FakeRequest(self.fake_context) + fake_req.GET = {} + if self.ext_mgr is not None: + self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(True) + self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn( + self.include_server_group_quotas) + self.mox.StubOutWithMock(extensions, 'extension_authorizer') + self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas') + quota.QUOTAS.get_project_quotas(self.fake_context, '%s' % project_id, + usages=True).AndReturn({}) + self.mox.ReplayAll() + res = wsgi.ResponseObject(obj) + self.controller.index(fake_req, res) + + def test_non_admin_cannot_fetch_used_limits_for_any_other_project(self): + project_id = "123456" + user_id = "A1234" + tenant_id = "abcd" + self.fake_context.project_id = project_id + self.fake_context.user_id = user_id + obj = { + "limits": { + "rate": [], + "absolute": {}, + }, + } + target = { + "project_id": tenant_id, + "user_id": user_id + } + fake_req = FakeRequest(self.fake_context) + fake_req.GET = {'tenant_id': tenant_id} + if self.ext_mgr is not None: + self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(True) + self.authorize(self.fake_context, target=target). \ + AndRaise(exception.PolicyNotAuthorized( + action=self.used_limit_extension)) + self.mox.ReplayAll() + res = wsgi.ResponseObject(obj) + self.assertRaises(exception.PolicyNotAuthorized, self.controller.index, + fake_req, res) + + def test_used_limits_fetched_for_context_project_id(self): + project_id = "123456" + self.fake_context.project_id = project_id + obj = { + "limits": { + "rate": [], + "absolute": {}, + }, + } + fake_req = FakeRequest(self.fake_context) + if self.ext_mgr is not None: + self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False) + self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn( + self.include_server_group_quotas) + self.mox.StubOutWithMock(quota.QUOTAS, 'get_project_quotas') + quota.QUOTAS.get_project_quotas(self.fake_context, project_id, + usages=True).AndReturn({}) + self.mox.ReplayAll() + res = wsgi.ResponseObject(obj) + self.controller.index(fake_req, res) + + def test_used_ram_added(self): + fake_req = FakeRequest(self.fake_context) + obj = { + "limits": { + "rate": [], + "absolute": { + "maxTotalRAMSize": 512, + }, + }, + } + res = wsgi.ResponseObject(obj) + + def stub_get_project_quotas(context, project_id, usages=True): + return {'ram': {'limit': 512, 'in_use': 256}} + + if self.ext_mgr is not None: + self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False) + self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn( + self.include_server_group_quotas) + self.stubs.Set(quota.QUOTAS, "get_project_quotas", + stub_get_project_quotas) + self.mox.ReplayAll() + + self.controller.index(fake_req, res) + abs_limits = res.obj['limits']['absolute'] + self.assertIn('totalRAMUsed', abs_limits) + self.assertEqual(abs_limits['totalRAMUsed'], 256) + + def test_no_ram_quota(self): + fake_req = FakeRequest(self.fake_context) + obj = { + "limits": { + "rate": [], + "absolute": {}, + }, + } + res = wsgi.ResponseObject(obj) + + def stub_get_project_quotas(context, project_id, usages=True): + return {} + + if self.ext_mgr is not None: + self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False) + self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn( + self.include_server_group_quotas) + self.stubs.Set(quota.QUOTAS, "get_project_quotas", + stub_get_project_quotas) + self.mox.ReplayAll() + + self.controller.index(fake_req, res) + abs_limits = res.obj['limits']['absolute'] + self.assertNotIn('totalRAMUsed', abs_limits) + + +class UsedLimitsTestCaseV2(UsedLimitsTestCaseV21): + used_limit_extension = "compute_extension:used_limits_for_admin" + + def _set_up_controller(self): + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.controller = used_limits_v2.UsedLimitsController(self.ext_mgr) + self.mox.StubOutWithMock(used_limits_v2, 'authorize_for_admin') + self.authorize = used_limits_v2.authorize_for_admin + + +class UsedLimitsTestCaseV2WithoutServerGroupQuotas(UsedLimitsTestCaseV2): + used_limit_extension = "compute_extension:used_limits_for_admin" + include_server_group_quotas = False + + +class UsedLimitsTestCaseXml(test.NoDBTestCase): + def setUp(self): + """Run before each test.""" + super(UsedLimitsTestCaseXml, self).setUp() + self.ext_mgr = self.mox.CreateMock(extensions.ExtensionManager) + self.controller = used_limits_v2.UsedLimitsController(self.ext_mgr) + self.fake_context = nova.context.RequestContext('fake', 'fake') + + def test_used_limits_xmlns(self): + fake_req = FakeRequest(self.fake_context) + obj = { + "limits": { + "rate": [], + "absolute": {}, + }, + } + res = wsgi.ResponseObject(obj, xml=limits.LimitsTemplate) + res.preserialize('xml') + + def stub_get_project_quotas(context, project_id, usages=True): + return {} + + self.ext_mgr.is_loaded('os-used-limits-for-admin').AndReturn(False) + self.stubs.Set(quota.QUOTAS, "get_project_quotas", + stub_get_project_quotas) + self.ext_mgr.is_loaded('os-server-group-quotas').AndReturn(False) + self.mox.ReplayAll() + + self.controller.index(fake_req, res) + response = res.serialize(None, 'xml') + self.assertIn(used_limits_v2.XMLNS, response.body) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_virtual_interfaces.py b/nova/tests/unit/api/openstack/compute/contrib/test_virtual_interfaces.py new file mode 100644 index 0000000000..e8484d61b9 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_virtual_interfaces.py @@ -0,0 +1,127 @@ +# Copyright (C) 2011 Midokura KK +# 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.contrib import virtual_interfaces +from nova.api.openstack import wsgi +from nova import compute +from nova.compute import api as compute_api +from nova import context +from nova import exception +from nova import network +from nova import test +from nova.tests.unit.api.openstack import fakes + + +FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + + +def compute_api_get(self, context, instance_id, expected_attrs=None, + want_objects=False): + return dict(uuid=FAKE_UUID, id=instance_id, instance_type_id=1, host='bob') + + +def get_vifs_by_instance(self, context, instance_id): + return [{'uuid': '00000000-0000-0000-0000-00000000000000000', + 'address': '00-00-00-00-00-00'}, + {'uuid': '11111111-1111-1111-1111-11111111111111111', + 'address': '11-11-11-11-11-11'}] + + +class FakeRequest(object): + def __init__(self, context): + self.environ = {'nova.context': context} + + +class ServerVirtualInterfaceTest(test.NoDBTestCase): + + def setUp(self): + super(ServerVirtualInterfaceTest, self).setUp() + self.stubs.Set(compute.api.API, "get", + compute_api_get) + self.stubs.Set(network.api.API, "get_vifs_by_instance", + get_vifs_by_instance) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Virtual_interfaces']) + + def test_get_virtual_interfaces_list(self): + url = '/v2/fake/servers/abcd/os-virtual-interfaces' + req = webob.Request.blank(url) + res = req.get_response(fakes.wsgi_app( + init_only=('os-virtual-interfaces',))) + self.assertEqual(res.status_int, 200) + res_dict = jsonutils.loads(res.body) + response = {'virtual_interfaces': [ + {'id': '00000000-0000-0000-0000-00000000000000000', + 'mac_address': '00-00-00-00-00-00'}, + {'id': '11111111-1111-1111-1111-11111111111111111', + 'mac_address': '11-11-11-11-11-11'}]} + self.assertEqual(res_dict, response) + + def test_vif_instance_not_found(self): + self.mox.StubOutWithMock(compute_api.API, 'get') + fake_context = context.RequestContext('fake', 'fake') + fake_req = FakeRequest(fake_context) + + compute_api.API.get(fake_context, 'fake_uuid', + expected_attrs=None, + want_objects=True).AndRaise( + exception.InstanceNotFound(instance_id='instance-0000')) + + self.mox.ReplayAll() + self.assertRaises( + webob.exc.HTTPNotFound, + virtual_interfaces.ServerVirtualInterfaceController().index, + fake_req, 'fake_uuid') + + +class ServerVirtualInterfaceSerializerTest(test.NoDBTestCase): + def setUp(self): + super(ServerVirtualInterfaceSerializerTest, self).setUp() + self.namespace = wsgi.XMLNS_V11 + self.serializer = virtual_interfaces.VirtualInterfaceTemplate() + + def _tag(self, elem): + tagname = elem.tag + self.assertEqual(tagname[0], '{') + tmp = tagname.partition('}') + namespace = tmp[0][1:] + self.assertEqual(namespace, self.namespace) + return tmp[2] + + def test_serializer(self): + raw_vifs = [dict( + id='uuid1', + mac_address='aa:bb:cc:dd:ee:ff'), + dict( + id='uuid2', + mac_address='bb:aa:dd:cc:ff:ee')] + vifs = dict(virtual_interfaces=raw_vifs) + text = self.serializer.serialize(vifs) + + tree = etree.fromstring(text) + + self.assertEqual('virtual_interfaces', self._tag(tree)) + self.assertEqual(len(raw_vifs), len(tree)) + for idx, child in enumerate(tree): + self.assertEqual('virtual_interface', self._tag(child)) + self.assertEqual(raw_vifs[idx]['id'], child.get('id')) + self.assertEqual(raw_vifs[idx]['mac_address'], + child.get('mac_address')) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_volumes.py b/nova/tests/unit/api/openstack/compute/contrib/test_volumes.py new file mode 100644 index 0000000000..e3c5b8b071 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/contrib/test_volumes.py @@ -0,0 +1,1083 @@ +# Copyright 2013 Josh Durgin +# Copyright 2013 Red Hat, Inc. +# 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 datetime + +from lxml import etree +import mock +from oslo.config import cfg +from oslo.serialization import jsonutils +from oslo.utils import timeutils +import webob +from webob import exc + +from nova.api.openstack.compute.contrib import assisted_volume_snapshots as \ + assisted_snaps +from nova.api.openstack.compute.contrib import volumes +from nova.api.openstack.compute.plugins.v3 import volumes as volumes_v3 +from nova.api.openstack import extensions +from nova.compute import api as compute_api +from nova.compute import flavors +from nova import context +from nova import db +from nova import exception +from nova import objects +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_block_device +from nova.tests.unit import fake_instance +from nova.volume import cinder + +CONF = cfg.CONF +CONF.import_opt('password_length', 'nova.utils') + +FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +FAKE_UUID_A = '00000000-aaaa-aaaa-aaaa-000000000000' +FAKE_UUID_B = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb' +FAKE_UUID_C = 'cccccccc-cccc-cccc-cccc-cccccccccccc' +FAKE_UUID_D = 'dddddddd-dddd-dddd-dddd-dddddddddddd' + +IMAGE_UUID = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + + +def fake_get_instance(self, context, instance_id, want_objects=False, + expected_attrs=None): + return fake_instance.fake_instance_obj(context, **{'uuid': instance_id}) + + +def fake_get_volume(self, context, id): + return {'id': 'woot'} + + +def fake_attach_volume(self, context, instance, volume_id, device): + pass + + +def fake_detach_volume(self, context, instance, volume): + pass + + +def fake_swap_volume(self, context, instance, + old_volume_id, new_volume_id): + pass + + +def fake_create_snapshot(self, context, volume, name, description): + return {'id': 123, + 'volume_id': 'fakeVolId', + 'status': 'available', + 'volume_size': 123, + 'created_at': '2013-01-01 00:00:01', + 'display_name': 'myVolumeName', + 'display_description': 'myVolumeDescription'} + + +def fake_delete_snapshot(self, context, snapshot_id): + pass + + +def fake_compute_volume_snapshot_delete(self, context, volume_id, snapshot_id, + delete_info): + pass + + +def fake_compute_volume_snapshot_create(self, context, volume_id, + create_info): + pass + + +def fake_bdms_get_all_by_instance(context, instance_uuid, use_slave=False): + return [fake_block_device.FakeDbBlockDeviceDict( + {'id': 1, + 'instance_uuid': instance_uuid, + 'device_name': '/dev/fake0', + 'delete_on_termination': 'False', + 'source_type': 'volume', + 'destination_type': 'volume', + 'snapshot_id': None, + 'volume_id': FAKE_UUID_A, + 'volume_size': 1}), + fake_block_device.FakeDbBlockDeviceDict( + {'id': 2, + 'instance_uuid': instance_uuid, + 'device_name': '/dev/fake1', + 'delete_on_termination': 'False', + 'source_type': 'volume', + 'destination_type': 'volume', + 'snapshot_id': None, + 'volume_id': FAKE_UUID_B, + 'volume_size': 1})] + + +class BootFromVolumeTest(test.TestCase): + + def setUp(self): + super(BootFromVolumeTest, self).setUp() + self.stubs.Set(compute_api.API, 'create', + self._get_fake_compute_api_create()) + fakes.stub_out_nw_api(self.stubs) + self._block_device_mapping_seen = None + self._legacy_bdm_seen = True + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Volumes', 'Block_device_mapping_v2_boot']) + + def _get_fake_compute_api_create(self): + def _fake_compute_api_create(cls, context, instance_type, + image_href, **kwargs): + self._block_device_mapping_seen = kwargs.get( + 'block_device_mapping') + self._legacy_bdm_seen = kwargs.get('legacy_bdm') + + inst_type = flavors.get_flavor_by_flavor_id(2) + resv_id = None + return ([{'id': 1, + 'display_name': 'test_server', + 'uuid': FAKE_UUID, + 'instance_type': inst_type, + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': 'fead::1234', + 'image_ref': IMAGE_UUID, + 'user_id': 'fake', + 'project_id': 'fake', + 'created_at': datetime.datetime(2010, 10, 10, 12, 0, 0), + 'updated_at': datetime.datetime(2010, 11, 11, 11, 0, 0), + 'progress': 0, + 'fixed_ips': [] + }], resv_id) + return _fake_compute_api_create + + def test_create_root_volume(self): + body = dict(server=dict( + name='test_server', imageRef=IMAGE_UUID, + flavorRef=2, min_count=1, max_count=1, + block_device_mapping=[dict( + volume_id=1, + device_name='/dev/vda', + virtual='root', + delete_on_termination=False, + )] + )) + req = webob.Request.blank('/v2/fake/os-volumes_boot') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + res = req.get_response(fakes.wsgi_app( + init_only=('os-volumes_boot', 'servers'))) + self.assertEqual(res.status_int, 202) + server = jsonutils.loads(res.body)['server'] + self.assertEqual(FAKE_UUID, server['id']) + self.assertEqual(CONF.password_length, len(server['adminPass'])) + self.assertEqual(len(self._block_device_mapping_seen), 1) + self.assertTrue(self._legacy_bdm_seen) + self.assertEqual(self._block_device_mapping_seen[0]['volume_id'], 1) + self.assertEqual(self._block_device_mapping_seen[0]['device_name'], + '/dev/vda') + + def test_create_root_volume_bdm_v2(self): + body = dict(server=dict( + name='test_server', imageRef=IMAGE_UUID, + flavorRef=2, min_count=1, max_count=1, + block_device_mapping_v2=[dict( + source_type='volume', + uuid=1, + device_name='/dev/vda', + boot_index=0, + delete_on_termination=False, + )] + )) + req = webob.Request.blank('/v2/fake/os-volumes_boot') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + res = req.get_response(fakes.wsgi_app( + init_only=('os-volumes_boot', 'servers'))) + self.assertEqual(res.status_int, 202) + server = jsonutils.loads(res.body)['server'] + self.assertEqual(FAKE_UUID, server['id']) + self.assertEqual(CONF.password_length, len(server['adminPass'])) + self.assertEqual(len(self._block_device_mapping_seen), 1) + self.assertFalse(self._legacy_bdm_seen) + self.assertEqual(self._block_device_mapping_seen[0]['volume_id'], 1) + self.assertEqual(self._block_device_mapping_seen[0]['boot_index'], + 0) + self.assertEqual(self._block_device_mapping_seen[0]['device_name'], + '/dev/vda') + + +class VolumeApiTestV21(test.TestCase): + url_prefix = '/v2/fake' + + def setUp(self): + super(VolumeApiTestV21, self).setUp() + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + + self.stubs.Set(cinder.API, "delete", fakes.stub_volume_delete) + self.stubs.Set(cinder.API, "get", fakes.stub_volume_get) + self.stubs.Set(cinder.API, "get_all", fakes.stub_volume_get_all) + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Volumes']) + + self.context = context.get_admin_context() + self.app = self._get_app() + + def _get_app(self): + return fakes.wsgi_app_v21() + + def test_volume_create(self): + self.stubs.Set(cinder.API, "create", fakes.stub_volume_create) + + vol = {"size": 100, + "display_name": "Volume Test Name", + "display_description": "Volume Test Desc", + "availability_zone": "zone1:host1"} + body = {"volume": vol} + req = webob.Request.blank(self.url_prefix + '/os-volumes') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers['content-type'] = 'application/json' + resp = req.get_response(self.app) + + self.assertEqual(resp.status_int, 200) + + resp_dict = jsonutils.loads(resp.body) + self.assertIn('volume', resp_dict) + self.assertEqual(resp_dict['volume']['size'], + vol['size']) + self.assertEqual(resp_dict['volume']['displayName'], + vol['display_name']) + self.assertEqual(resp_dict['volume']['displayDescription'], + vol['display_description']) + self.assertEqual(resp_dict['volume']['availabilityZone'], + vol['availability_zone']) + + def test_volume_create_bad(self): + def fake_volume_create(self, context, size, name, description, + snapshot, **param): + raise exception.InvalidInput(reason="bad request data") + + self.stubs.Set(cinder.API, "create", fake_volume_create) + + vol = {"size": '#$?', + "display_name": "Volume Test Name", + "display_description": "Volume Test Desc", + "availability_zone": "zone1:host1"} + body = {"volume": vol} + + req = fakes.HTTPRequest.blank(self.url_prefix + '/os-volumes') + self.assertRaises(webob.exc.HTTPBadRequest, + volumes.VolumeController().create, req, body) + + def test_volume_index(self): + req = webob.Request.blank(self.url_prefix + '/os-volumes') + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 200) + + def test_volume_detail(self): + req = webob.Request.blank(self.url_prefix + '/os-volumes/detail') + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 200) + + def test_volume_show(self): + req = webob.Request.blank(self.url_prefix + '/os-volumes/123') + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 200) + + def test_volume_show_no_volume(self): + self.stubs.Set(cinder.API, "get", fakes.stub_volume_notfound) + + req = webob.Request.blank(self.url_prefix + '/os-volumes/456') + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 404) + self.assertIn('Volume 456 could not be found.', resp.body) + + def test_volume_delete(self): + req = webob.Request.blank(self.url_prefix + '/os-volumes/123') + req.method = 'DELETE' + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 202) + + def test_volume_delete_no_volume(self): + self.stubs.Set(cinder.API, "delete", fakes.stub_volume_notfound) + + req = webob.Request.blank(self.url_prefix + '/os-volumes/456') + req.method = 'DELETE' + resp = req.get_response(self.app) + self.assertEqual(resp.status_int, 404) + self.assertIn('Volume 456 could not be found.', resp.body) + + +class VolumeApiTestV2(VolumeApiTestV21): + + def setUp(self): + super(VolumeApiTestV2, self).setUp() + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Volumes']) + + self.context = context.get_admin_context() + self.app = self._get_app() + + def _get_app(self): + return fakes.wsgi_app() + + +class VolumeAttachTests(test.TestCase): + def setUp(self): + super(VolumeAttachTests, self).setUp() + self.stubs.Set(db, 'block_device_mapping_get_all_by_instance', + fake_bdms_get_all_by_instance) + self.stubs.Set(compute_api.API, 'get', fake_get_instance) + self.stubs.Set(cinder.API, 'get', fake_get_volume) + self.context = context.get_admin_context() + self.expected_show = {'volumeAttachment': + {'device': '/dev/fake0', + 'serverId': FAKE_UUID, + 'id': FAKE_UUID_A, + 'volumeId': FAKE_UUID_A + }} + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.attachments = volumes.VolumeAttachmentController(self.ext_mgr) + + def test_show(self): + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + result = self.attachments.show(req, FAKE_UUID, FAKE_UUID_A) + self.assertEqual(self.expected_show, result) + + @mock.patch.object(compute_api.API, 'get', + side_effect=exception.InstanceNotFound(instance_id=FAKE_UUID)) + def test_show_no_instance(self, mock_mr): + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + self.assertRaises(exc.HTTPNotFound, + self.attachments.show, + req, + FAKE_UUID, + FAKE_UUID_A) + + @mock.patch.object(objects.BlockDeviceMappingList, + 'get_by_instance_uuid', return_value=None) + def test_show_no_bdms(self, mock_mr): + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + self.assertRaises(exc.HTTPNotFound, + self.attachments.show, + req, + FAKE_UUID, + FAKE_UUID_A) + + def test_show_bdms_no_mountpoint(self): + FAKE_UUID_NOTEXIST = '00000000-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + self.assertRaises(exc.HTTPNotFound, + self.attachments.show, + req, + FAKE_UUID, + FAKE_UUID_NOTEXIST) + + def test_detach(self): + self.stubs.Set(compute_api.API, + 'detach_volume', + fake_detach_volume) + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') + req.method = 'DELETE' + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + result = self.attachments.delete(req, FAKE_UUID, FAKE_UUID_A) + self.assertEqual('202 Accepted', result.status) + + def test_detach_vol_not_found(self): + self.stubs.Set(compute_api.API, + 'detach_volume', + fake_detach_volume) + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') + req.method = 'DELETE' + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + self.assertRaises(exc.HTTPNotFound, + self.attachments.delete, + req, + FAKE_UUID, + FAKE_UUID_C) + + @mock.patch('nova.objects.BlockDeviceMapping.is_root', + new_callable=mock.PropertyMock) + def test_detach_vol_root(self, mock_isroot): + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') + req.method = 'DELETE' + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + mock_isroot.return_value = True + self.assertRaises(exc.HTTPForbidden, + self.attachments.delete, + req, + FAKE_UUID, + FAKE_UUID_A) + + def test_detach_volume_from_locked_server(self): + def fake_detach_volume_from_locked_server(self, context, + instance, volume): + raise exception.InstanceIsLocked(instance_uuid=instance['uuid']) + + self.stubs.Set(compute_api.API, + 'detach_volume', + fake_detach_volume_from_locked_server) + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') + req.method = 'DELETE' + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + self.assertRaises(webob.exc.HTTPConflict, self.attachments.delete, + req, FAKE_UUID, FAKE_UUID_A) + + def test_attach_volume(self): + self.stubs.Set(compute_api.API, + 'attach_volume', + fake_attach_volume) + body = {'volumeAttachment': {'volumeId': FAKE_UUID_A, + 'device': '/dev/fake'}} + req = webob.Request.blank('/v2/servers/id/os-volume_attachments') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + result = self.attachments.create(req, FAKE_UUID, body) + self.assertEqual(result['volumeAttachment']['id'], + '00000000-aaaa-aaaa-aaaa-000000000000') + + def test_attach_volume_to_locked_server(self): + def fake_attach_volume_to_locked_server(self, context, instance, + volume_id, device=None): + raise exception.InstanceIsLocked(instance_uuid=instance['uuid']) + + self.stubs.Set(compute_api.API, + 'attach_volume', + fake_attach_volume_to_locked_server) + body = {'volumeAttachment': {'volumeId': FAKE_UUID_A, + 'device': '/dev/fake'}} + req = webob.Request.blank('/v2/servers/id/os-volume_attachments') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + self.assertRaises(webob.exc.HTTPConflict, self.attachments.create, + req, FAKE_UUID, body) + + def test_attach_volume_bad_id(self): + self.stubs.Set(compute_api.API, + 'attach_volume', + fake_attach_volume) + + body = { + 'volumeAttachment': { + 'device': None, + 'volumeId': 'TESTVOLUME', + } + } + + req = webob.Request.blank('/v2/servers/id/os-volume_attachments') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + self.assertRaises(webob.exc.HTTPBadRequest, self.attachments.create, + req, FAKE_UUID, body) + + def test_attach_volume_without_volumeId(self): + self.stubs.Set(compute_api.API, + 'attach_volume', + fake_attach_volume) + + body = { + 'volumeAttachment': { + 'device': None + } + } + + req = webob.Request.blank('/v2/servers/id/os-volume_attachments') + req.method = 'POST' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + + self.assertRaises(webob.exc.HTTPBadRequest, self.attachments.create, + req, FAKE_UUID, body) + + def _test_swap(self, uuid=FAKE_UUID_A, fake_func=None, body=None): + fake_func = fake_func or fake_swap_volume + self.stubs.Set(compute_api.API, + 'swap_volume', + fake_func) + body = body or {'volumeAttachment': {'volumeId': FAKE_UUID_B, + 'device': '/dev/fake'}} + + req = webob.Request.blank('/v2/servers/id/os-volume_attachments/uuid') + req.method = 'PUT' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + return self.attachments.update(req, FAKE_UUID, uuid, body) + + def test_swap_volume_for_locked_server(self): + self.ext_mgr.extensions['os-volume-attachment-update'] = True + + def fake_swap_volume_for_locked_server(self, context, instance, + old_volume, new_volume): + raise exception.InstanceIsLocked(instance_uuid=instance['uuid']) + + self.ext_mgr.extensions['os-volume-attachment-update'] = True + self.assertRaises(webob.exc.HTTPConflict, self._test_swap, + fake_func=fake_swap_volume_for_locked_server) + + def test_swap_volume_no_extension(self): + self.assertRaises(webob.exc.HTTPBadRequest, self._test_swap) + + def test_swap_volume(self): + self.ext_mgr.extensions['os-volume-attachment-update'] = True + result = self._test_swap() + self.assertEqual('202 Accepted', result.status) + + def test_swap_volume_no_attachment(self): + self.ext_mgr.extensions['os-volume-attachment-update'] = True + + self.assertRaises(exc.HTTPNotFound, self._test_swap, FAKE_UUID_C) + + def test_swap_volume_without_volumeId(self): + self.ext_mgr.extensions['os-volume-attachment-update'] = True + body = {'volumeAttachment': {'device': '/dev/fake'}} + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_swap, + body=body) + + +class VolumeSerializerTest(test.TestCase): + def _verify_volume_attachment(self, attach, tree): + for attr in ('id', 'volumeId', 'serverId', 'device'): + self.assertEqual(str(attach[attr]), tree.get(attr)) + + def _verify_volume(self, vol, tree): + self.assertEqual(tree.tag, 'volume') + + for attr in ('id', 'status', 'size', 'availabilityZone', 'createdAt', + 'displayName', 'displayDescription', 'volumeType', + 'snapshotId'): + self.assertEqual(str(vol[attr]), tree.get(attr)) + + for child in tree: + self.assertIn(child.tag, ('attachments', 'metadata')) + if child.tag == 'attachments': + self.assertEqual(1, len(child)) + self.assertEqual('attachment', child[0].tag) + self._verify_volume_attachment(vol['attachments'][0], child[0]) + elif child.tag == 'metadata': + not_seen = set(vol['metadata'].keys()) + for gr_child in child: + self.assertIn(gr_child.get("key"), not_seen) + self.assertEqual(str(vol['metadata'][gr_child.get("key")]), + gr_child.text) + not_seen.remove(gr_child.get("key")) + self.assertEqual(0, len(not_seen)) + + def test_attach_show_create_serializer(self): + serializer = volumes.VolumeAttachmentTemplate() + raw_attach = dict( + id='vol_id', + volumeId='vol_id', + serverId='instance_uuid', + device='/foo') + text = serializer.serialize(dict(volumeAttachment=raw_attach)) + + tree = etree.fromstring(text) + + self.assertEqual('volumeAttachment', tree.tag) + self._verify_volume_attachment(raw_attach, tree) + + def test_attach_index_serializer(self): + serializer = volumes.VolumeAttachmentsTemplate() + raw_attaches = [dict( + id='vol_id1', + volumeId='vol_id1', + serverId='instance1_uuid', + device='/foo1'), + dict( + id='vol_id2', + volumeId='vol_id2', + serverId='instance2_uuid', + device='/foo2')] + text = serializer.serialize(dict(volumeAttachments=raw_attaches)) + + tree = etree.fromstring(text) + + self.assertEqual('volumeAttachments', tree.tag) + self.assertEqual(len(raw_attaches), len(tree)) + for idx, child in enumerate(tree): + self.assertEqual('volumeAttachment', child.tag) + self._verify_volume_attachment(raw_attaches[idx], child) + + def test_volume_show_create_serializer(self): + serializer = volumes.VolumeTemplate() + raw_volume = dict( + id='vol_id', + status='vol_status', + size=1024, + availabilityZone='vol_availability', + createdAt=timeutils.utcnow(), + attachments=[dict( + id='vol_id', + volumeId='vol_id', + serverId='instance_uuid', + device='/foo')], + displayName='vol_name', + displayDescription='vol_desc', + volumeType='vol_type', + snapshotId='snap_id', + metadata=dict( + foo='bar', + baz='quux', + ), + ) + text = serializer.serialize(dict(volume=raw_volume)) + + tree = etree.fromstring(text) + + self._verify_volume(raw_volume, tree) + + def test_volume_index_detail_serializer(self): + serializer = volumes.VolumesTemplate() + raw_volumes = [dict( + id='vol1_id', + status='vol1_status', + size=1024, + availabilityZone='vol1_availability', + createdAt=timeutils.utcnow(), + attachments=[dict( + id='vol1_id', + volumeId='vol1_id', + serverId='instance_uuid', + device='/foo1')], + displayName='vol1_name', + displayDescription='vol1_desc', + volumeType='vol1_type', + snapshotId='snap1_id', + metadata=dict( + foo='vol1_foo', + bar='vol1_bar', + ), + ), + dict( + id='vol2_id', + status='vol2_status', + size=1024, + availabilityZone='vol2_availability', + createdAt=timeutils.utcnow(), + attachments=[dict( + id='vol2_id', + volumeId='vol2_id', + serverId='instance_uuid', + device='/foo2')], + displayName='vol2_name', + displayDescription='vol2_desc', + volumeType='vol2_type', + snapshotId='snap2_id', + metadata=dict( + foo='vol2_foo', + bar='vol2_bar', + ), + )] + text = serializer.serialize(dict(volumes=raw_volumes)) + + tree = etree.fromstring(text) + + self.assertEqual('volumes', tree.tag) + self.assertEqual(len(raw_volumes), len(tree)) + for idx, child in enumerate(tree): + self._verify_volume(raw_volumes[idx], child) + + +class TestVolumeCreateRequestXMLDeserializer(test.TestCase): + + def setUp(self): + super(TestVolumeCreateRequestXMLDeserializer, self).setUp() + self.deserializer = volumes.CreateDeserializer() + + def test_minimal_volume(self): + self_request = """ +<volume xmlns="http://docs.openstack.org/compute/api/v1.1" + size="1"></volume>""" + request = self.deserializer.deserialize(self_request) + expected = { + "volume": { + "size": "1", + }, + } + self.assertEqual(request['body'], expected) + + def test_display_name(self): + self_request = """ +<volume xmlns="http://docs.openstack.org/compute/api/v1.1" + size="1" + display_name="Volume-xml"></volume>""" + request = self.deserializer.deserialize(self_request) + expected = { + "volume": { + "size": "1", + "display_name": "Volume-xml", + }, + } + self.assertEqual(request['body'], expected) + + def test_display_description(self): + self_request = """ +<volume xmlns="http://docs.openstack.org/compute/api/v1.1" + size="1" + display_name="Volume-xml" + display_description="description"></volume>""" + request = self.deserializer.deserialize(self_request) + expected = { + "volume": { + "size": "1", + "display_name": "Volume-xml", + "display_description": "description", + }, + } + self.assertEqual(request['body'], expected) + + def test_volume_type(self): + self_request = """ +<volume xmlns="http://docs.openstack.org/compute/api/v1.1" + size="1" + display_name="Volume-xml" + display_description="description" + volume_type="289da7f8-6440-407c-9fb4-7db01ec49164"></volume>""" + request = self.deserializer.deserialize(self_request) + expected = { + "volume": { + "size": "1", + "display_name": "Volume-xml", + "display_description": "description", + "volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164", + }, + } + self.assertEqual(request['body'], expected) + + def test_availability_zone(self): + self_request = """ +<volume xmlns="http://docs.openstack.org/compute/api/v1.1" + size="1" + display_name="Volume-xml" + display_description="description" + volume_type="289da7f8-6440-407c-9fb4-7db01ec49164" + availability_zone="us-east1"></volume>""" + request = self.deserializer.deserialize(self_request) + expected = { + "volume": { + "size": "1", + "display_name": "Volume-xml", + "display_description": "description", + "volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164", + "availability_zone": "us-east1", + }, + } + self.assertEqual(request['body'], expected) + + def test_metadata(self): + self_request = """ +<volume xmlns="http://docs.openstack.org/compute/api/v1.1" + display_name="Volume-xml" + size="1"> + <metadata><meta key="Type">work</meta></metadata></volume>""" + request = self.deserializer.deserialize(self_request) + expected = { + "volume": { + "display_name": "Volume-xml", + "size": "1", + "metadata": { + "Type": "work", + }, + }, + } + self.assertEqual(request['body'], expected) + + def test_full_volume(self): + self_request = """ +<volume xmlns="http://docs.openstack.org/compute/api/v1.1" + size="1" + display_name="Volume-xml" + display_description="description" + volume_type="289da7f8-6440-407c-9fb4-7db01ec49164" + availability_zone="us-east1"> + <metadata><meta key="Type">work</meta></metadata></volume>""" + request = self.deserializer.deserialize(self_request) + expected = { + "volume": { + "size": "1", + "display_name": "Volume-xml", + "display_description": "description", + "volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164", + "availability_zone": "us-east1", + "metadata": { + "Type": "work", + }, + }, + } + self.maxDiff = None + self.assertEqual(request['body'], expected) + + +class CommonBadRequestTestCase(object): + + resource = None + entity_name = None + controller_cls = None + kwargs = {} + + """ + Tests of places we throw 400 Bad Request from + """ + + def setUp(self): + super(CommonBadRequestTestCase, self).setUp() + self.controller = self.controller_cls() + + def _bad_request_create(self, body): + req = fakes.HTTPRequest.blank('/v2/fake/' + self.resource) + req.method = 'POST' + + kwargs = self.kwargs.copy() + kwargs['body'] = body + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, req, **kwargs) + + def test_create_no_body(self): + self._bad_request_create(body=None) + + def test_create_missing_volume(self): + body = {'foo': {'a': 'b'}} + self._bad_request_create(body=body) + + def test_create_malformed_entity(self): + body = {self.entity_name: 'string'} + self._bad_request_create(body=body) + + +class BadRequestVolumeTestCaseV21(CommonBadRequestTestCase, + test.TestCase): + + resource = 'os-volumes' + entity_name = 'volume' + controller_cls = volumes_v3.VolumeController + + +class BadRequestVolumeTestCaseV2(BadRequestVolumeTestCaseV21): + controller_cls = volumes.VolumeController + + +class BadRequestAttachmentTestCase(CommonBadRequestTestCase, + test.TestCase): + resource = 'servers/' + FAKE_UUID + '/os-volume_attachments' + entity_name = 'volumeAttachment' + controller_cls = volumes.VolumeAttachmentController + kwargs = {'server_id': FAKE_UUID} + + +class BadRequestSnapshotTestCaseV21(CommonBadRequestTestCase, + test.TestCase): + + resource = 'os-snapshots' + entity_name = 'snapshot' + controller_cls = volumes.SnapshotController + + +class BadRequestSnapshotTestCaseV2(BadRequestSnapshotTestCaseV21): + controller_cls = volumes_v3.SnapshotController + + +class ShowSnapshotTestCaseV21(test.TestCase): + snapshot_cls = volumes_v3.SnapshotController + + def setUp(self): + super(ShowSnapshotTestCaseV21, self).setUp() + self.controller = self.snapshot_cls() + self.req = fakes.HTTPRequest.blank('/v2/fake/os-snapshots') + self.req.method = 'GET' + + def test_show_snapshot_not_exist(self): + def fake_get_snapshot(self, context, id): + raise exception.SnapshotNotFound(snapshot_id=id) + self.stubs.Set(cinder.API, 'get_snapshot', fake_get_snapshot) + self.assertRaises(exc.HTTPNotFound, + self.controller.show, self.req, FAKE_UUID_A) + + +class ShowSnapshotTestCaseV2(ShowSnapshotTestCaseV21): + snapshot_cls = volumes.SnapshotController + + +class CreateSnapshotTestCaseV21(test.TestCase): + snapshot_cls = volumes_v3.SnapshotController + + def setUp(self): + super(CreateSnapshotTestCaseV21, self).setUp() + self.controller = self.snapshot_cls() + self.stubs.Set(cinder.API, 'get', fake_get_volume) + self.stubs.Set(cinder.API, 'create_snapshot_force', + fake_create_snapshot) + self.stubs.Set(cinder.API, 'create_snapshot', fake_create_snapshot) + self.req = fakes.HTTPRequest.blank('/v2/fake/os-snapshots') + self.req.method = 'POST' + self.body = {'snapshot': {'volume_id': 1}} + + def test_force_true(self): + self.body['snapshot']['force'] = 'True' + self.controller.create(self.req, body=self.body) + + def test_force_false(self): + self.body['snapshot']['force'] = 'f' + self.controller.create(self.req, body=self.body) + + def test_force_invalid(self): + self.body['snapshot']['force'] = 'foo' + self.assertRaises(exc.HTTPBadRequest, + self.controller.create, self.req, body=self.body) + + +class CreateSnapshotTestCaseV2(CreateSnapshotTestCaseV21): + snapshot_cls = volumes.SnapshotController + + +class DeleteSnapshotTestCaseV21(test.TestCase): + snapshot_cls = volumes_v3.SnapshotController + + def setUp(self): + super(DeleteSnapshotTestCaseV21, self).setUp() + self.controller = self.snapshot_cls() + self.stubs.Set(cinder.API, 'get', fake_get_volume) + self.stubs.Set(cinder.API, 'create_snapshot_force', + fake_create_snapshot) + self.stubs.Set(cinder.API, 'create_snapshot', fake_create_snapshot) + self.stubs.Set(cinder.API, 'delete_snapshot', fake_delete_snapshot) + self.req = fakes.HTTPRequest.blank('/v2/fake/os-snapshots') + + def test_normal_delete(self): + self.req.method = 'POST' + self.body = {'snapshot': {'volume_id': 1}} + result = self.controller.create(self.req, body=self.body) + + self.req.method = 'DELETE' + result = self.controller.delete(self.req, result['snapshot']['id']) + + # NOTE: on v2.1, http status code is set as wsgi_code of API + # method instead of status_int in a response object. + if isinstance(self.controller, volumes_v3.SnapshotController): + status_int = self.controller.delete.wsgi_code + else: + status_int = result.status_int + self.assertEqual(202, status_int) + + def test_delete_snapshot_not_exists(self): + def fake_delete_snapshot_not_exist(self, context, snapshot_id): + raise exception.SnapshotNotFound(snapshot_id=snapshot_id) + + self.stubs.Set(cinder.API, 'delete_snapshot', + fake_delete_snapshot_not_exist) + self.req.method = 'POST' + self.body = {'snapshot': {'volume_id': 1}} + result = self.controller.create(self.req, body=self.body) + + self.req.method = 'DELETE' + self.assertRaises(exc.HTTPNotFound, self.controller.delete, + self.req, result['snapshot']['id']) + + +class DeleteSnapshotTestCaseV2(DeleteSnapshotTestCaseV21): + snapshot_cls = volumes.SnapshotController + + +class AssistedSnapshotCreateTestCase(test.TestCase): + def setUp(self): + super(AssistedSnapshotCreateTestCase, self).setUp() + + self.controller = assisted_snaps.AssistedVolumeSnapshotsController() + self.stubs.Set(compute_api.API, 'volume_snapshot_create', + fake_compute_volume_snapshot_create) + + def test_assisted_create(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-assisted-volume-snapshots') + body = {'snapshot': {'volume_id': 1, 'create_info': {}}} + req.method = 'POST' + self.controller.create(req, body=body) + + def test_assisted_create_missing_create_info(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-assisted-volume-snapshots') + body = {'snapshot': {'volume_id': 1}} + req.method = 'POST' + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, body=body) + + +class AssistedSnapshotDeleteTestCase(test.TestCase): + def setUp(self): + super(AssistedSnapshotDeleteTestCase, self).setUp() + + self.controller = assisted_snaps.AssistedVolumeSnapshotsController() + self.stubs.Set(compute_api.API, 'volume_snapshot_delete', + fake_compute_volume_snapshot_delete) + + def test_assisted_delete(self): + params = { + 'delete_info': jsonutils.dumps({'volume_id': 1}), + } + req = fakes.HTTPRequest.blank( + '/v2/fake/os-assisted-volume-snapshots?%s' % + '&'.join(['%s=%s' % (k, v) for k, v in params.iteritems()])) + req.method = 'DELETE' + result = self.controller.delete(req, '5') + self.assertEqual(result.status_int, 204) + + def test_assisted_delete_missing_delete_info(self): + req = fakes.HTTPRequest.blank('/v2/fake/os-assisted-volume-snapshots') + req.method = 'DELETE' + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete, + req, '5') diff --git a/nova/tests/unit/api/openstack/compute/extensions/__init__.py b/nova/tests/unit/api/openstack/compute/extensions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/extensions/__init__.py diff --git a/nova/tests/unit/api/openstack/compute/extensions/foxinsocks.py b/nova/tests/unit/api/openstack/compute/extensions/foxinsocks.py new file mode 100644 index 0000000000..7d1e273ea7 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/extensions/foxinsocks.py @@ -0,0 +1,92 @@ +# Copyright 2011 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 webob.exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi + + +class FoxInSocksController(object): + + def index(self, req): + return "Try to say this Mr. Knox, sir..." + + +class FoxInSocksServerControllerExtension(wsgi.Controller): + @wsgi.action('add_tweedle') + def _add_tweedle(self, req, id, body): + + return "Tweedle Beetle Added." + + @wsgi.action('delete_tweedle') + def _delete_tweedle(self, req, id, body): + + return "Tweedle Beetle Deleted." + + @wsgi.action('fail') + def _fail(self, req, id, body): + + raise webob.exc.HTTPBadRequest(explanation='Tweedle fail') + + +class FoxInSocksFlavorGooseControllerExtension(wsgi.Controller): + @wsgi.extends + def show(self, req, resp_obj, id): + # NOTE: This only handles JSON responses. + # You can use content type header to test for XML. + resp_obj.obj['flavor']['googoose'] = req.GET.get('chewing') + + +class FoxInSocksFlavorBandsControllerExtension(wsgi.Controller): + @wsgi.extends + def show(self, req, resp_obj, id): + # NOTE: This only handles JSON responses. + # You can use content type header to test for XML. + resp_obj.obj['big_bands'] = 'Pig Bands!' + + +class Foxinsocks(extensions.ExtensionDescriptor): + """The Fox In Socks Extension.""" + + name = "Fox In Socks" + alias = "FOXNSOX" + namespace = "http://www.fox.in.socks/api/ext/pie/v1.0" + updated = "2011-01-22T13:25:27-06:00" + + def __init__(self, ext_mgr): + ext_mgr.register(self) + + def get_resources(self): + resources = [] + resource = extensions.ResourceExtension('foxnsocks', + FoxInSocksController()) + resources.append(resource) + return resources + + def get_controller_extensions(self): + extension_list = [] + + extension_set = [ + (FoxInSocksServerControllerExtension, 'servers'), + (FoxInSocksFlavorGooseControllerExtension, 'flavors'), + (FoxInSocksFlavorBandsControllerExtension, 'flavors'), + ] + for klass, collection in extension_set: + controller = klass() + ext = extensions.ControllerExtension(self, collection, controller) + extension_list.append(ext) + + return extension_list diff --git a/nova/tests/unit/api/openstack/compute/plugins/__init__.py b/nova/tests/unit/api/openstack/compute/plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/__init__.py diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/__init__.py b/nova/tests/unit/api/openstack/compute/plugins/v3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/__init__.py diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/admin_only_action_common.py b/nova/tests/unit/api/openstack/compute/plugins/v3/admin_only_action_common.py new file mode 100644 index 0000000000..ce99d1069b --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/admin_only_action_common.py @@ -0,0 +1,263 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +from oslo.serialization import jsonutils +from oslo.utils import timeutils +import webob + +from nova.compute import vm_states +import nova.context +from nova import exception +from nova.openstack.common import uuidutils +from nova import test +from nova.tests.unit import fake_instance + + +class CommonMixin(object): + def setUp(self): + super(CommonMixin, self).setUp() + self.compute_api = None + self.context = nova.context.RequestContext('fake', 'fake') + + def _make_request(self, url, body): + req = webob.Request.blank('/v2/fake' + url) + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.content_type = 'application/json' + return req.get_response(self.app) + + def _stub_instance_get(self, uuid=None): + if uuid is None: + uuid = uuidutils.generate_uuid() + instance = fake_instance.fake_instance_obj(self.context, + id=1, uuid=uuid, vm_state=vm_states.ACTIVE, + task_state=None, launched_at=timeutils.utcnow()) + self.compute_api.get(self.context, uuid, expected_attrs=None, + want_objects=True).AndReturn(instance) + return instance + + def _stub_instance_get_failure(self, exc_info, uuid=None): + if uuid is None: + uuid = uuidutils.generate_uuid() + self.compute_api.get(self.context, uuid, expected_attrs=None, + want_objects=True).AndRaise(exc_info) + return uuid + + def _test_non_existing_instance(self, action, body_map=None): + uuid = uuidutils.generate_uuid() + self._stub_instance_get_failure( + exception.InstanceNotFound(instance_id=uuid), uuid=uuid) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % uuid, + {action: body_map.get(action)}) + self.assertEqual(404, res.status_int) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_action(self, action, body=None, method=None, + compute_api_args_map=None): + if method is None: + method = action + + compute_api_args_map = compute_api_args_map or {} + + instance = self._stub_instance_get() + + args, kwargs = compute_api_args_map.get(action, ((), {})) + getattr(self.compute_api, method)(self.context, instance, *args, + **kwargs) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {action: body}) + self.assertEqual(202, res.status_int) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_not_implemented_state(self, action, method=None): + if method is None: + method = action + + instance = self._stub_instance_get() + body = {} + compute_api_args_map = {} + args, kwargs = compute_api_args_map.get(action, ((), {})) + getattr(self.compute_api, method)(self.context, instance, + *args, **kwargs).AndRaise( + NotImplementedError()) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {action: body}) + self.assertEqual(501, res.status_int) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_invalid_state(self, action, method=None, body_map=None, + compute_api_args_map=None): + if method is None: + method = action + if body_map is None: + body_map = {} + if compute_api_args_map is None: + compute_api_args_map = {} + + instance = self._stub_instance_get() + + args, kwargs = compute_api_args_map.get(action, ((), {})) + + getattr(self.compute_api, method)(self.context, instance, + *args, **kwargs).AndRaise( + exception.InstanceInvalidState( + attr='vm_state', instance_uuid=instance.uuid, + state='foo', method=method)) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {action: body_map.get(action)}) + self.assertEqual(409, res.status_int) + self.assertIn("Cannot \'%(action)s\' instance %(id)s" + % {'action': action, 'id': instance.uuid}, res.body) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_locked_instance(self, action, method=None, body=None, + compute_api_args_map=None): + if method is None: + method = action + + compute_api_args_map = compute_api_args_map or {} + instance = self._stub_instance_get() + + args, kwargs = compute_api_args_map.get(action, ((), {})) + getattr(self.compute_api, method)(self.context, instance, *args, + **kwargs).AndRaise( + exception.InstanceIsLocked(instance_uuid=instance.uuid)) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {action: body}) + self.assertEqual(409, res.status_int) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_instance_not_found_in_compute_api(self, action, + method=None, body=None, compute_api_args_map=None): + if method is None: + method = action + + compute_api_args_map = compute_api_args_map or {} + + instance = self._stub_instance_get() + + args, kwargs = compute_api_args_map.get(action, ((), {})) + getattr(self.compute_api, method)(self.context, instance, *args, + **kwargs).AndRaise( + exception.InstanceNotFound(instance_id=instance.uuid)) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {action: body}) + self.assertEqual(404, res.status_int) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + +class CommonTests(CommonMixin, test.NoDBTestCase): + def _test_actions(self, actions, method_translations=None, body_map=None, + args_map=None): + method_translations = method_translations or {} + body_map = body_map or {} + args_map = args_map or {} + for action in actions: + method = method_translations.get(action) + body = body_map.get(action) + self.mox.StubOutWithMock(self.compute_api, method or action) + self._test_action(action, method=method, body=body, + compute_api_args_map=args_map) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + def _test_actions_instance_not_found_in_compute_api(self, + actions, method_translations=None, body_map=None, + args_map=None): + method_translations = method_translations or {} + body_map = body_map or {} + args_map = args_map or {} + for action in actions: + method = method_translations.get(action) + body = body_map.get(action) + self.mox.StubOutWithMock(self.compute_api, method or action) + self._test_instance_not_found_in_compute_api( + action, method=method, body=body, + compute_api_args_map=args_map) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + def _test_actions_with_non_existed_instance(self, actions, body_map=None): + body_map = body_map or {} + for action in actions: + self._test_non_existing_instance(action, + body_map=body_map) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + def _test_actions_raise_conflict_on_invalid_state( + self, actions, method_translations=None, body_map=None, + args_map=None): + method_translations = method_translations or {} + body_map = body_map or {} + args_map = args_map or {} + for action in actions: + method = method_translations.get(action) + self.mox.StubOutWithMock(self.compute_api, method or action) + self._test_invalid_state(action, method=method, + body_map=body_map, + compute_api_args_map=args_map) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + def _test_actions_with_locked_instance(self, actions, + method_translations=None, + body_map=None, args_map=None): + method_translations = method_translations or {} + body_map = body_map or {} + args_map = args_map or {} + for action in actions: + method = method_translations.get(action) + body = body_map.get(action) + self.mox.StubOutWithMock(self.compute_api, method or action) + self._test_locked_instance(action, method=method, body=body, + compute_api_args_map=args_map) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_access_ips.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_access_ips.py new file mode 100644 index 0000000000..44c1d5b5cd --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_access_ips.py @@ -0,0 +1,383 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +from oslo.serialization import jsonutils + +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import access_ips +from nova.api.openstack.compute.plugins.v3 import servers +from nova.api.openstack import wsgi +from nova.compute import api as compute_api +from nova import db +from nova import exception +from nova.objects import instance as instance_obj +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit.image import fake + + +class AccessIPsExtTest(test.NoDBTestCase): + def setUp(self): + super(AccessIPsExtTest, self).setUp() + self.access_ips_ext = access_ips.AccessIPs(None) + + def _test(self, func): + server_dict = {access_ips.AccessIPs.v4_key: '1.1.1.1', + access_ips.AccessIPs.v6_key: 'fe80::'} + create_kwargs = {} + func(server_dict, create_kwargs) + self.assertEqual(create_kwargs, {'access_ip_v4': '1.1.1.1', + 'access_ip_v6': 'fe80::'}) + + def _test_with_ipv4_only(self, func): + server_dict = {access_ips.AccessIPs.v4_key: '1.1.1.1'} + create_kwargs = {} + func(server_dict, create_kwargs) + self.assertEqual(create_kwargs, {'access_ip_v4': '1.1.1.1'}) + + def _test_with_ipv6_only(self, func): + server_dict = {access_ips.AccessIPs.v6_key: 'fe80::'} + create_kwargs = {} + func(server_dict, create_kwargs) + self.assertEqual(create_kwargs, {'access_ip_v6': 'fe80::'}) + + def _test_without_ipv4_and_ipv6(self, func): + server_dict = {} + create_kwargs = {} + func(server_dict, create_kwargs) + self.assertEqual(create_kwargs, {}) + + def _test_with_ipv4_null(self, func): + server_dict = {access_ips.AccessIPs.v4_key: None} + create_kwargs = {} + func(server_dict, create_kwargs) + self.assertEqual(create_kwargs, {'access_ip_v4': None}) + + def _test_with_ipv6_null(self, func): + server_dict = {access_ips.AccessIPs.v6_key: None} + create_kwargs = {} + func(server_dict, create_kwargs) + self.assertEqual(create_kwargs, {'access_ip_v6': None}) + + def _test_with_ipv4_blank(self, func): + server_dict = {access_ips.AccessIPs.v4_key: ''} + create_kwargs = {} + func(server_dict, create_kwargs) + self.assertEqual(create_kwargs, {'access_ip_v4': None}) + + def _test_with_ipv6_blank(self, func): + server_dict = {access_ips.AccessIPs.v6_key: ''} + create_kwargs = {} + func(server_dict, create_kwargs) + self.assertEqual(create_kwargs, {'access_ip_v6': None}) + + def test_server_create(self): + self._test(self.access_ips_ext.server_create) + + def test_server_create_with_ipv4_only(self): + self._test_with_ipv4_only(self.access_ips_ext.server_create) + + def test_server_create_with_ipv6_only(self): + self._test_with_ipv6_only(self.access_ips_ext.server_create) + + def test_server_create_without_ipv4_and_ipv6(self): + self._test_without_ipv4_and_ipv6(self.access_ips_ext.server_create) + + def test_server_create_with_ipv4_null(self): + self._test_with_ipv4_null(self.access_ips_ext.server_create) + + def test_server_create_with_ipv6_null(self): + self._test_with_ipv6_null(self.access_ips_ext.server_create) + + def test_server_create_with_ipv4_blank(self): + self._test_with_ipv4_blank(self.access_ips_ext.server_create) + + def test_server_create_with_ipv6_blank(self): + self._test_with_ipv6_blank(self.access_ips_ext.server_create) + + def test_server_update(self): + self._test(self.access_ips_ext.server_update) + + def test_server_update_with_ipv4_only(self): + self._test_with_ipv4_only(self.access_ips_ext.server_update) + + def test_server_update_with_ipv6_only(self): + self._test_with_ipv6_only(self.access_ips_ext.server_update) + + def test_server_update_without_ipv4_and_ipv6(self): + self._test_without_ipv4_and_ipv6(self.access_ips_ext.server_update) + + def test_server_update_with_ipv4_null(self): + self._test_with_ipv4_null(self.access_ips_ext.server_update) + + def test_server_update_with_ipv6_null(self): + self._test_with_ipv6_null(self.access_ips_ext.server_update) + + def test_server_update_with_ipv4_blank(self): + self._test_with_ipv4_blank(self.access_ips_ext.server_update) + + def test_server_update_with_ipv6_blank(self): + self._test_with_ipv6_blank(self.access_ips_ext.server_update) + + def test_server_rebuild(self): + self._test(self.access_ips_ext.server_rebuild) + + def test_server_rebuild_with_ipv4_only(self): + self._test_with_ipv4_only(self.access_ips_ext.server_rebuild) + + def test_server_rebuild_with_ipv6_only(self): + self._test_with_ipv6_only(self.access_ips_ext.server_rebuild) + + def test_server_rebuild_without_ipv4_and_ipv6(self): + self._test_without_ipv4_and_ipv6(self.access_ips_ext.server_rebuild) + + def test_server_rebuild_with_ipv4_null(self): + self._test_with_ipv4_null(self.access_ips_ext.server_rebuild) + + def test_server_rebuild_with_ipv6_null(self): + self._test_with_ipv6_null(self.access_ips_ext.server_rebuild) + + def test_server_rebuild_with_ipv4_blank(self): + self._test_with_ipv4_blank(self.access_ips_ext.server_rebuild) + + def test_server_rebuild_with_ipv6_blank(self): + self._test_with_ipv6_blank(self.access_ips_ext.server_rebuild) + + +class AccessIPsExtAPIValidationTest(test.TestCase): + def setUp(self): + super(AccessIPsExtAPIValidationTest, self).setUp() + + def fake_save(context, **kwargs): + pass + + def fake_rebuild(*args, **kwargs): + pass + + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers.ServersController(extension_info=ext_info) + fake.stub_out_image_service(self.stubs) + self.stubs.Set(db, 'instance_get_by_uuid', fakes.fake_instance_get()) + self.stubs.Set(instance_obj.Instance, 'save', fake_save) + self.stubs.Set(compute_api.API, 'rebuild', fake_rebuild) + + def _test_create(self, params): + body = { + 'server': { + 'name': 'server_test', + 'imageRef': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + 'flavorRef': 'http://localhost/123/flavors/3', + }, + } + body['server'].update(params) + + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.headers['content-type'] = 'application/json' + req.body = jsonutils.dumps(body) + self.controller.create(req, body=body) + + def _test_update(self, params): + body = { + 'server': { + }, + } + body['server'].update(params) + + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'PUT' + req.headers['content-type'] = 'application/json' + req.body = jsonutils.dumps(body) + self.controller.update(req, fakes.FAKE_UUID, body=body) + + def _test_rebuild(self, params): + body = { + 'rebuild': { + 'imageRef': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + }, + } + body['rebuild'].update(params) + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'PUT' + req.headers['content-type'] = 'application/json' + req.body = jsonutils.dumps(body) + self.controller._action_rebuild(req, fakes.FAKE_UUID, body=body) + + def test_create_server_with_access_ipv4(self): + params = {access_ips.AccessIPs.v4_key: '192.168.0.10'} + self._test_create(params) + + def test_create_server_with_invalid_access_ipv4(self): + params = {access_ips.AccessIPs.v4_key: '1.1.1.1.1.1'} + self.assertRaises(exception.ValidationError, self._test_create, params) + + def test_create_server_with_access_ipv6(self): + params = {access_ips.AccessIPs.v6_key: '2001:db8::9abc'} + self._test_create(params) + + def test_create_server_with_invalid_access_ipv6(self): + params = {access_ips.AccessIPs.v6_key: 'fe80:::::::'} + self.assertRaises(exception.ValidationError, self._test_create, params) + + def test_update_server_with_access_ipv4(self): + params = {access_ips.AccessIPs.v4_key: '192.168.0.10'} + self._test_update(params) + + def test_update_server_with_invalid_access_ipv4(self): + params = {access_ips.AccessIPs.v4_key: '1.1.1.1.1.1'} + self.assertRaises(exception.ValidationError, self._test_update, params) + + def test_update_server_with_access_ipv6(self): + params = {access_ips.AccessIPs.v6_key: '2001:db8::9abc'} + self._test_update(params) + + def test_update_server_with_invalid_access_ipv6(self): + params = {access_ips.AccessIPs.v6_key: 'fe80:::::::'} + self.assertRaises(exception.ValidationError, self._test_update, params) + + def test_rebuild_server_with_access_ipv4(self): + params = {access_ips.AccessIPs.v4_key: '192.168.0.10'} + self._test_rebuild(params) + + def test_rebuild_server_with_invalid_access_ipv4(self): + params = {access_ips.AccessIPs.v4_key: '1.1.1.1.1.1'} + self.assertRaises(exception.ValidationError, self._test_rebuild, + params) + + def test_rebuild_server_with_access_ipv6(self): + params = {access_ips.AccessIPs.v6_key: '2001:db8::9abc'} + self._test_rebuild(params) + + def test_rebuild_server_with_invalid_access_ipv6(self): + params = {access_ips.AccessIPs.v6_key: 'fe80:::::::'} + self.assertRaises(exception.ValidationError, self._test_rebuild, + params) + + +class AccessIPsControllerTest(test.NoDBTestCase): + def setUp(self): + super(AccessIPsControllerTest, self).setUp() + self.controller = access_ips.AccessIPsController() + + def _test_with_access_ips(self, func, kwargs={'id': 'fake'}): + req = wsgi.Request({'nova.context': + fakes.FakeRequestContext('fake_user', 'fake', + is_admin=True)}) + instance = {'uuid': 'fake', + 'access_ip_v4': '1.1.1.1', + 'access_ip_v6': 'fe80::'} + req.cache_db_instance(instance) + resp_obj = wsgi.ResponseObject( + {"server": {'id': 'fake'}}) + func(req, resp_obj, **kwargs) + self.assertEqual(resp_obj.obj['server'][access_ips.AccessIPs.v4_key], + '1.1.1.1') + self.assertEqual(resp_obj.obj['server'][access_ips.AccessIPs.v6_key], + 'fe80::') + + def _test_without_access_ips(self, func, kwargs={'id': 'fake'}): + req = wsgi.Request({'nova.context': + fakes.FakeRequestContext('fake_user', 'fake', + is_admin=True)}) + instance = {'uuid': 'fake', + 'access_ip_v4': None, + 'access_ip_v6': None} + req.cache_db_instance(instance) + resp_obj = wsgi.ResponseObject( + {"server": {'id': 'fake'}}) + func(req, resp_obj, **kwargs) + self.assertEqual(resp_obj.obj['server'][access_ips.AccessIPs.v4_key], + '') + self.assertEqual(resp_obj.obj['server'][access_ips.AccessIPs.v6_key], + '') + + def test_create(self): + self._test_with_access_ips(self.controller.create, {'body': {}}) + + def test_create_without_access_ips(self): + self._test_with_access_ips(self.controller.create, {'body': {}}) + + def test_show(self): + self._test_with_access_ips(self.controller.show) + + def test_show_without_access_ips(self): + self._test_without_access_ips(self.controller.show) + + def test_detail(self): + req = wsgi.Request({'nova.context': + fakes.FakeRequestContext('fake_user', 'fake', + is_admin=True)}) + instance1 = {'uuid': 'fake1', + 'access_ip_v4': '1.1.1.1', + 'access_ip_v6': 'fe80::'} + instance2 = {'uuid': 'fake2', + 'access_ip_v4': '1.1.1.2', + 'access_ip_v6': 'fe81::'} + req.cache_db_instance(instance1) + req.cache_db_instance(instance2) + resp_obj = wsgi.ResponseObject( + {"servers": [{'id': 'fake1'}, {'id': 'fake2'}]}) + self.controller.detail(req, resp_obj) + self.assertEqual( + resp_obj.obj['servers'][0][access_ips.AccessIPs.v4_key], + '1.1.1.1') + self.assertEqual( + resp_obj.obj['servers'][0][access_ips.AccessIPs.v6_key], + 'fe80::') + self.assertEqual( + resp_obj.obj['servers'][1][access_ips.AccessIPs.v4_key], + '1.1.1.2') + self.assertEqual( + resp_obj.obj['servers'][1][access_ips.AccessIPs.v6_key], + 'fe81::') + + def test_detail_without_access_ips(self): + req = wsgi.Request({'nova.context': + fakes.FakeRequestContext('fake_user', 'fake', + is_admin=True)}) + instance1 = {'uuid': 'fake1', + 'access_ip_v4': None, + 'access_ip_v6': None} + instance2 = {'uuid': 'fake2', + 'access_ip_v4': None, + 'access_ip_v6': None} + req.cache_db_instance(instance1) + req.cache_db_instance(instance2) + resp_obj = wsgi.ResponseObject( + {"servers": [{'id': 'fake1'}, {'id': 'fake2'}]}) + self.controller.detail(req, resp_obj) + self.assertEqual( + resp_obj.obj['servers'][0][access_ips.AccessIPs.v4_key], '') + self.assertEqual( + resp_obj.obj['servers'][0][access_ips.AccessIPs.v6_key], '') + self.assertEqual( + resp_obj.obj['servers'][1][access_ips.AccessIPs.v4_key], '') + self.assertEqual( + resp_obj.obj['servers'][1][access_ips.AccessIPs.v6_key], '') + + def test_update(self): + self._test_with_access_ips(self.controller.update, {'id': 'fake', + 'body': {}}) + + def test_update_without_access_ips(self): + self._test_without_access_ips(self.controller.update, {'id': 'fake', + 'body': {}}) + + def test_rebuild(self): + self._test_with_access_ips(self.controller.rebuild, {'id': 'fake', + 'body': {}}) + + def test_rebuild_without_access_ips(self): + self._test_without_access_ips(self.controller.rebuild, {'id': 'fake', + 'body': {}}) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_console_auth_tokens.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_console_auth_tokens.py new file mode 100644 index 0000000000..259906c535 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_console_auth_tokens.py @@ -0,0 +1,95 @@ +# Copyright 2013 Cloudbase Solutions Srl +# 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. + +from oslo.serialization import jsonutils + +from nova.consoleauth import rpcapi as consoleauth_rpcapi +from nova import context +from nova import test +from nova.tests.unit.api.openstack import fakes + + +_FAKE_CONNECT_INFO = {'instance_uuid': 'fake_instance_uuid', + 'host': 'fake_host', + 'port': 'fake_port', + 'internal_access_path': 'fake_access_path', + 'console_type': 'rdp-html5'} + + +def _fake_check_token(self, context, token): + return _FAKE_CONNECT_INFO + + +def _fake_check_token_not_found(self, context, token): + return None + + +def _fake_check_token_unauthorized(self, context, token): + connect_info = _FAKE_CONNECT_INFO + connect_info['console_type'] = 'unauthorized_console_type' + return connect_info + + +class ConsoleAuthTokensExtensionTest(test.TestCase): + + _FAKE_URL = '/v2/fake/os-console-auth-tokens/1' + + _EXPECTED_OUTPUT = {'console': {'instance_uuid': 'fake_instance_uuid', + 'host': 'fake_host', + 'port': 'fake_port', + 'internal_access_path': + 'fake_access_path'}} + + def setUp(self): + super(ConsoleAuthTokensExtensionTest, self).setUp() + self.stubs.Set(consoleauth_rpcapi.ConsoleAuthAPI, 'check_token', + _fake_check_token) + + ctxt = self._get_admin_context() + self.app = fakes.wsgi_app_v21(init_only=('os-console-auth-tokens'), + fake_auth_context=ctxt) + + def _get_admin_context(self): + ctxt = context.get_admin_context() + ctxt.user_id = 'fake' + ctxt.project_id = 'fake' + return ctxt + + def _create_request(self): + req = fakes.HTTPRequestV3.blank(self._FAKE_URL) + req.method = "GET" + req.headers["content-type"] = "application/json" + return req + + def test_get_console_connect_info(self): + req = self._create_request() + res = req.get_response(self.app) + self.assertEqual(200, res.status_int) + output = jsonutils.loads(res.body) + self.assertEqual(self._EXPECTED_OUTPUT, output) + + def test_get_console_connect_info_token_not_found(self): + self.stubs.Set(consoleauth_rpcapi.ConsoleAuthAPI, 'check_token', + _fake_check_token_not_found) + req = self._create_request() + res = req.get_response(self.app) + self.assertEqual(404, res.status_int) + + def test_get_console_connect_info_unauthorized_console_type(self): + self.stubs.Set(consoleauth_rpcapi.ConsoleAuthAPI, 'check_token', + _fake_check_token_unauthorized) + req = self._create_request() + res = req.get_response(self.app) + self.assertEqual(401, res.status_int) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_consoles.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_consoles.py new file mode 100644 index 0000000000..d3ba83dcbc --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_consoles.py @@ -0,0 +1,270 @@ +# Copyright 2010-2011 OpenStack Foundation +# Copyright 2011 Piston Cloud Computing, Inc. +# 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 datetime +import uuid as stdlib_uuid + +from oslo.utils import timeutils +import webob + +from nova.api.openstack.compute.plugins.v3 import consoles +from nova.compute import vm_states +from nova import console +from nova import db +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import matchers + + +FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + + +class FakeInstanceDB(object): + + def __init__(self): + self.instances_by_id = {} + self.ids_by_uuid = {} + self.max_id = 0 + + def return_server_by_id(self, context, id): + if id not in self.instances_by_id: + self._add_server(id=id) + return dict(self.instances_by_id[id]) + + def return_server_by_uuid(self, context, uuid): + if uuid not in self.ids_by_uuid: + self._add_server(uuid=uuid) + return dict(self.instances_by_id[self.ids_by_uuid[uuid]]) + + def _add_server(self, id=None, uuid=None): + if id is None: + id = self.max_id + 1 + if uuid is None: + uuid = str(stdlib_uuid.uuid4()) + instance = stub_instance(id, uuid=uuid) + self.instances_by_id[id] = instance + self.ids_by_uuid[uuid] = id + if id > self.max_id: + self.max_id = id + + +def stub_instance(id, user_id='fake', project_id='fake', host=None, + vm_state=None, task_state=None, + reservation_id="", uuid=FAKE_UUID, image_ref="10", + flavor_id="1", name=None, key_name='', + access_ipv4=None, access_ipv6=None, progress=0): + + if host is not None: + host = str(host) + + if key_name: + key_data = 'FAKE' + else: + key_data = '' + + # ReservationID isn't sent back, hack it in there. + server_name = name or "server%s" % id + if reservation_id != "": + server_name = "reservation_%s" % (reservation_id, ) + + instance = { + "id": int(id), + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "admin_password": "", + "user_id": user_id, + "project_id": project_id, + "image_ref": image_ref, + "kernel_id": "", + "ramdisk_id": "", + "launch_index": 0, + "key_name": key_name, + "key_data": key_data, + "vm_state": vm_state or vm_states.BUILDING, + "task_state": task_state, + "memory_mb": 0, + "vcpus": 0, + "root_gb": 0, + "hostname": "", + "host": host, + "instance_type": {}, + "user_data": "", + "reservation_id": reservation_id, + "mac_address": "", + "scheduled_at": timeutils.utcnow(), + "launched_at": timeutils.utcnow(), + "terminated_at": timeutils.utcnow(), + "availability_zone": "", + "display_name": server_name, + "display_description": "", + "locked": False, + "metadata": [], + "access_ip_v4": access_ipv4, + "access_ip_v6": access_ipv6, + "uuid": uuid, + "progress": progress} + + return instance + + +class ConsolesControllerTest(test.NoDBTestCase): + def setUp(self): + super(ConsolesControllerTest, self).setUp() + self.flags(verbose=True) + self.instance_db = FakeInstanceDB() + self.stubs.Set(db, 'instance_get', + self.instance_db.return_server_by_id) + self.stubs.Set(db, 'instance_get_by_uuid', + self.instance_db.return_server_by_uuid) + self.uuid = str(stdlib_uuid.uuid4()) + self.url = '/v3/fake/servers/%s/consoles' % self.uuid + self.controller = consoles.ConsolesController() + + def test_create_console(self): + def fake_create_console(cons_self, context, instance_id): + self.assertEqual(instance_id, self.uuid) + return {} + self.stubs.Set(console.api.API, 'create_console', fake_create_console) + + req = fakes.HTTPRequestV3.blank(self.url) + self.controller.create(req, self.uuid, None) + self.assertEqual(self.controller.create.wsgi_code, 201) + + def test_create_console_unknown_instance(self): + def fake_create_console(cons_self, context, instance_id): + raise exception.InstanceNotFound(instance_id=instance_id) + self.stubs.Set(console.api.API, 'create_console', fake_create_console) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.create, + req, self.uuid, None) + + def test_show_console(self): + def fake_get_console(cons_self, context, instance_id, console_id): + self.assertEqual(instance_id, self.uuid) + self.assertEqual(console_id, 20) + pool = dict(console_type='fake_type', + public_hostname='fake_hostname') + return dict(id=console_id, password='fake_password', + port='fake_port', pool=pool, instance_name='inst-0001') + + expected = {'console': {'id': 20, + 'port': 'fake_port', + 'host': 'fake_hostname', + 'password': 'fake_password', + 'instance_name': 'inst-0001', + 'console_type': 'fake_type'}} + + self.stubs.Set(console.api.API, 'get_console', fake_get_console) + + req = fakes.HTTPRequestV3.blank(self.url + '/20') + res_dict = self.controller.show(req, self.uuid, '20') + self.assertThat(res_dict, matchers.DictMatches(expected)) + + def test_show_console_unknown_console(self): + def fake_get_console(cons_self, context, instance_id, console_id): + raise exception.ConsoleNotFound(console_id=console_id) + + self.stubs.Set(console.api.API, 'get_console', fake_get_console) + + req = fakes.HTTPRequestV3.blank(self.url + '/20') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, self.uuid, '20') + + def test_show_console_unknown_instance(self): + def fake_get_console(cons_self, context, instance_id, console_id): + raise exception.ConsoleNotFoundForInstance( + instance_uuid=instance_id) + + self.stubs.Set(console.api.API, 'get_console', fake_get_console) + + req = fakes.HTTPRequestV3.blank(self.url + '/20') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, self.uuid, '20') + + def test_list_consoles(self): + def fake_get_consoles(cons_self, context, instance_id): + self.assertEqual(instance_id, self.uuid) + + pool1 = dict(console_type='fake_type', + public_hostname='fake_hostname') + cons1 = dict(id=10, password='fake_password', + port='fake_port', pool=pool1) + pool2 = dict(console_type='fake_type2', + public_hostname='fake_hostname2') + cons2 = dict(id=11, password='fake_password2', + port='fake_port2', pool=pool2) + return [cons1, cons2] + + expected = {'consoles': + [{'id': 10, 'console_type': 'fake_type'}, + {'id': 11, 'console_type': 'fake_type2'}]} + + self.stubs.Set(console.api.API, 'get_consoles', fake_get_consoles) + + req = fakes.HTTPRequestV3.blank(self.url) + res_dict = self.controller.index(req, self.uuid) + self.assertThat(res_dict, matchers.DictMatches(expected)) + + def test_list_consoles_unknown_instance(self): + def fake_get_consoles(cons_self, context, instance_id): + raise exception.InstanceNotFound(instance_id=instance_id) + self.stubs.Set(console.api.API, 'get_consoles', fake_get_consoles) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.index, + req, self.uuid) + + def test_delete_console(self): + def fake_get_console(cons_self, context, instance_id, console_id): + self.assertEqual(instance_id, self.uuid) + self.assertEqual(console_id, 20) + pool = dict(console_type='fake_type', + public_hostname='fake_hostname') + return dict(id=console_id, password='fake_password', + port='fake_port', pool=pool) + + def fake_delete_console(cons_self, context, instance_id, console_id): + self.assertEqual(instance_id, self.uuid) + self.assertEqual(console_id, 20) + + self.stubs.Set(console.api.API, 'get_console', fake_get_console) + self.stubs.Set(console.api.API, 'delete_console', fake_delete_console) + + req = fakes.HTTPRequestV3.blank(self.url + '/20') + self.controller.delete(req, self.uuid, '20') + + def test_delete_console_unknown_console(self): + def fake_delete_console(cons_self, context, instance_id, console_id): + raise exception.ConsoleNotFound(console_id=console_id) + + self.stubs.Set(console.api.API, 'delete_console', fake_delete_console) + + req = fakes.HTTPRequestV3.blank(self.url + '/20') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, self.uuid, '20') + + def test_delete_console_unknown_instance(self): + def fake_delete_console(cons_self, context, instance_id, console_id): + raise exception.ConsoleNotFoundForInstance( + instance_uuid=instance_id) + + self.stubs.Set(console.api.API, 'delete_console', fake_delete_console) + + req = fakes.HTTPRequestV3.blank(self.url + '/20') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, self.uuid, '20') diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_create_backup.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_create_backup.py new file mode 100644 index 0000000000..83701090f8 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_create_backup.py @@ -0,0 +1,261 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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. + +from nova.api.openstack import common +from nova.api.openstack.compute.plugins.v3 import create_backup +from nova.openstack.common import uuidutils +from nova import test +from nova.tests.unit.api.openstack.compute.plugins.v3 import \ + admin_only_action_common +from nova.tests.unit.api.openstack import fakes + + +class CreateBackupTests(admin_only_action_common.CommonMixin, + test.NoDBTestCase): + def setUp(self): + super(CreateBackupTests, self).setUp() + self.controller = create_backup.CreateBackupController() + self.compute_api = self.controller.compute_api + + def _fake_controller(*args, **kwargs): + return self.controller + + self.stubs.Set(create_backup, 'CreateBackupController', + _fake_controller) + self.app = fakes.wsgi_app_v21(init_only=('servers', + 'os-create-backup'), + fake_auth_context=self.context) + self.mox.StubOutWithMock(self.compute_api, 'get') + self.mox.StubOutWithMock(common, + 'check_img_metadata_properties_quota') + self.mox.StubOutWithMock(self.compute_api, 'backup') + + def _make_url(self, uuid=None): + if uuid is None: + uuid = uuidutils.generate_uuid() + return '/servers/%s/action' % uuid + + def test_create_backup_with_metadata(self): + metadata = {'123': 'asdf'} + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + 'metadata': metadata, + }, + } + + image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1', + properties=metadata) + + common.check_img_metadata_properties_quota(self.context, metadata) + instance = self._stub_instance_get() + self.compute_api.backup(self.context, instance, 'Backup 1', + 'daily', 1, + extra_properties=metadata).AndReturn(image) + + self.mox.ReplayAll() + + res = self._make_request(self._make_url(instance.uuid), body) + self.assertEqual(202, res.status_int) + self.assertIn('fake-image-id', res.headers['Location']) + + def test_create_backup_no_name(self): + # Name is required for backups. + body = { + 'createBackup': { + 'backup_type': 'daily', + 'rotation': 1, + }, + } + res = self._make_request(self._make_url(), body) + self.assertEqual(400, res.status_int) + + def test_create_backup_no_rotation(self): + # Rotation is required for backup requests. + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + }, + } + res = self._make_request(self._make_url(), body) + self.assertEqual(400, res.status_int) + + def test_create_backup_negative_rotation(self): + """Rotation must be greater than or equal to zero + for backup requests + """ + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': -1, + }, + } + res = self._make_request(self._make_url(), body) + self.assertEqual(400, res.status_int) + + def test_create_backup_negative_rotation_with_string_number(self): + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': '-1', + }, + } + res = self._make_request(self._make_url('fake'), body) + self.assertEqual(400, res.status_int) + + def test_create_backup_no_backup_type(self): + # Backup Type (daily or weekly) is required for backup requests. + body = { + 'createBackup': { + 'name': 'Backup 1', + 'rotation': 1, + }, + } + res = self._make_request(self._make_url(), body) + self.assertEqual(400, res.status_int) + + def test_create_backup_non_dict_metadata(self): + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + 'metadata': 'non_dict', + }, + } + res = self._make_request(self._make_url('fake'), body) + self.assertEqual(400, res.status_int) + + def test_create_backup_bad_entity(self): + body = {'createBackup': 'go'} + res = self._make_request(self._make_url(), body) + self.assertEqual(400, res.status_int) + + def test_create_backup_rotation_is_zero(self): + # The happy path for creating backups if rotation is zero. + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 0, + }, + } + + image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1', + properties={}) + common.check_img_metadata_properties_quota(self.context, {}) + instance = self._stub_instance_get() + self.compute_api.backup(self.context, instance, 'Backup 1', + 'daily', 0, + extra_properties={}).AndReturn(image) + + self.mox.ReplayAll() + + res = self._make_request(self._make_url(instance.uuid), body) + self.assertEqual(202, res.status_int) + self.assertNotIn('Location', res.headers) + + def test_create_backup_rotation_is_positive(self): + # The happy path for creating backups if rotation is positive. + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + }, + } + + image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1', + properties={}) + common.check_img_metadata_properties_quota(self.context, {}) + instance = self._stub_instance_get() + self.compute_api.backup(self.context, instance, 'Backup 1', + 'daily', 1, + extra_properties={}).AndReturn(image) + + self.mox.ReplayAll() + + res = self._make_request(self._make_url(instance.uuid), body) + self.assertEqual(202, res.status_int) + self.assertIn('fake-image-id', res.headers['Location']) + + def test_create_backup_rotation_is_string_number(self): + body = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': '1', + }, + } + + image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1', + properties={}) + common.check_img_metadata_properties_quota(self.context, {}) + instance = self._stub_instance_get() + self.compute_api.backup(self.context, instance, 'Backup 1', + 'daily', 1, + extra_properties={}).AndReturn(image) + + self.mox.ReplayAll() + + res = self._make_request(self._make_url(instance['uuid']), body) + self.assertEqual(202, res.status_int) + self.assertIn('fake-image-id', res.headers['Location']) + + def test_create_backup_raises_conflict_on_invalid_state(self): + body_map = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + }, + } + args_map = { + 'createBackup': ( + ('Backup 1', 'daily', 1), {'extra_properties': {}} + ), + } + common.check_img_metadata_properties_quota(self.context, {}) + self._test_invalid_state('createBackup', method='backup', + body_map=body_map, + compute_api_args_map=args_map) + + def test_create_backup_with_non_existed_instance(self): + body_map = { + 'createBackup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + }, + } + common.check_img_metadata_properties_quota(self.context, {}) + self._test_non_existing_instance('createBackup', + body_map=body_map) + + def test_create_backup_with_invalid_create_backup(self): + body = { + 'createBackupup': { + 'name': 'Backup 1', + 'backup_type': 'daily', + 'rotation': 1, + }, + } + res = self._make_request(self._make_url(), body) + self.assertEqual(400, res.status_int) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_extended_volumes.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_extended_volumes.py new file mode 100644 index 0000000000..dc6dd2898f --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_extended_volumes.py @@ -0,0 +1,387 @@ +# Copyright 2013 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 mock +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute.plugins.v3 import extended_volumes +from nova import compute +from nova import context +from nova import db +from nova import exception +from nova import objects +from nova.objects import instance as instance_obj +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_block_device +from nova.tests.unit import fake_instance +from nova import volume + +UUID1 = '00000000-0000-0000-0000-000000000001' +UUID2 = '00000000-0000-0000-0000-000000000002' +UUID3 = '00000000-0000-0000-0000-000000000003' + + +def fake_compute_get(*args, **kwargs): + inst = fakes.stub_instance(1, uuid=UUID1) + return fake_instance.fake_instance_obj(args[1], **inst) + + +def fake_compute_get_not_found(*args, **kwargs): + raise exception.InstanceNotFound(instance_id=UUID1) + + +def fake_compute_get_all(*args, **kwargs): + db_list = [fakes.stub_instance(1), fakes.stub_instance(2)] + fields = instance_obj.INSTANCE_DEFAULT_FIELDS + return instance_obj._make_instance_list(args[1], + objects.InstanceList(), + db_list, fields) + + +def fake_bdms_get_all_by_instance(*args, **kwargs): + return [fake_block_device.FakeDbBlockDeviceDict( + {'volume_id': UUID1, 'source_type': 'volume', + 'destination_type': 'volume', 'id': 1}), + fake_block_device.FakeDbBlockDeviceDict( + {'volume_id': UUID2, 'source_type': 'volume', + 'destination_type': 'volume', 'id': 2})] + + +def fake_attach_volume(self, context, instance, volume_id, + device, disk_bus, device_type): + pass + + +def fake_attach_volume_not_found_vol(self, context, instance, volume_id, + device, disk_bus, device_type): + raise exception.VolumeNotFound(volume_id=volume_id) + + +def fake_attach_volume_invalid_device_path(self, context, instance, + volume_id, device, disk_bus, + device_type): + raise exception.InvalidDevicePath(path=device) + + +def fake_attach_volume_instance_invalid_state(self, context, instance, + volume_id, device, disk_bus, + device_type): + raise exception.InstanceInvalidState(instance_uuid=UUID1, state='', + method='', attr='') + + +def fake_attach_volume_invalid_volume(self, context, instance, + volume_id, device, disk_bus, + device_type): + raise exception.InvalidVolume(reason='') + + +def fake_detach_volume(self, context, instance, volume): + pass + + +def fake_swap_volume(self, context, instance, + old_volume_id, new_volume_id): + pass + + +def fake_swap_volume_invalid_volume(self, context, instance, + volume_id, device): + raise exception.InvalidVolume(reason='', volume_id=volume_id) + + +def fake_swap_volume_unattached_volume(self, context, instance, + volume_id, device): + raise exception.VolumeUnattached(reason='', volume_id=volume_id) + + +def fake_detach_volume_invalid_volume(self, context, instance, volume): + raise exception.InvalidVolume(reason='') + + +def fake_swap_volume_instance_invalid_state(self, context, instance, + volume_id, device): + raise exception.InstanceInvalidState(instance_uuid=UUID1, state='', + method='', attr='') + + +def fake_volume_get(*args, **kwargs): + pass + + +def fake_volume_get_not_found(*args, **kwargs): + raise exception.VolumeNotFound(volume_id=UUID1) + + +class ExtendedVolumesTest(test.TestCase): + content_type = 'application/json' + prefix = 'os-extended-volumes:' + + def setUp(self): + super(ExtendedVolumesTest, self).setUp() + self.Controller = extended_volumes.ExtendedVolumesController() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(compute.api.API, 'get', fake_compute_get) + self.stubs.Set(compute.api.API, 'get_all', fake_compute_get_all) + self.stubs.Set(db, 'block_device_mapping_get_all_by_instance', + fake_bdms_get_all_by_instance) + self.stubs.Set(volume.cinder.API, 'get', fake_volume_get) + self.stubs.Set(compute.api.API, 'detach_volume', fake_detach_volume) + self.stubs.Set(compute.api.API, 'attach_volume', fake_attach_volume) + self.app = fakes.wsgi_app_v21(init_only=('os-extended-volumes', + 'servers')) + return_server = fakes.fake_instance_get() + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + def _make_request(self, url, body=None): + base_url = '/v2/fake/servers' + req = webob.Request.blank(base_url + url) + req.headers['Accept'] = self.content_type + if body: + req.body = jsonutils.dumps(body) + req.method = 'POST' + req.content_type = 'application/json' + res = req.get_response(self.app) + return res + + def _get_server(self, body): + return jsonutils.loads(body).get('server') + + def _get_servers(self, body): + return jsonutils.loads(body).get('servers') + + def test_show(self): + url = '/%s' % UUID1 + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + server = self._get_server(res.body) + exp_volumes = [{'id': UUID1}, {'id': UUID2}] + if self.content_type == 'application/json': + actual = server.get('%svolumes_attached' % self.prefix) + self.assertEqual(exp_volumes, actual) + + def test_detail(self): + url = '/detail' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + exp_volumes = [{'id': UUID1}, {'id': UUID2}] + for i, server in enumerate(self._get_servers(res.body)): + if self.content_type == 'application/json': + actual = server.get('%svolumes_attached' % self.prefix) + self.assertEqual(exp_volumes, actual) + + def test_detach(self): + url = "/%s/action" % UUID1 + res = self._make_request(url, {"detach": {"volume_id": UUID1}}) + self.assertEqual(res.status_int, 202) + + def test_detach_volume_from_locked_server(self): + url = "/%s/action" % UUID1 + self.stubs.Set(compute.api.API, 'detach_volume', + fakes.fake_actions_to_locked_server) + res = self._make_request(url, {"detach": {"volume_id": UUID1}}) + self.assertEqual(res.status_int, 409) + + def test_detach_with_non_existed_vol(self): + url = "/%s/action" % UUID1 + self.stubs.Set(volume.cinder.API, 'get', fake_volume_get_not_found) + res = self._make_request(url, {"detach": {"volume_id": UUID2}}) + self.assertEqual(res.status_int, 404) + + def test_detach_with_non_existed_instance(self): + url = "/%s/action" % UUID1 + self.stubs.Set(compute.api.API, 'get', fake_compute_get_not_found) + res = self._make_request(url, {"detach": {"volume_id": UUID2}}) + self.assertEqual(res.status_int, 404) + + def test_detach_with_invalid_vol(self): + url = "/%s/action" % UUID1 + self.stubs.Set(compute.api.API, 'detach_volume', + fake_detach_volume_invalid_volume) + res = self._make_request(url, {"detach": {"volume_id": UUID2}}) + self.assertEqual(res.status_int, 400) + + def test_detach_with_bad_id(self): + url = "/%s/action" % UUID1 + res = self._make_request(url, {"detach": {"volume_id": 'xxx'}}) + self.assertEqual(res.status_int, 400) + + def test_detach_without_id(self): + url = "/%s/action" % UUID1 + res = self._make_request(url, {"detach": {}}) + self.assertEqual(res.status_int, 400) + + def test_detach_volume_with_invalid_request(self): + url = "/%s/action" % UUID1 + res = self._make_request(url, {"detach": None}) + self.assertEqual(res.status_int, 400) + + @mock.patch('nova.objects.BlockDeviceMapping.is_root', + new_callable=mock.PropertyMock) + def test_detach_volume_root(self, mock_isroot): + url = "/%s/action" % UUID1 + mock_isroot.return_value = True + res = self._make_request(url, {"detach": {"volume_id": UUID1}}) + self.assertEqual(res.status_int, 403) + + def test_attach_volume(self): + url = "/%s/action" % UUID1 + res = self._make_request(url, {"attach": {"volume_id": UUID1}}) + self.assertEqual(res.status_int, 202) + + def test_attach_volume_to_locked_server(self): + url = "/%s/action" % UUID1 + self.stubs.Set(compute.api.API, 'attach_volume', + fakes.fake_actions_to_locked_server) + res = self._make_request(url, {"attach": {"volume_id": UUID1}}) + self.assertEqual(res.status_int, 409) + + def test_attach_volume_disk_bus_and_disk_dev(self): + url = "/%s/action" % UUID1 + self._make_request(url, {"attach": {"volume_id": UUID1, + "device": "/dev/vdb", + "disk_bus": "ide", + "device_type": "cdrom"}}) + + def test_attach_volume_with_bad_id(self): + url = "/%s/action" % UUID1 + res = self._make_request(url, {"attach": {"volume_id": 'xxx'}}) + self.assertEqual(res.status_int, 400) + + def test_attach_volume_without_id(self): + url = "/%s/action" % UUID1 + res = self._make_request(url, {"attach": {}}) + self.assertEqual(res.status_int, 400) + + def test_attach_volume_with_invalid_request(self): + url = "/%s/action" % UUID1 + res = self._make_request(url, {"attach": None}) + self.assertEqual(res.status_int, 400) + + def test_attach_volume_with_non_existe_vol(self): + url = "/%s/action" % UUID1 + self.stubs.Set(compute.api.API, 'attach_volume', + fake_attach_volume_not_found_vol) + res = self._make_request(url, {"attach": {"volume_id": UUID1}}) + self.assertEqual(res.status_int, 404) + + def test_attach_volume_with_non_existed_instance(self): + url = "/%s/action" % UUID1 + self.stubs.Set(compute.api.API, 'get', fake_compute_get_not_found) + res = self._make_request(url, {"attach": {"volume_id": UUID1}}) + self.assertEqual(res.status_int, 404) + + def test_attach_volume_with_invalid_device_path(self): + url = "/%s/action" % UUID1 + self.stubs.Set(compute.api.API, 'attach_volume', + fake_attach_volume_invalid_device_path) + res = self._make_request(url, {"attach": {"volume_id": UUID1, + 'device': 'xxx'}}) + self.assertEqual(res.status_int, 400) + + def test_attach_volume_with_instance_invalid_state(self): + url = "/%s/action" % UUID1 + self.stubs.Set(compute.api.API, 'attach_volume', + fake_attach_volume_instance_invalid_state) + res = self._make_request(url, {"attach": {"volume_id": UUID1}}) + self.assertEqual(res.status_int, 409) + + def test_attach_volume_with_invalid_volume(self): + url = "/%s/action" % UUID1 + self.stubs.Set(compute.api.API, 'attach_volume', + fake_attach_volume_invalid_volume) + res = self._make_request(url, {"attach": {"volume_id": UUID1}}) + self.assertEqual(res.status_int, 400) + + def test_attach_volume_with_invalid_request_body(self): + url = "/%s/action" % UUID1 + self.stubs.Set(compute.api.API, 'attach_volume', + fake_attach_volume_invalid_volume) + res = self._make_request(url, {"attach": None}) + self.assertEqual(res.status_int, 400) + + def _test_swap(self, uuid=UUID1, body=None): + body = body or {'swap_volume_attachment': {'old_volume_id': uuid, + 'new_volume_id': UUID2}} + req = webob.Request.blank('/v2/fake/servers/%s/action' % UUID1) + req.method = 'PUT' + req.body = jsonutils.dumps({}) + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = context.get_admin_context() + return self.Controller.swap(req, UUID1, body=body) + + def test_swap_volume(self): + self.stubs.Set(compute.api.API, 'swap_volume', fake_swap_volume) + # Check any exceptions don't happen and status code + self._test_swap() + self.assertEqual(202, self.Controller.swap.wsgi_code) + + def test_swap_volume_for_locked_server(self): + def fake_swap_volume_for_locked_server(self, context, instance, + old_volume, new_volume): + raise exception.InstanceIsLocked(instance_uuid=instance['uuid']) + self.stubs.Set(compute.api.API, 'swap_volume', + fake_swap_volume_for_locked_server) + self.assertRaises(webob.exc.HTTPConflict, self._test_swap) + + def test_swap_volume_for_locked_server_new(self): + self.stubs.Set(compute.api.API, 'swap_volume', + fakes.fake_actions_to_locked_server) + self.assertRaises(webob.exc.HTTPConflict, self._test_swap) + + def test_swap_volume_instance_not_found(self): + self.stubs.Set(compute.api.API, 'get', fake_compute_get_not_found) + self.assertRaises(webob.exc.HTTPNotFound, self._test_swap) + + def test_swap_volume_with_bad_action(self): + self.stubs.Set(compute.api.API, 'swap_volume', fake_swap_volume) + body = {'swap_volume_attachment_bad_action': None} + self.assertRaises(exception.ValidationError, self._test_swap, + body=body) + + def test_swap_volume_with_invalid_body(self): + self.stubs.Set(compute.api.API, 'swap_volume', fake_swap_volume) + body = {'swap_volume_attachment': {'bad_volume_id_body': UUID1, + 'new_volume_id': UUID2}} + self.assertRaises(exception.ValidationError, self._test_swap, + body=body) + + def test_swap_volume_with_invalid_volume(self): + self.stubs.Set(compute.api.API, 'swap_volume', + fake_swap_volume_invalid_volume) + self.assertRaises(webob.exc.HTTPBadRequest, self._test_swap) + + def test_swap_volume_with_unattached_volume(self): + self.stubs.Set(compute.api.API, 'swap_volume', + fake_swap_volume_unattached_volume) + self.assertRaises(webob.exc.HTTPNotFound, self._test_swap) + + def test_swap_volume_with_bad_state_instance(self): + self.stubs.Set(compute.api.API, 'swap_volume', + fake_swap_volume_instance_invalid_state) + self.assertRaises(webob.exc.HTTPConflict, self._test_swap) + + def test_swap_volume_no_attachment(self): + self.stubs.Set(compute.api.API, 'swap_volume', fake_swap_volume) + self.assertRaises(webob.exc.HTTPNotFound, self._test_swap, UUID3) + + def test_swap_volume_not_found(self): + self.stubs.Set(compute.api.API, 'swap_volume', fake_swap_volume) + self.stubs.Set(volume.cinder.API, 'get', fake_volume_get_not_found) + self.assertRaises(webob.exc.HTTPNotFound, self._test_swap) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_extension_info.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_extension_info.py new file mode 100644 index 0000000000..ee4e9d18b9 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_extension_info.py @@ -0,0 +1,98 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import extension_info +from nova import exception +from nova import policy +from nova import test +from nova.tests.unit.api.openstack import fakes + + +class fake_extension(object): + def __init__(self, name, alias, description, version): + self.name = name + self.alias = alias + self.__doc__ = description + self.version = version + + +fake_extensions = { + 'ext1-alias': fake_extension('ext1', 'ext1-alias', 'ext1 description', 1), + 'ext2-alias': fake_extension('ext2', 'ext2-alias', 'ext2 description', 2), + 'ext3-alias': fake_extension('ext3', 'ext3-alias', 'ext3 description', 1) +} + + +def fake_policy_enforce(context, action, target, do_raise=True): + return True + + +def fake_policy_enforce_selective(context, action, target, do_raise=True): + if action == 'compute_extension:v3:ext1-alias:discoverable': + raise exception.Forbidden + else: + return True + + +class ExtensionInfoTest(test.NoDBTestCase): + + def setUp(self): + super(ExtensionInfoTest, self).setUp() + ext_info = plugins.LoadedExtensionInfo() + ext_info.extensions = fake_extensions + self.controller = extension_info.ExtensionInfoController(ext_info) + + def test_extension_info_list(self): + self.stubs.Set(policy, 'enforce', fake_policy_enforce) + req = fakes.HTTPRequestV3.blank('/extensions') + res_dict = self.controller.index(req) + self.assertEqual(3, len(res_dict['extensions'])) + for e in res_dict['extensions']: + self.assertIn(e['alias'], fake_extensions) + self.assertEqual(e['name'], fake_extensions[e['alias']].name) + self.assertEqual(e['alias'], fake_extensions[e['alias']].alias) + self.assertEqual(e['description'], + fake_extensions[e['alias']].__doc__) + self.assertEqual(e['version'], + fake_extensions[e['alias']].version) + + def test_extension_info_show(self): + self.stubs.Set(policy, 'enforce', fake_policy_enforce) + req = fakes.HTTPRequestV3.blank('/extensions/ext1-alias') + res_dict = self.controller.show(req, 'ext1-alias') + self.assertEqual(1, len(res_dict)) + self.assertEqual(res_dict['extension']['name'], + fake_extensions['ext1-alias'].name) + self.assertEqual(res_dict['extension']['alias'], + fake_extensions['ext1-alias'].alias) + self.assertEqual(res_dict['extension']['description'], + fake_extensions['ext1-alias'].__doc__) + self.assertEqual(res_dict['extension']['version'], + fake_extensions['ext1-alias'].version) + + def test_extension_info_list_not_all_discoverable(self): + self.stubs.Set(policy, 'enforce', fake_policy_enforce_selective) + req = fakes.HTTPRequestV3.blank('/extensions') + res_dict = self.controller.index(req) + self.assertEqual(2, len(res_dict['extensions'])) + for e in res_dict['extensions']: + self.assertNotEqual('ext1-alias', e['alias']) + self.assertIn(e['alias'], fake_extensions) + self.assertEqual(e['name'], fake_extensions[e['alias']].name) + self.assertEqual(e['alias'], fake_extensions[e['alias']].alias) + self.assertEqual(e['description'], + fake_extensions[e['alias']].__doc__) + self.assertEqual(e['version'], + fake_extensions[e['alias']].version) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_lock_server.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_lock_server.py new file mode 100644 index 0000000000..ff5817ba19 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_lock_server.py @@ -0,0 +1,57 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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. + +from nova.api.openstack.compute.plugins.v3 import lock_server +from nova import exception +from nova.tests.unit.api.openstack.compute.plugins.v3 import \ + admin_only_action_common +from nova.tests.unit.api.openstack import fakes + + +class LockServerTests(admin_only_action_common.CommonTests): + def setUp(self): + super(LockServerTests, self).setUp() + self.controller = lock_server.LockServerController() + self.compute_api = self.controller.compute_api + + def _fake_controller(*args, **kwargs): + return self.controller + + self.stubs.Set(lock_server, 'LockServerController', + _fake_controller) + self.app = fakes.wsgi_app_v21(init_only=('servers', + 'os-lock-server'), + fake_auth_context=self.context) + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_lock_unlock(self): + self._test_actions(['lock', 'unlock']) + + def test_lock_unlock_with_non_existed_instance(self): + self._test_actions_with_non_existed_instance(['lock', 'unlock']) + + def test_unlock_not_authorized(self): + self.mox.StubOutWithMock(self.compute_api, 'unlock') + + instance = self._stub_instance_get() + + self.compute_api.unlock(self.context, instance).AndRaise( + exception.PolicyNotAuthorized(action='unlock')) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {'unlock': None}) + self.assertEqual(403, res.status_int) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_migrations.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_migrations.py new file mode 100644 index 0000000000..c735e87fea --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_migrations.py @@ -0,0 +1,115 @@ +# 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 datetime + +from nova.api.openstack.compute.plugins.v3 import migrations +from nova import context +from nova import exception +from nova import objects +from nova.objects import base +from nova.openstack.common.fixture import moxstubout +from nova import test + + +fake_migrations = [ + { + 'id': 1234, + 'source_node': 'node1', + 'dest_node': 'node2', + 'source_compute': 'compute1', + 'dest_compute': 'compute2', + 'dest_host': '1.2.3.4', + 'status': 'Done', + 'instance_uuid': 'instance_id_123', + 'old_instance_type_id': 1, + 'new_instance_type_id': 2, + 'created_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + 'deleted_at': None, + 'deleted': False + }, + { + 'id': 5678, + 'source_node': 'node10', + 'dest_node': 'node20', + 'source_compute': 'compute10', + 'dest_compute': 'compute20', + 'dest_host': '5.6.7.8', + 'status': 'Done', + 'instance_uuid': 'instance_id_456', + 'old_instance_type_id': 5, + 'new_instance_type_id': 6, + 'created_at': datetime.datetime(2013, 10, 22, 13, 42, 2), + 'updated_at': datetime.datetime(2013, 10, 22, 13, 42, 2), + 'deleted_at': None, + 'deleted': False + } +] + +migrations_obj = base.obj_make_list( + 'fake-context', + objects.MigrationList(), + objects.Migration, + fake_migrations) + + +class FakeRequest(object): + environ = {"nova.context": context.get_admin_context()} + GET = {} + + +class MigrationsTestCase(test.NoDBTestCase): + def setUp(self): + """Run before each test.""" + super(MigrationsTestCase, self).setUp() + self.controller = migrations.MigrationsController() + self.context = context.get_admin_context() + self.req = FakeRequest() + self.req.environ['nova.context'] = self.context + mox_fixture = self.useFixture(moxstubout.MoxStubout()) + self.mox = mox_fixture.mox + + def test_index(self): + migrations_in_progress = { + 'migrations': migrations.output(migrations_obj)} + + for mig in migrations_in_progress['migrations']: + self.assertIn('id', mig) + self.assertNotIn('deleted', mig) + self.assertNotIn('deleted_at', mig) + + filters = {'host': 'host1', 'status': 'migrating', + 'cell_name': 'ChildCell'} + self.req.GET = filters + self.mox.StubOutWithMock(self.controller.compute_api, + "get_migrations") + + self.controller.compute_api.get_migrations( + self.context, filters).AndReturn(migrations_obj) + self.mox.ReplayAll() + + response = self.controller.index(self.req) + self.assertEqual(migrations_in_progress, response) + + def test_index_needs_authorization(self): + user_context = context.RequestContext(user_id=None, + project_id=None, + is_admin=False, + read_deleted="no", + overwrite=False) + self.req.environ['nova.context'] = user_context + + self.assertRaises(exception.PolicyNotAuthorized, self.controller.index, + self.req) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_multiple_create.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_multiple_create.py new file mode 100644 index 0000000000..35a559c668 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_multiple_create.py @@ -0,0 +1,547 @@ +# Copyright 2013 IBM Corp. +# 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 datetime +import uuid + +from oslo.config import cfg +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import block_device_mapping +from nova.api.openstack.compute.plugins.v3 import multiple_create +from nova.api.openstack.compute.plugins.v3 import servers +from nova.compute import api as compute_api +from nova.compute import flavors +from nova import db +from nova import exception +from nova.network import manager +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance +from nova.tests.unit.image import fake + +CONF = cfg.CONF +FAKE_UUID = fakes.FAKE_UUID + + +def fake_gen_uuid(): + return FAKE_UUID + + +def return_security_group(context, instance_id, security_group_id): + pass + + +class ServersControllerCreateTest(test.TestCase): + + def setUp(self): + """Shared implementation for tests below that create instance.""" + super(ServersControllerCreateTest, self).setUp() + + self.flags(verbose=True, + enable_instance_password=True) + self.instance_cache_num = 0 + self.instance_cache_by_id = {} + self.instance_cache_by_uuid = {} + + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers.ServersController(extension_info=ext_info) + CONF.set_override('extensions_blacklist', 'os-multiple-create', + 'osapi_v3') + self.no_mult_create_controller = servers.ServersController( + extension_info=ext_info) + + def instance_create(context, inst): + inst_type = flavors.get_flavor_by_flavor_id(3) + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + def_image_ref = 'http://localhost/images/%s' % image_uuid + self.instance_cache_num += 1 + instance = fake_instance.fake_db_instance(**{ + 'id': self.instance_cache_num, + 'display_name': inst['display_name'] or 'test', + 'uuid': FAKE_UUID, + 'instance_type': inst_type, + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': 'fead::1234', + 'image_ref': inst.get('image_ref', def_image_ref), + 'user_id': 'fake', + 'project_id': 'fake', + 'reservation_id': inst['reservation_id'], + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "progress": 0, + "fixed_ips": [], + "task_state": "", + "vm_state": "", + "security_groups": inst['security_groups'], + }) + + self.instance_cache_by_id[instance['id']] = instance + self.instance_cache_by_uuid[instance['uuid']] = instance + return instance + + def instance_get(context, instance_id): + """Stub for compute/api create() pulling in instance after + scheduling + """ + return self.instance_cache_by_id[instance_id] + + def instance_update(context, uuid, values): + instance = self.instance_cache_by_uuid[uuid] + instance.update(values) + return instance + + def server_update(context, instance_uuid, params, update_cells=True, + columns_to_join=None): + inst = self.instance_cache_by_uuid[instance_uuid] + inst.update(params) + return (inst, inst) + + def fake_method(*args, **kwargs): + pass + + def project_get_networks(context, user_id): + return dict(id='1', host='localhost') + + def queue_get_for(context, *args): + return 'network_topic' + + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + fake.stub_out_image_service(self.stubs) + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(uuid, 'uuid4', fake_gen_uuid) + self.stubs.Set(db, 'instance_add_security_group', + return_security_group) + self.stubs.Set(db, 'project_get_networks', + project_get_networks) + self.stubs.Set(db, 'instance_create', instance_create) + self.stubs.Set(db, 'instance_system_metadata_update', + fake_method) + self.stubs.Set(db, 'instance_get', instance_get) + self.stubs.Set(db, 'instance_update', instance_update) + self.stubs.Set(db, 'instance_update_and_get_original', + server_update) + self.stubs.Set(manager.VlanManager, 'allocate_fixed_ip', + fake_method) + + def _test_create_extra(self, params, no_image=False, + override_controller=None): + image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + server = dict(name='server_test', imageRef=image_uuid, flavorRef=2) + if no_image: + server.pop('imageRef', None) + server.update(params) + body = dict(server=server) + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + if override_controller: + server = override_controller.create(req, body=body).obj['server'] + else: + server = self.controller.create(req, body=body).obj['server'] + + def test_create_instance_with_multiple_create_disabled(self): + min_count = 2 + max_count = 3 + params = { + multiple_create.MIN_ATTRIBUTE_NAME: min_count, + multiple_create.MAX_ATTRIBUTE_NAME: max_count, + } + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertNotIn('min_count', kwargs) + self.assertNotIn('max_count', kwargs) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra( + params, + override_controller=self.no_mult_create_controller) + + def test_multiple_create_with_string_type_min_and_max(self): + min_count = '2' + max_count = '3' + params = { + multiple_create.MIN_ATTRIBUTE_NAME: min_count, + multiple_create.MAX_ATTRIBUTE_NAME: max_count, + } + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertIsInstance(kwargs['min_count'], int) + self.assertIsInstance(kwargs['max_count'], int) + self.assertEqual(kwargs['min_count'], 2) + self.assertEqual(kwargs['max_count'], 3) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_multiple_create_enabled(self): + min_count = 2 + max_count = 3 + params = { + multiple_create.MIN_ATTRIBUTE_NAME: min_count, + multiple_create.MAX_ATTRIBUTE_NAME: max_count, + } + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['min_count'], 2) + self.assertEqual(kwargs['max_count'], 3) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_invalid_negative_min(self): + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + + body = { + 'server': { + multiple_create.MIN_ATTRIBUTE_NAME: -1, + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + } + } + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + self.assertRaises(exception.ValidationError, + self.controller.create, + req, + body=body) + + def test_create_instance_invalid_negative_max(self): + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + + body = { + 'server': { + multiple_create.MAX_ATTRIBUTE_NAME: -1, + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + } + } + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + self.assertRaises(exception.ValidationError, + self.controller.create, + req, + body=body) + + def test_create_instance_with_blank_min(self): + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + + body = { + 'server': { + multiple_create.MIN_ATTRIBUTE_NAME: '', + 'name': 'server_test', + 'image_ref': image_href, + 'flavor_ref': flavor_ref, + } + } + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + self.assertRaises(exception.ValidationError, + self.controller.create, + req, + body=body) + + def test_create_instance_with_blank_max(self): + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + + body = { + 'server': { + multiple_create.MAX_ATTRIBUTE_NAME: '', + 'name': 'server_test', + 'image_ref': image_href, + 'flavor_ref': flavor_ref, + } + } + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + self.assertRaises(exception.ValidationError, + self.controller.create, + req, + body=body) + + def test_create_instance_invalid_min_greater_than_max(self): + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + + body = { + 'server': { + multiple_create.MIN_ATTRIBUTE_NAME: 4, + multiple_create.MAX_ATTRIBUTE_NAME: 2, + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + } + } + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + req, + body=body) + + def test_create_instance_invalid_alpha_min(self): + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + + body = { + 'server': { + multiple_create.MIN_ATTRIBUTE_NAME: 'abcd', + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + } + } + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + self.assertRaises(exception.ValidationError, + self.controller.create, + req, + body=body) + + def test_create_instance_invalid_alpha_max(self): + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + + body = { + 'server': { + multiple_create.MAX_ATTRIBUTE_NAME: 'abcd', + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + } + } + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + self.assertRaises(exception.ValidationError, + self.controller.create, + req, + body=body) + + def test_create_multiple_instances(self): + """Test creating multiple instances but not asking for + reservation_id + """ + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + body = { + 'server': { + multiple_create.MIN_ATTRIBUTE_NAME: 2, + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': {'hello': 'world', + 'open': 'stack'}, + } + } + + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body=body).obj + + self.assertEqual(FAKE_UUID, res["server"]["id"]) + self._check_admin_password_len(res["server"]) + + def test_create_multiple_instances_pass_disabled(self): + """Test creating multiple instances but not asking for + reservation_id + """ + self.flags(enable_instance_password=False) + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + body = { + 'server': { + multiple_create.MIN_ATTRIBUTE_NAME: 2, + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': {'hello': 'world', + 'open': 'stack'}, + } + } + + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body=body).obj + + self.assertEqual(FAKE_UUID, res["server"]["id"]) + self._check_admin_password_missing(res["server"]) + + def _check_admin_password_len(self, server_dict): + """utility function - check server_dict for admin_password length.""" + self.assertEqual(CONF.password_length, + len(server_dict["adminPass"])) + + def _check_admin_password_missing(self, server_dict): + """utility function - check server_dict for admin_password absence.""" + self.assertNotIn("admin_password", server_dict) + + def _create_multiple_instances_resv_id_return(self, resv_id_return): + """Test creating multiple instances with asking for + reservation_id + """ + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + body = { + 'server': { + multiple_create.MIN_ATTRIBUTE_NAME: 2, + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': {'hello': 'world', + 'open': 'stack'}, + multiple_create.RRID_ATTRIBUTE_NAME: resv_id_return + } + } + + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, body=body) + reservation_id = res.obj['reservation_id'] + self.assertNotEqual(reservation_id, "") + self.assertIsNotNone(reservation_id) + self.assertTrue(len(reservation_id) > 1) + + def test_create_multiple_instances_with_resv_id_return(self): + self._create_multiple_instances_resv_id_return(True) + + def test_create_multiple_instances_with_string_resv_id_return(self): + self._create_multiple_instances_resv_id_return("True") + + def test_create_multiple_instances_with_multiple_volume_bdm(self): + """Test that a BadRequest is raised if multiple instances + are requested with a list of block device mappings for volumes. + """ + min_count = 2 + bdm = [{'source_type': 'volume', 'uuid': 'vol-xxxx'}, + {'source_type': 'volume', 'uuid': 'vol-yyyy'} + ] + params = { + block_device_mapping.ATTRIBUTE_NAME: bdm, + multiple_create.MIN_ATTRIBUTE_NAME: min_count + } + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['min_count'], 2) + self.assertEqual(len(kwargs['block_device_mapping']), 2) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + exc = self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params, no_image=True) + self.assertEqual("Cannot attach one or more volumes to multiple " + "instances", exc.explanation) + + def test_create_multiple_instances_with_single_volume_bdm(self): + """Test that a BadRequest is raised if multiple instances + are requested to boot from a single volume. + """ + min_count = 2 + bdm = [{'source_type': 'volume', 'uuid': 'vol-xxxx'}] + params = { + block_device_mapping.ATTRIBUTE_NAME: bdm, + multiple_create.MIN_ATTRIBUTE_NAME: min_count + } + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['min_count'], 2) + self.assertEqual(kwargs['block_device_mapping'][0]['volume_id'], + 'vol-xxxx') + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + exc = self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params, no_image=True) + self.assertEqual("Cannot attach one or more volumes to multiple " + "instances", exc.explanation) + + def test_create_multiple_instance_with_non_integer_max_count(self): + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + body = { + 'server': { + multiple_create.MAX_ATTRIBUTE_NAME: 2.5, + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': {'hello': 'world', + 'open': 'stack'}, + } + } + + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + self.assertRaises(exception.ValidationError, + self.controller.create, req, body=body) + + def test_create_multiple_instance_with_non_integer_min_count(self): + image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + body = { + 'server': { + multiple_create.MIN_ATTRIBUTE_NAME: 2.5, + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': {'hello': 'world', + 'open': 'stack'}, + } + } + + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + self.assertRaises(exception.ValidationError, + self.controller.create, req, body=body) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_pause_server.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_pause_server.py new file mode 100644 index 0000000000..5364fb45b3 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_pause_server.py @@ -0,0 +1,60 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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. + +from nova.api.openstack.compute.plugins.v3 import pause_server +from nova.tests.unit.api.openstack.compute.plugins.v3 import \ + admin_only_action_common +from nova.tests.unit.api.openstack import fakes + + +class PauseServerTests(admin_only_action_common.CommonTests): + def setUp(self): + super(PauseServerTests, self).setUp() + self.controller = pause_server.PauseServerController() + self.compute_api = self.controller.compute_api + + def _fake_controller(*args, **kwargs): + return self.controller + + self.stubs.Set(pause_server, 'PauseServerController', + _fake_controller) + self.app = fakes.wsgi_app_v21(init_only=('servers', + 'os-pause-server'), + fake_auth_context=self.context) + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_pause_unpause(self): + self._test_actions(['pause', 'unpause']) + + def test_actions_raise_on_not_implemented(self): + for action in ['pause', 'unpause']: + self.mox.StubOutWithMock(self.compute_api, action) + self._test_not_implemented_state(action) + # Re-mock this. + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_pause_unpause_with_non_existed_instance(self): + self._test_actions_with_non_existed_instance(['pause', 'unpause']) + + def test_pause_unpause_with_non_existed_instance_in_compute_api(self): + self._test_actions_instance_not_found_in_compute_api(['pause', + 'unpause']) + + def test_pause_unpause_raise_conflict_on_invalid_state(self): + self._test_actions_raise_conflict_on_invalid_state(['pause', + 'unpause']) + + def test_actions_with_locked_instance(self): + self._test_actions_with_locked_instance(['pause', 'unpause']) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_pci.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_pci.py new file mode 100644 index 0000000000..6ac6269195 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_pci.py @@ -0,0 +1,236 @@ +# Copyright 2013 Intel Corp. +# +# 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. + + +from oslo.serialization import jsonutils +from webob import exc + +from nova.api.openstack.compute.plugins.v3 import pci +from nova.api.openstack import wsgi +from nova import context +from nova import db +from nova import exception +from nova import objects +from nova.pci import device +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit.objects import test_pci_device + + +fake_compute_node = { + 'pci_stats': [{"count": 3, + "vendor_id": "8086", + "product_id": "1520", + "extra_info": {"phys_function": '[["0x0000", "0x04", ' + '"0x00", "0x1"]]'}}]} + + +class FakeResponse(wsgi.ResponseObject): + pass + + +class PciServerControllerTest(test.NoDBTestCase): + def setUp(self): + super(PciServerControllerTest, self).setUp() + self.controller = pci.PciServerController() + self.fake_obj = {'server': {'addresses': {}, + 'id': 'fb08', + 'name': 'a3', + 'status': 'ACTIVE', + 'tenant_id': '9a3af784c', + 'user_id': 'e992080ac0', + }} + self.fake_list = {'servers': [{'addresses': {}, + 'id': 'fb08', + 'name': 'a3', + 'status': 'ACTIVE', + 'tenant_id': '9a3af784c', + 'user_id': 'e992080ac', + }]} + self._create_fake_instance() + self._create_fake_pci_device() + device.claim(self.pci_device, self.inst) + device.allocate(self.pci_device, self.inst) + + def _create_fake_instance(self): + self.inst = objects.Instance() + self.inst.uuid = 'fake-inst-uuid' + self.inst.pci_devices = objects.PciDeviceList() + + def _create_fake_pci_device(self): + def fake_pci_device_get_by_addr(ctxt, id, addr): + return test_pci_device.fake_db_dev + + ctxt = context.get_admin_context() + self.stubs.Set(db, 'pci_device_get_by_addr', + fake_pci_device_get_by_addr) + self.pci_device = objects.PciDevice.get_by_dev_addr(ctxt, 1, 'a') + + def test_show(self): + def fake_get_db_instance(id): + return self.inst + + resp = FakeResponse(self.fake_obj, '') + req = fakes.HTTPRequestV3.blank('/os-pci/1', use_admin_context=True) + self.stubs.Set(req, 'get_db_instance', fake_get_db_instance) + self.controller.show(req, resp, '1') + self.assertEqual([{'id': 1}], + resp.obj['server']['os-pci:pci_devices']) + + def test_detail(self): + def fake_get_db_instance(id): + return self.inst + + resp = FakeResponse(self.fake_list, '') + req = fakes.HTTPRequestV3.blank('/os-pci/detail', + use_admin_context=True) + self.stubs.Set(req, 'get_db_instance', fake_get_db_instance) + self.controller.detail(req, resp) + self.assertEqual([{'id': 1}], + resp.obj['servers'][0]['os-pci:pci_devices']) + + +class PciHypervisorControllerTest(test.NoDBTestCase): + def setUp(self): + super(PciHypervisorControllerTest, self).setUp() + self.controller = pci.PciHypervisorController() + self.fake_objs = dict(hypervisors=[ + dict(id=1, + service=dict(id=1, host="compute1"), + hypervisor_type="xen", + hypervisor_version=3, + hypervisor_hostname="hyper1")]) + self.fake_obj = dict(hypervisor=dict( + id=1, + service=dict(id=1, host="compute1"), + hypervisor_type="xen", + hypervisor_version=3, + hypervisor_hostname="hyper1")) + + def test_show(self): + def fake_get_db_compute_node(id): + fake_compute_node['pci_stats'] = jsonutils.dumps( + fake_compute_node['pci_stats']) + return fake_compute_node + + req = fakes.HTTPRequestV3.blank('/os-hypervisors/1', + use_admin_context=True) + resp = FakeResponse(self.fake_obj, '') + self.stubs.Set(req, 'get_db_compute_node', fake_get_db_compute_node) + self.controller.show(req, resp, '1') + self.assertIn('os-pci:pci_stats', resp.obj['hypervisor']) + fake_compute_node['pci_stats'] = jsonutils.loads( + fake_compute_node['pci_stats']) + self.assertEqual(fake_compute_node['pci_stats'][0], + resp.obj['hypervisor']['os-pci:pci_stats'][0]) + + def test_detail(self): + def fake_get_db_compute_node(id): + fake_compute_node['pci_stats'] = jsonutils.dumps( + fake_compute_node['pci_stats']) + return fake_compute_node + + req = fakes.HTTPRequestV3.blank('/os-hypervisors/detail', + use_admin_context=True) + resp = FakeResponse(self.fake_objs, '') + self.stubs.Set(req, 'get_db_compute_node', fake_get_db_compute_node) + self.controller.detail(req, resp) + fake_compute_node['pci_stats'] = jsonutils.loads( + fake_compute_node['pci_stats']) + self.assertIn('os-pci:pci_stats', resp.obj['hypervisors'][0]) + self.assertEqual(fake_compute_node['pci_stats'][0], + resp.obj['hypervisors'][0]['os-pci:pci_stats'][0]) + + +class PciControlletest(test.NoDBTestCase): + def setUp(self): + super(PciControlletest, self).setUp() + self.controller = pci.PciController() + + def test_show(self): + def fake_pci_device_get_by_id(context, id): + return test_pci_device.fake_db_dev + + self.stubs.Set(db, 'pci_device_get_by_id', fake_pci_device_get_by_id) + req = fakes.HTTPRequestV3.blank('/os-pci/1', use_admin_context=True) + result = self.controller.show(req, '1') + dist = {'pci_device': {'address': 'a', + 'compute_node_id': 1, + 'dev_id': 'i', + 'extra_info': {}, + 'dev_type': 't', + 'id': 1, + 'server_uuid': None, + 'label': 'l', + 'product_id': 'p', + 'status': 'available', + 'vendor_id': 'v'}} + self.assertEqual(dist, result) + + def test_show_error_id(self): + def fake_pci_device_get_by_id(context, id): + raise exception.PciDeviceNotFoundById(id=id) + + self.stubs.Set(db, 'pci_device_get_by_id', fake_pci_device_get_by_id) + req = fakes.HTTPRequestV3.blank('/os-pci/0', use_admin_context=True) + self.assertRaises(exc.HTTPNotFound, self.controller.show, req, '0') + + def _fake_compute_node_get_all(self, context): + return [dict(id=1, + service_id=1, + cpu_info='cpu_info', + disk_available_least=100)] + + def _fake_pci_device_get_all_by_node(self, context, node): + return [test_pci_device.fake_db_dev, test_pci_device.fake_db_dev_1] + + def test_index(self): + self.stubs.Set(db, 'compute_node_get_all', + self._fake_compute_node_get_all) + self.stubs.Set(db, 'pci_device_get_all_by_node', + self._fake_pci_device_get_all_by_node) + + req = fakes.HTTPRequestV3.blank('/os-pci', use_admin_context=True) + result = self.controller.index(req) + dist = {'pci_devices': [test_pci_device.fake_db_dev, + test_pci_device.fake_db_dev_1]} + for i in range(len(result['pci_devices'])): + self.assertEqual(dist['pci_devices'][i]['vendor_id'], + result['pci_devices'][i]['vendor_id']) + self.assertEqual(dist['pci_devices'][i]['id'], + result['pci_devices'][i]['id']) + self.assertEqual(dist['pci_devices'][i]['status'], + result['pci_devices'][i]['status']) + self.assertEqual(dist['pci_devices'][i]['address'], + result['pci_devices'][i]['address']) + + def test_detail(self): + self.stubs.Set(db, 'compute_node_get_all', + self._fake_compute_node_get_all) + self.stubs.Set(db, 'pci_device_get_all_by_node', + self._fake_pci_device_get_all_by_node) + req = fakes.HTTPRequestV3.blank('/os-pci/detail', + use_admin_context=True) + result = self.controller.detail(req) + dist = {'pci_devices': [test_pci_device.fake_db_dev, + test_pci_device.fake_db_dev_1]} + for i in range(len(result['pci_devices'])): + self.assertEqual(dist['pci_devices'][i]['vendor_id'], + result['pci_devices'][i]['vendor_id']) + self.assertEqual(dist['pci_devices'][i]['id'], + result['pci_devices'][i]['id']) + self.assertEqual(dist['pci_devices'][i]['label'], + result['pci_devices'][i]['label']) + self.assertEqual(dist['pci_devices'][i]['dev_id'], + result['pci_devices'][i]['dev_id']) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_actions.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_actions.py new file mode 100644 index 0000000000..0bfe0eb2d4 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_actions.py @@ -0,0 +1,1131 @@ +# Copyright 2011 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 uuid + +import mock +import mox +from oslo.config import cfg +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import servers +from nova.compute import api as compute_api +from nova.compute import task_states +from nova.compute import vm_states +from nova import context +from nova import db +from nova import exception +from nova.image import glance +from nova import objects +from nova.openstack.common import uuidutils +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_block_device +from nova.tests.unit import fake_instance +from nova.tests.unit.image import fake + +CONF = cfg.CONF +CONF.import_opt('password_length', 'nova.utils') +FAKE_UUID = fakes.FAKE_UUID +INSTANCE_IDS = {FAKE_UUID: 1} + + +def return_server_not_found(*arg, **kwarg): + raise exception.InstanceNotFound(instance_id='42') + + +def instance_update_and_get_original(context, instance_uuid, values, + update_cells=True, + columns_to_join=None, + ): + inst = fakes.stub_instance(INSTANCE_IDS[instance_uuid], host='fake_host') + inst = dict(inst, **values) + return (inst, inst) + + +def instance_update(context, instance_uuid, kwargs, update_cells=True): + inst = fakes.stub_instance(INSTANCE_IDS[instance_uuid], host='fake_host') + return inst + + +class MockSetAdminPassword(object): + def __init__(self): + self.instance_id = None + self.password = None + + def __call__(self, context, instance, password): + self.instance_id = instance['uuid'] + self.password = password + + +class ServerActionsControllerTest(test.TestCase): + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + image_href = 'http://localhost/v2/fake/images/%s' % image_uuid + + def setUp(self): + super(ServerActionsControllerTest, self).setUp() + + CONF.set_override('host', 'localhost', group='glance') + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE, + host='fake_host')) + self.stubs.Set(db, 'instance_update_and_get_original', + instance_update_and_get_original) + + fakes.stub_out_nw_api(self.stubs) + fakes.stub_out_compute_api_snapshot(self.stubs) + fake.stub_out_image_service(self.stubs) + self.flags(allow_instance_snapshots=True, + enable_instance_password=True) + self.uuid = FAKE_UUID + self.url = '/servers/%s/action' % self.uuid + self._image_href = '155d900f-4e14-4e4c-a73d-069cbf4541e6' + + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers.ServersController(extension_info=ext_info) + self.compute_api = self.controller.compute_api + self.context = context.RequestContext('fake', 'fake') + self.app = fakes.wsgi_app_v21(init_only=('servers',), + fake_auth_context=self.context) + + def _make_request(self, url, body): + req = webob.Request.blank('/v2/fake' + url) + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.content_type = 'application/json' + return req.get_response(self.app) + + def _stub_instance_get(self, uuid=None): + self.mox.StubOutWithMock(compute_api.API, 'get') + if uuid is None: + uuid = uuidutils.generate_uuid() + instance = fake_instance.fake_db_instance( + id=1, uuid=uuid, vm_state=vm_states.ACTIVE, task_state=None) + instance = objects.Instance._from_db_object( + self.context, objects.Instance(), instance) + + self.compute_api.get(self.context, uuid, want_objects=True, + expected_attrs=['pci_devices']).AndReturn(instance) + return instance + + def _test_locked_instance(self, action, method=None, body_map=None, + compute_api_args_map=None): + if method is None: + method = action + if body_map is None: + body_map = {} + if compute_api_args_map is None: + compute_api_args_map = {} + + instance = self._stub_instance_get() + args, kwargs = compute_api_args_map.get(action, ((), {})) + + getattr(compute_api.API, method)(self.context, instance, + *args, **kwargs).AndRaise( + exception.InstanceIsLocked(instance_uuid=instance['uuid'])) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance['uuid'], + {action: body_map.get(action)}) + self.assertEqual(409, res.status_int) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def test_actions_with_locked_instance(self): + actions = ['resize', 'confirmResize', 'revertResize', 'reboot', + 'rebuild'] + + method_translations = {'confirmResize': 'confirm_resize', + 'revertResize': 'revert_resize'} + + body_map = {'resize': {'flavorRef': '2'}, + 'reboot': {'type': 'HARD'}, + 'rebuild': {'imageRef': self.image_uuid, + 'adminPass': 'TNc53Dr8s7vw'}} + + args_map = {'resize': (('2'), {}), + 'confirmResize': ((), {}), + 'reboot': (('HARD',), {}), + 'rebuild': ((self.image_uuid, 'TNc53Dr8s7vw'), {})} + + for action in actions: + method = method_translations.get(action) + self.mox.StubOutWithMock(compute_api.API, method or action) + self._test_locked_instance(action, method=method, + body_map=body_map, + compute_api_args_map=args_map) + + def test_reboot_hard(self): + body = dict(reboot=dict(type="HARD")) + req = fakes.HTTPRequestV3.blank(self.url) + self.controller._action_reboot(req, FAKE_UUID, body) + + def test_reboot_soft(self): + body = dict(reboot=dict(type="SOFT")) + req = fakes.HTTPRequestV3.blank(self.url) + self.controller._action_reboot(req, FAKE_UUID, body) + + def test_reboot_incorrect_type(self): + body = dict(reboot=dict(type="NOT_A_TYPE")) + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_reboot, + req, FAKE_UUID, body) + + def test_reboot_missing_type(self): + body = dict(reboot=dict()) + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_reboot, + req, FAKE_UUID, body) + + def test_reboot_none(self): + body = dict(reboot=dict(type=None)) + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_reboot, + req, FAKE_UUID, body) + + def test_reboot_not_found(self): + self.stubs.Set(db, 'instance_get_by_uuid', + return_server_not_found) + + body = dict(reboot=dict(type="HARD")) + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._action_reboot, + req, str(uuid.uuid4()), body) + + def test_reboot_raises_conflict_on_invalid_state(self): + body = dict(reboot=dict(type="HARD")) + + def fake_reboot(*args, **kwargs): + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + + self.stubs.Set(compute_api.API, 'reboot', fake_reboot) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_reboot, + req, FAKE_UUID, body) + + def test_reboot_soft_with_soft_in_progress_raises_conflict(self): + body = dict(reboot=dict(type="SOFT")) + req = fakes.HTTPRequestV3.blank(self.url) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE, + task_state=task_states.REBOOTING)) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_reboot, + req, FAKE_UUID, body) + + def test_reboot_hard_with_soft_in_progress_does_not_raise(self): + body = dict(reboot=dict(type="HARD")) + req = fakes.HTTPRequestV3.blank(self.url) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE, + task_state=task_states.REBOOTING)) + self.controller._action_reboot(req, FAKE_UUID, body) + + def test_reboot_hard_with_hard_in_progress_raises_conflict(self): + body = dict(reboot=dict(type="HARD")) + req = fakes.HTTPRequestV3.blank(self.url) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE, + task_state=task_states.REBOOTING_HARD)) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_reboot, + req, FAKE_UUID, body) + + def test_rebuild_accepted_minimum(self): + return_server = fakes.fake_instance_get(image_ref='2', + vm_state=vm_states.ACTIVE, host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + self_href = 'http://localhost/v3/servers/%s' % FAKE_UUID + + body = { + "rebuild": { + "imageRef": self._image_href, + }, + } + + req = fakes.HTTPRequestV3.blank(self.url) + robj = self.controller._action_rebuild(req, FAKE_UUID, body=body) + body = robj.obj + + self.assertEqual(body['server']['image']['id'], '2') + self.assertEqual(len(body['server']['adminPass']), + CONF.password_length) + + self.assertEqual(robj['location'], self_href) + + def test_rebuild_instance_with_image_uuid(self): + info = dict(image_href_in_call=None) + + def rebuild(self2, context, instance, image_href, *args, **kwargs): + info['image_href_in_call'] = image_href + + self.stubs.Set(db, 'instance_get', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE)) + self.stubs.Set(compute_api.API, 'rebuild', rebuild) + + body = { + 'rebuild': { + 'imageRef': self.image_uuid, + }, + } + + req = fakes.HTTPRequestV3.blank('/v2/fake/servers/a/action') + self.controller._action_rebuild(req, FAKE_UUID, body=body) + self.assertEqual(info['image_href_in_call'], self.image_uuid) + + def test_rebuild_instance_with_image_href_uses_uuid(self): + info = dict(image_href_in_call=None) + + def rebuild(self2, context, instance, image_href, *args, **kwargs): + info['image_href_in_call'] = image_href + + self.stubs.Set(db, 'instance_get', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE)) + self.stubs.Set(compute_api.API, 'rebuild', rebuild) + + body = { + 'rebuild': { + 'imageRef': self.image_href, + }, + } + + req = fakes.HTTPRequestV3.blank('/v2/fake/servers/a/action') + self.controller._action_rebuild(req, FAKE_UUID, body=body) + self.assertEqual(info['image_href_in_call'], self.image_uuid) + + def test_rebuild_accepted_minimum_pass_disabled(self): + # run with enable_instance_password disabled to verify admin_password + # is missing from response. See lp bug 921814 + self.flags(enable_instance_password=False) + + return_server = fakes.fake_instance_get(image_ref='2', + vm_state=vm_states.ACTIVE, host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + self_href = 'http://localhost/v3/servers/%s' % FAKE_UUID + + body = { + "rebuild": { + "imageRef": self._image_href, + }, + } + + req = fakes.HTTPRequestV3.blank(self.url) + robj = self.controller._action_rebuild(req, FAKE_UUID, body=body) + body = robj.obj + + self.assertEqual(body['server']['image']['id'], '2') + self.assertNotIn("admin_password", body['server']) + + self.assertEqual(robj['location'], self_href) + + def test_rebuild_raises_conflict_on_invalid_state(self): + body = { + "rebuild": { + "imageRef": self._image_href, + }, + } + + def fake_rebuild(*args, **kwargs): + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + + self.stubs.Set(compute_api.API, 'rebuild', fake_rebuild) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_rebuild, + req, FAKE_UUID, body=body) + + def test_rebuild_accepted_with_metadata(self): + metadata = {'new': 'metadata'} + + return_server = fakes.fake_instance_get(metadata=metadata, + vm_state=vm_states.ACTIVE, host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + body = { + "rebuild": { + "imageRef": self._image_href, + "metadata": metadata, + }, + } + + req = fakes.HTTPRequestV3.blank(self.url) + body = self.controller._action_rebuild(req, FAKE_UUID, body=body).obj + + self.assertEqual(body['server']['metadata'], metadata) + + def test_rebuild_accepted_with_bad_metadata(self): + body = { + "rebuild": { + "imageRef": self._image_href, + "metadata": "stack", + }, + } + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + req, FAKE_UUID, body=body) + + def test_rebuild_with_too_large_metadata(self): + body = { + "rebuild": { + "imageRef": self._image_href, + "metadata": { + 256 * "k": "value" + } + } + } + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, req, + FAKE_UUID, body=body) + + def test_rebuild_bad_entity(self): + body = { + "rebuild": { + "imageId": self._image_href, + }, + } + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + req, FAKE_UUID, body=body) + + def test_rebuild_admin_password(self): + return_server = fakes.fake_instance_get(image_ref='2', + vm_state=vm_states.ACTIVE, host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + body = { + "rebuild": { + "imageRef": self._image_href, + "adminPass": "asdf", + }, + } + + req = fakes.HTTPRequestV3.blank(self.url) + body = self.controller._action_rebuild(req, FAKE_UUID, body=body).obj + + self.assertEqual(body['server']['image']['id'], '2') + self.assertEqual(body['server']['adminPass'], 'asdf') + + def test_rebuild_admin_password_pass_disabled(self): + # run with enable_instance_password disabled to verify admin_password + # is missing from response. See lp bug 921814 + self.flags(enable_instance_password=False) + + return_server = fakes.fake_instance_get(image_ref='2', + vm_state=vm_states.ACTIVE, host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + body = { + "rebuild": { + "imageRef": self._image_href, + "admin_password": "asdf", + }, + } + + req = fakes.HTTPRequestV3.blank(self.url) + body = self.controller._action_rebuild(req, FAKE_UUID, body=body).obj + + self.assertEqual(body['server']['image']['id'], '2') + self.assertNotIn('adminPass', body['server']) + + def test_rebuild_server_not_found(self): + def server_not_found(self, instance_id, + columns_to_join=None, use_slave=False): + raise exception.InstanceNotFound(instance_id=instance_id) + self.stubs.Set(db, 'instance_get_by_uuid', server_not_found) + + body = { + "rebuild": { + "imageRef": self._image_href, + }, + } + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._action_rebuild, + req, FAKE_UUID, body=body) + + def test_rebuild_with_bad_image(self): + body = { + "rebuild": { + "imageRef": "foo", + }, + } + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + req, FAKE_UUID, body=body) + + def test_rebuild_when_kernel_not_exists(self): + + def return_image_meta(*args, **kwargs): + image_meta_table = { + '2': {'id': 2, 'status': 'active', 'container_format': 'ari'}, + '155d900f-4e14-4e4c-a73d-069cbf4541e6': + {'id': 3, 'status': 'active', 'container_format': 'raw', + 'properties': {'kernel_id': 1, 'ramdisk_id': 2}}, + } + image_id = args[2] + try: + image_meta = image_meta_table[str(image_id)] + except KeyError: + raise exception.ImageNotFound(image_id=image_id) + + return image_meta + + self.stubs.Set(fake._FakeImageService, 'show', return_image_meta) + body = { + "rebuild": { + "imageRef": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + }, + } + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + req, FAKE_UUID, body=body) + + def test_rebuild_proper_kernel_ram(self): + instance_meta = {'kernel_id': None, 'ramdisk_id': None} + + orig_get = compute_api.API.get + + def wrap_get(*args, **kwargs): + inst = orig_get(*args, **kwargs) + instance_meta['instance'] = inst + return inst + + def fake_save(context, **kwargs): + instance = instance_meta['instance'] + for key in instance_meta.keys(): + if key in instance.obj_what_changed(): + instance_meta[key] = instance[key] + + def return_image_meta(*args, **kwargs): + image_meta_table = { + '1': {'id': 1, 'status': 'active', 'container_format': 'aki'}, + '2': {'id': 2, 'status': 'active', 'container_format': 'ari'}, + '155d900f-4e14-4e4c-a73d-069cbf4541e6': + {'id': 3, 'status': 'active', 'container_format': 'raw', + 'properties': {'kernel_id': 1, 'ramdisk_id': 2}}, + } + image_id = args[2] + try: + image_meta = image_meta_table[str(image_id)] + except KeyError: + raise exception.ImageNotFound(image_id=image_id) + + return image_meta + + self.stubs.Set(fake._FakeImageService, 'show', return_image_meta) + self.stubs.Set(compute_api.API, 'get', wrap_get) + self.stubs.Set(objects.Instance, 'save', fake_save) + body = { + "rebuild": { + "imageRef": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + }, + } + req = fakes.HTTPRequestV3.blank(self.url) + self.controller._action_rebuild(req, FAKE_UUID, body=body).obj + self.assertEqual(instance_meta['kernel_id'], '1') + self.assertEqual(instance_meta['ramdisk_id'], '2') + + def _test_rebuild_preserve_ephemeral(self, value=None): + return_server = fakes.fake_instance_get(image_ref='2', + vm_state=vm_states.ACTIVE, + host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + body = { + "rebuild": { + "imageRef": self._image_href, + }, + } + if value is not None: + body['rebuild']['preserve_ephemeral'] = value + + req = fakes.HTTPRequestV3.blank(self.url) + context = req.environ['nova.context'] + + self.mox.StubOutWithMock(compute_api.API, 'rebuild') + if value is not None: + compute_api.API.rebuild(context, mox.IgnoreArg(), self._image_href, + mox.IgnoreArg(), preserve_ephemeral=value) + else: + compute_api.API.rebuild(context, mox.IgnoreArg(), self._image_href, + mox.IgnoreArg()) + self.mox.ReplayAll() + + self.controller._action_rebuild(req, FAKE_UUID, body=body) + + def test_rebuild_preserve_ephemeral_true(self): + self._test_rebuild_preserve_ephemeral(True) + + def test_rebuild_preserve_ephemeral_false(self): + self._test_rebuild_preserve_ephemeral(False) + + def test_rebuild_preserve_ephemeral_default(self): + self._test_rebuild_preserve_ephemeral() + + @mock.patch.object(compute_api.API, 'rebuild', + side_effect=exception.AutoDiskConfigDisabledByImage( + image='dummy')) + def test_rebuild_instance_raise_auto_disk_config_exc(self, mock_rebuild): + body = { + "rebuild": { + "imageRef": self._image_href, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + req, FAKE_UUID, body=body) + + def test_resize_server(self): + + body = dict(resize=dict(flavorRef="http://localhost/3")) + + self.resize_called = False + + def resize_mock(*args): + self.resize_called = True + + self.stubs.Set(compute_api.API, 'resize', resize_mock) + + req = fakes.HTTPRequestV3.blank(self.url) + body = self.controller._action_resize(req, FAKE_UUID, body=body) + + self.assertEqual(self.resize_called, True) + + def test_resize_server_no_flavor(self): + body = dict(resize=dict()) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(exception.ValidationError, + self.controller._action_resize, + req, FAKE_UUID, body=body) + + def test_resize_server_no_flavor_ref(self): + body = dict(resize=dict(flavorRef=None)) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(exception.ValidationError, + self.controller._action_resize, + req, FAKE_UUID, body=body) + + def test_resize_with_server_not_found(self): + body = dict(resize=dict(flavorRef="http://localhost/3")) + + self.stubs.Set(compute_api.API, 'get', return_server_not_found) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._action_resize, + req, FAKE_UUID, body=body) + + def test_resize_with_image_exceptions(self): + body = dict(resize=dict(flavorRef="http://localhost/3")) + self.resize_called = 0 + image_id = 'fake_image_id' + + exceptions = [ + (exception.ImageNotAuthorized(image_id=image_id), + webob.exc.HTTPUnauthorized), + (exception.ImageNotFound(image_id=image_id), + webob.exc.HTTPBadRequest), + (exception.Invalid, webob.exc.HTTPBadRequest), + (exception.NoValidHost(reason='Bad host'), + webob.exc.HTTPBadRequest), + (exception.AutoDiskConfigDisabledByImage(image=image_id), + webob.exc.HTTPBadRequest), + ] + + raised, expected = map(iter, zip(*exceptions)) + + def _fake_resize(obj, context, instance, flavor_id): + self.resize_called += 1 + raise raised.next() + + self.stubs.Set(compute_api.API, 'resize', _fake_resize) + + for call_no in range(len(exceptions)): + req = fakes.HTTPRequestV3.blank(self.url) + next_exception = expected.next() + actual = self.assertRaises(next_exception, + self.controller._action_resize, + req, FAKE_UUID, body=body) + if (isinstance(exceptions[call_no][0], + exception.NoValidHost)): + self.assertEqual(actual.explanation, + 'No valid host was found. Bad host') + elif (isinstance(exceptions[call_no][0], + exception.AutoDiskConfigDisabledByImage)): + self.assertEqual(actual.explanation, + 'Requested image fake_image_id has automatic' + ' disk resize disabled.') + self.assertEqual(self.resize_called, call_no + 1) + + def test_resize_with_too_many_instances(self): + body = dict(resize=dict(flavorRef="http://localhost/3")) + + def fake_resize(*args, **kwargs): + raise exception.TooManyInstances(message="TooManyInstance") + + self.stubs.Set(compute_api.API, 'resize', fake_resize) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPForbidden, + self.controller._action_resize, + req, FAKE_UUID, body=body) + + @mock.patch('nova.compute.api.API.resize', + side_effect=exception.CannotResizeDisk(reason='')) + def test_resize_raises_cannot_resize_disk(self, mock_resize): + body = dict(resize=dict(flavorRef="http://localhost/3")) + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_resize, + req, FAKE_UUID, body=body) + + @mock.patch('nova.compute.api.API.resize', + side_effect=exception.FlavorNotFound(reason='', + flavor_id='fake_id')) + def test_resize_raises_flavor_not_found(self, mock_resize): + body = dict(resize=dict(flavorRef="http://localhost/3")) + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_resize, + req, FAKE_UUID, body=body) + + def test_resize_raises_conflict_on_invalid_state(self): + body = dict(resize=dict(flavorRef="http://localhost/3")) + + def fake_resize(*args, **kwargs): + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + + self.stubs.Set(compute_api.API, 'resize', fake_resize) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_resize, + req, FAKE_UUID, body=body) + + def test_confirm_resize_server(self): + body = dict(confirmResize=None) + + self.confirm_resize_called = False + + def cr_mock(*args): + self.confirm_resize_called = True + + self.stubs.Set(compute_api.API, 'confirm_resize', cr_mock) + + req = fakes.HTTPRequestV3.blank(self.url) + body = self.controller._action_confirm_resize(req, FAKE_UUID, body) + + self.assertEqual(self.confirm_resize_called, True) + + def test_confirm_resize_migration_not_found(self): + body = dict(confirmResize=None) + + def confirm_resize_mock(*args): + raise exception.MigrationNotFoundByStatus(instance_id=1, + status='finished') + + self.stubs.Set(compute_api.API, + 'confirm_resize', + confirm_resize_mock) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_confirm_resize, + req, FAKE_UUID, body) + + def test_confirm_resize_raises_conflict_on_invalid_state(self): + body = dict(confirmResize=None) + + def fake_confirm_resize(*args, **kwargs): + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + + self.stubs.Set(compute_api.API, 'confirm_resize', + fake_confirm_resize) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_confirm_resize, + req, FAKE_UUID, body) + + def test_revert_resize_migration_not_found(self): + body = dict(revertResize=None) + + def revert_resize_mock(*args): + raise exception.MigrationNotFoundByStatus(instance_id=1, + status='finished') + + self.stubs.Set(compute_api.API, + 'revert_resize', + revert_resize_mock) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_revert_resize, + req, FAKE_UUID, body) + + def test_revert_resize_server_not_found(self): + body = dict(revertResize=None) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob. exc.HTTPNotFound, + self.controller._action_revert_resize, + req, "bad_server_id", body) + + def test_revert_resize_server(self): + body = dict(revertResize=None) + + self.revert_resize_called = False + + def revert_mock(*args): + self.revert_resize_called = True + + self.stubs.Set(compute_api.API, 'revert_resize', revert_mock) + + req = fakes.HTTPRequestV3.blank(self.url) + body = self.controller._action_revert_resize(req, FAKE_UUID, body) + + self.assertEqual(self.revert_resize_called, True) + + def test_revert_resize_raises_conflict_on_invalid_state(self): + body = dict(revertResize=None) + + def fake_revert_resize(*args, **kwargs): + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + + self.stubs.Set(compute_api.API, 'revert_resize', + fake_revert_resize) + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_revert_resize, + req, FAKE_UUID, body) + + def test_create_image(self): + body = { + 'createImage': { + 'name': 'Snapshot 1', + }, + } + + req = fakes.HTTPRequestV3.blank(self.url) + response = self.controller._action_create_image(req, FAKE_UUID, body) + + location = response.headers['Location'] + self.assertEqual(glance.generate_image_url('123'), location) + + def test_create_image_name_too_long(self): + long_name = 'a' * 260 + body = { + 'createImage': { + 'name': long_name, + }, + } + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_create_image, req, + FAKE_UUID, body) + + def _do_test_create_volume_backed_image(self, extra_properties): + + def _fake_id(x): + return '%s-%s-%s-%s' % (x * 8, x * 4, x * 4, x * 12) + + body = dict(createImage=dict(name='snapshot_of_volume_backed')) + + if extra_properties: + body['createImage']['metadata'] = extra_properties + + image_service = glance.get_default_image_service() + + bdm = [dict(volume_id=_fake_id('a'), + volume_size=1, + device_name='vda', + delete_on_termination=False)] + props = dict(kernel_id=_fake_id('b'), + ramdisk_id=_fake_id('c'), + root_device_name='/dev/vda', + block_device_mapping=bdm) + original_image = dict(properties=props, + container_format='ami', + status='active', + is_public=True) + + image_service.create(None, original_image) + + def fake_block_device_mapping_get_all_by_instance(context, inst_id, + use_slave=False): + return [fake_block_device.FakeDbBlockDeviceDict( + {'volume_id': _fake_id('a'), + 'source_type': 'snapshot', + 'destination_type': 'volume', + 'volume_size': 1, + 'device_name': 'vda', + 'snapshot_id': 1, + 'boot_index': 0, + 'delete_on_termination': False, + 'no_device': None})] + + self.stubs.Set(db, 'block_device_mapping_get_all_by_instance', + fake_block_device_mapping_get_all_by_instance) + + instance = fakes.fake_instance_get(image_ref=original_image['id'], + vm_state=vm_states.ACTIVE, + root_device_name='/dev/vda') + self.stubs.Set(db, 'instance_get_by_uuid', instance) + + volume = dict(id=_fake_id('a'), + size=1, + host='fake', + display_description='fake') + snapshot = dict(id=_fake_id('d')) + self.mox.StubOutWithMock(self.controller.compute_api, 'volume_api') + volume_api = self.controller.compute_api.volume_api + volume_api.get(mox.IgnoreArg(), volume['id']).AndReturn(volume) + volume_api.create_snapshot_force(mox.IgnoreArg(), volume['id'], + mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(snapshot) + + self.mox.ReplayAll() + + req = fakes.HTTPRequestV3.blank(self.url) + response = self.controller._action_create_image(req, FAKE_UUID, body) + + location = response.headers['Location'] + image_id = location.replace(glance.generate_image_url(''), '') + image = image_service.show(None, image_id) + + self.assertEqual(image['name'], 'snapshot_of_volume_backed') + properties = image['properties'] + self.assertEqual(properties['kernel_id'], _fake_id('b')) + self.assertEqual(properties['ramdisk_id'], _fake_id('c')) + self.assertEqual(properties['root_device_name'], '/dev/vda') + self.assertEqual(properties['bdm_v2'], True) + bdms = properties['block_device_mapping'] + self.assertEqual(len(bdms), 1) + self.assertEqual(bdms[0]['boot_index'], 0) + self.assertEqual(bdms[0]['source_type'], 'snapshot') + self.assertEqual(bdms[0]['destination_type'], 'volume') + self.assertEqual(bdms[0]['snapshot_id'], snapshot['id']) + for fld in ('connection_info', 'id', + 'instance_uuid', 'device_name'): + self.assertNotIn(fld, bdms[0]) + for k in extra_properties.keys(): + self.assertEqual(properties[k], extra_properties[k]) + + def test_create_volume_backed_image_no_metadata(self): + self._do_test_create_volume_backed_image({}) + + def test_create_volume_backed_image_with_metadata(self): + self._do_test_create_volume_backed_image(dict(ImageType='Gold', + ImageVersion='2.0')) + + def _test_create_volume_backed_image_with_metadata_from_volume( + self, extra_metadata=None): + + def _fake_id(x): + return '%s-%s-%s-%s' % (x * 8, x * 4, x * 4, x * 12) + + body = dict(createImage=dict(name='snapshot_of_volume_backed')) + if extra_metadata: + body['createImage']['metadata'] = extra_metadata + + image_service = glance.get_default_image_service() + + def fake_block_device_mapping_get_all_by_instance(context, inst_id, + use_slave=False): + return [fake_block_device.FakeDbBlockDeviceDict( + {'volume_id': _fake_id('a'), + 'source_type': 'snapshot', + 'destination_type': 'volume', + 'volume_size': 1, + 'device_name': 'vda', + 'snapshot_id': 1, + 'boot_index': 0, + 'delete_on_termination': False, + 'no_device': None})] + + self.stubs.Set(db, 'block_device_mapping_get_all_by_instance', + fake_block_device_mapping_get_all_by_instance) + + instance = fakes.fake_instance_get(image_ref='', + vm_state=vm_states.ACTIVE, + root_device_name='/dev/vda') + self.stubs.Set(db, 'instance_get_by_uuid', instance) + + fake_metadata = {'test_key1': 'test_value1', + 'test_key2': 'test_value2'} + volume = dict(id=_fake_id('a'), + size=1, + host='fake', + display_description='fake', + volume_image_metadata=fake_metadata) + snapshot = dict(id=_fake_id('d')) + self.mox.StubOutWithMock(self.controller.compute_api, 'volume_api') + volume_api = self.controller.compute_api.volume_api + volume_api.get(mox.IgnoreArg(), volume['id']).AndReturn(volume) + volume_api.get(mox.IgnoreArg(), volume['id']).AndReturn(volume) + volume_api.create_snapshot_force(mox.IgnoreArg(), volume['id'], + mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(snapshot) + + req = fakes.HTTPRequestV3.blank(self.url) + + self.mox.ReplayAll() + response = self.controller._action_create_image(req, FAKE_UUID, body) + location = response.headers['Location'] + image_id = location.replace('http://localhost:9292/images/', '') + image = image_service.show(None, image_id) + + properties = image['properties'] + self.assertEqual(properties['test_key1'], 'test_value1') + self.assertEqual(properties['test_key2'], 'test_value2') + if extra_metadata: + for key, val in extra_metadata.items(): + self.assertEqual(properties[key], val) + + def test_create_vol_backed_img_with_meta_from_vol_without_extra_meta(self): + self._test_create_volume_backed_image_with_metadata_from_volume() + + def test_create_vol_backed_img_with_meta_from_vol_with_extra_meta(self): + self._test_create_volume_backed_image_with_metadata_from_volume( + extra_metadata={'a': 'b'}) + + def test_create_image_snapshots_disabled(self): + """Don't permit a snapshot if the allow_instance_snapshots flag is + False + """ + self.flags(allow_instance_snapshots=False) + body = { + 'createImage': { + 'name': 'Snapshot 1', + }, + } + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_create_image, + req, FAKE_UUID, body) + + def test_create_image_with_metadata(self): + body = { + 'createImage': { + 'name': 'Snapshot 1', + 'metadata': {'key': 'asdf'}, + }, + } + + req = fakes.HTTPRequestV3.blank(self.url) + response = self.controller._action_create_image(req, FAKE_UUID, body) + + location = response.headers['Location'] + self.assertEqual(glance.generate_image_url('123'), location) + + def test_create_image_with_too_much_metadata(self): + body = { + 'createImage': { + 'name': 'Snapshot 1', + 'metadata': {}, + }, + } + for num in range(CONF.quota_metadata_items + 1): + body['createImage']['metadata']['foo%i' % num] = "bar" + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPForbidden, + self.controller._action_create_image, + req, FAKE_UUID, body) + + def test_create_image_no_name(self): + body = { + 'createImage': {}, + } + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_create_image, + req, FAKE_UUID, body) + + def test_create_image_blank_name(self): + body = { + 'createImage': { + 'name': '', + } + } + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_create_image, + req, FAKE_UUID, body) + + def test_create_image_bad_metadata(self): + body = { + 'createImage': { + 'name': 'geoff', + 'metadata': 'henry', + }, + } + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_create_image, + req, FAKE_UUID, body) + + def test_create_image_raises_conflict_on_invalid_state(self): + def snapshot(*args, **kwargs): + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + self.stubs.Set(compute_api.API, 'snapshot', snapshot) + + body = { + "createImage": { + "name": "test_snapshot", + }, + } + + req = fakes.HTTPRequestV3.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_create_image, + req, FAKE_UUID, body) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_external_events.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_external_events.py new file mode 100644 index 0000000000..e9bd4538a0 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_external_events.py @@ -0,0 +1,140 @@ +# Copyright 2014 Red Hat, 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.serialization import jsonutils +import webob + +from nova.api.openstack.compute.plugins.v3 import server_external_events +from nova import context +from nova import exception +from nova import objects +from nova import test + +fake_instances = { + '00000000-0000-0000-0000-000000000001': objects.Instance( + uuid='00000000-0000-0000-0000-000000000001', host='host1'), + '00000000-0000-0000-0000-000000000002': objects.Instance( + uuid='00000000-0000-0000-0000-000000000002', host='host1'), + '00000000-0000-0000-0000-000000000003': objects.Instance( + uuid='00000000-0000-0000-0000-000000000003', host='host2'), +} +fake_instance_uuids = sorted(fake_instances.keys()) +MISSING_UUID = '00000000-0000-0000-0000-000000000004' + + +@classmethod +def fake_get_by_uuid(cls, context, uuid): + try: + return fake_instances[uuid] + except KeyError: + raise exception.InstanceNotFound(instance_id=uuid) + + +@mock.patch('nova.objects.instance.Instance.get_by_uuid', fake_get_by_uuid) +class ServerExternalEventsTest(test.NoDBTestCase): + def setUp(self): + super(ServerExternalEventsTest, self).setUp() + self.api = server_external_events.ServerExternalEventsController() + self.context = context.get_admin_context() + self.default_body = { + 'events': [ + {'name': 'network-vif-plugged', + 'tag': 'foo', + 'status': 'completed', + 'server_uuid': fake_instance_uuids[0]}, + {'name': 'network-changed', + 'status': 'completed', + 'server_uuid': fake_instance_uuids[1]}, + ] + } + + def _create_req(self, body): + req = webob.Request.blank('/v2/fake/os-server-external-events') + req.method = 'POST' + req.headers['content-type'] = 'application/json' + req.environ['nova.context'] = self.context + req.body = jsonutils.dumps(body) + return req + + def _assert_call(self, req, body, expected_uuids, expected_events): + with mock.patch.object(self.api.compute_api, + 'external_instance_event') as api_method: + response = self.api.create(req, body) + + result = response.obj + code = response._code + + self.assertEqual(1, api_method.call_count) + for inst in api_method.call_args_list[0][0][1]: + expected_uuids.remove(inst.uuid) + self.assertEqual([], expected_uuids) + for event in api_method.call_args_list[0][0][2]: + expected_events.remove(event.name) + self.assertEqual([], expected_events) + return result, code + + def test_create(self): + req = self._create_req(self.default_body) + result, code = self._assert_call(req, self.default_body, + fake_instance_uuids[:2], + ['network-vif-plugged', + 'network-changed']) + self.assertEqual(self.default_body, result) + self.assertEqual(200, code) + + def test_create_one_bad_instance(self): + body = self.default_body + body['events'][1]['server_uuid'] = MISSING_UUID + req = self._create_req(body) + result, code = self._assert_call(req, body, [fake_instance_uuids[0]], + ['network-vif-plugged']) + self.assertEqual('failed', result['events'][1]['status']) + self.assertEqual(200, result['events'][0]['code']) + self.assertEqual(404, result['events'][1]['code']) + self.assertEqual(207, code) + + def test_create_no_good_instances(self): + body = self.default_body + body['events'][0]['server_uuid'] = MISSING_UUID + body['events'][1]['server_uuid'] = MISSING_UUID + req = self._create_req(body) + self.assertRaises(webob.exc.HTTPNotFound, + self.api.create, req, body) + + def test_create_bad_status(self): + body = self.default_body + body['events'][1]['status'] = 'foo' + req = self._create_req(body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.api.create, req, body) + + def test_create_extra_gorp(self): + body = self.default_body + body['events'][0]['foobar'] = 'bad stuff' + req = self._create_req(body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.api.create, req, body) + + def test_create_bad_events(self): + body = {'events': 'foo'} + req = self._create_req(body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.api.create, req, body) + + def test_create_bad_body(self): + body = {'foo': 'bar'} + req = self._create_req(body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.api.create, req, body) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_password.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_password.py new file mode 100644 index 0000000000..20a8c1e0a1 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_server_password.py @@ -0,0 +1,80 @@ +# Copyright 2012 Nebula, Inc. +# 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. + +from oslo.config import cfg +from oslo.serialization import jsonutils +import webob + +from nova.api.metadata import password +from nova import compute +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + + +CONF = cfg.CONF + + +class ServerPasswordTest(test.TestCase): + content_type = 'application/json' + + def setUp(self): + super(ServerPasswordTest, self).setUp() + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set( + compute.api.API, 'get', + lambda self, ctxt, *a, **kw: + fake_instance.fake_instance_obj( + ctxt, + system_metadata={}, + expected_attrs=['system_metadata'])) + self.password = 'fakepass' + + def fake_extract_password(instance): + return self.password + + def fake_convert_password(context, password): + self.password = password + return {} + + self.stubs.Set(password, 'extract_password', fake_extract_password) + self.stubs.Set(password, 'convert_password', fake_convert_password) + + def _make_request(self, url, method='GET'): + req = webob.Request.blank(url) + req.headers['Accept'] = self.content_type + req.method = method + res = req.get_response( + fakes.wsgi_app_v21(init_only=('servers', 'os-server-password'))) + return res + + def _get_pass(self, body): + return jsonutils.loads(body).get('password') + + def test_get_password(self): + url = '/v2/fake/servers/fake/os-server-password' + res = self._make_request(url) + + self.assertEqual(res.status_int, 200) + self.assertEqual(self._get_pass(res.body), 'fakepass') + + def test_reset_password(self): + url = '/v2/fake/servers/fake/os-server-password' + res = self._make_request(url, 'DELETE') + self.assertEqual(res.status_int, 204) + + res = self._make_request(url) + self.assertEqual(res.status_int, 200) + self.assertEqual(self._get_pass(res.body), '') diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_servers.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_servers.py new file mode 100644 index 0000000000..6eb92902fe --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_servers.py @@ -0,0 +1,3353 @@ +# Copyright 2010-2011 OpenStack Foundation +# Copyright 2011 Piston Cloud Computing, Inc. +# 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 base64 +import contextlib +import copy +import datetime +import uuid + +import iso8601 +import mock +import mox +from oslo.config import cfg +from oslo.serialization import jsonutils +from oslo.utils import timeutils +import six.moves.urllib.parse as urlparse +import testtools +import webob + +from nova.api.openstack import compute +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import disk_config +from nova.api.openstack.compute.plugins.v3 import ips +from nova.api.openstack.compute.plugins.v3 import keypairs +from nova.api.openstack.compute.plugins.v3 import servers +from nova.api.openstack.compute.schemas.v3 import disk_config as \ + disk_config_schema +from nova.api.openstack.compute.schemas.v3 import servers as servers_schema +from nova.api.openstack.compute import views +from nova.api.openstack import extensions +from nova.compute import api as compute_api +from nova.compute import delete_types +from nova.compute import flavors +from nova.compute import task_states +from nova.compute import vm_states +from nova import context +from nova import db +from nova.db.sqlalchemy import models +from nova import exception +from nova.i18n import _ +from nova.image import glance +from nova.network import manager +from nova.network.neutronv2 import api as neutron_api +from nova import objects +from nova.objects import instance as instance_obj +from nova.openstack.common import policy as common_policy +from nova import policy +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance +from nova.tests.unit import fake_network +from nova.tests.unit.image import fake +from nova.tests.unit import matchers +from nova import utils as nova_utils + +CONF = cfg.CONF +CONF.import_opt('password_length', 'nova.utils') + +FAKE_UUID = fakes.FAKE_UUID + +INSTANCE_IDS = {FAKE_UUID: 1} +FIELDS = instance_obj.INSTANCE_DEFAULT_FIELDS + + +def fake_gen_uuid(): + return FAKE_UUID + + +def return_servers_empty(context, *args, **kwargs): + return [] + + +def instance_update_and_get_original(context, instance_uuid, values, + update_cells=True, + columns_to_join=None, + ): + inst = fakes.stub_instance(INSTANCE_IDS.get(instance_uuid), + name=values.get('display_name')) + inst = dict(inst, **values) + return (inst, inst) + + +def instance_update(context, instance_uuid, values, update_cells=True): + inst = fakes.stub_instance(INSTANCE_IDS.get(instance_uuid), + name=values.get('display_name')) + inst = dict(inst, **values) + return inst + + +def fake_compute_api(cls, req, id): + return True + + +def fake_start_stop_not_ready(self, context, instance): + raise exception.InstanceNotReady(instance_id=instance["uuid"]) + + +def fake_start_stop_invalid_state(self, context, instance): + raise exception.InstanceInvalidState( + instance_uuid=instance['uuid'], attr='fake_attr', + method='fake_method', state='fake_state') + + +def fake_instance_get_by_uuid_not_found(context, uuid, + columns_to_join, use_slave=False): + raise exception.InstanceNotFound(instance_id=uuid) + + +class MockSetAdminPassword(object): + def __init__(self): + self.instance_id = None + self.password = None + + def __call__(self, context, instance_id, password): + self.instance_id = instance_id + self.password = password + + +class Base64ValidationTest(test.TestCase): + def setUp(self): + super(Base64ValidationTest, self).setUp() + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers.ServersController(extension_info=ext_info) + + def test_decode_base64(self): + value = "A random string" + result = self.controller._decode_base64(base64.b64encode(value)) + self.assertEqual(result, value) + + def test_decode_base64_binary(self): + value = "\x00\x12\x75\x99" + result = self.controller._decode_base64(base64.b64encode(value)) + self.assertEqual(result, value) + + def test_decode_base64_whitespace(self): + value = "A random string" + encoded = base64.b64encode(value) + white = "\n \n%s\t%s\n" % (encoded[:2], encoded[2:]) + result = self.controller._decode_base64(white) + self.assertEqual(result, value) + + def test_decode_base64_invalid(self): + invalid = "A random string" + result = self.controller._decode_base64(invalid) + self.assertIsNone(result) + + def test_decode_base64_illegal_bytes(self): + value = "A random string" + encoded = base64.b64encode(value) + white = ">\x01%s*%s()" % (encoded[:2], encoded[2:]) + result = self.controller._decode_base64(white) + self.assertIsNone(result) + + +class NeutronV2Subclass(neutron_api.API): + """Used to ensure that API handles subclasses properly.""" + pass + + +class ControllerTest(test.TestCase): + + def setUp(self): + super(ControllerTest, self).setUp() + self.flags(verbose=True, use_ipv6=False) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + fake.stub_out_image_service(self.stubs) + return_server = fakes.fake_instance_get() + return_servers = fakes.fake_instance_get_all_by_filters() + self.stubs.Set(db, 'instance_get_all_by_filters', + return_servers) + self.stubs.Set(db, 'instance_get_by_uuid', + return_server) + self.stubs.Set(db, 'instance_update_and_get_original', + instance_update_and_get_original) + + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers.ServersController(extension_info=ext_info) + self.ips_controller = ips.IPsController() + policy.reset() + policy.init() + fake_network.stub_out_nw_api_get_instance_nw_info(self.stubs) + + +class ServersControllerTest(ControllerTest): + + def setUp(self): + super(ServersControllerTest, self).setUp() + CONF.set_override('host', 'localhost', group='glance') + + def test_requested_networks_prefix(self): + uuid = 'br-00000000-0000-0000-0000-000000000000' + requested_networks = [{'uuid': uuid}] + res = self.controller._get_requested_networks(requested_networks) + self.assertIn((uuid, None), res.as_tuples()) + + def test_requested_networks_neutronv2_enabled_with_port(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'port': port}] + res = self.controller._get_requested_networks(requested_networks) + self.assertEqual([(None, None, port, None)], res.as_tuples()) + + def test_requested_networks_neutronv2_enabled_with_network(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + requested_networks = [{'uuid': network}] + res = self.controller._get_requested_networks(requested_networks) + self.assertEqual([(network, None, None, None)], res.as_tuples()) + + def test_requested_networks_neutronv2_enabled_with_network_and_port(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network, 'port': port}] + res = self.controller._get_requested_networks(requested_networks) + self.assertEqual([(None, None, port, None)], res.as_tuples()) + + def test_requested_networks_neutronv2_enabled_conflict_on_fixed_ip(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + addr = '10.0.0.1' + requested_networks = [{'uuid': network, + 'fixed_ip': addr, + 'port': port}] + self.assertRaises( + webob.exc.HTTPBadRequest, + self.controller._get_requested_networks, + requested_networks) + + def test_requested_networks_neutronv2_disabled_with_port(self): + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'port': port}] + self.assertRaises( + webob.exc.HTTPBadRequest, + self.controller._get_requested_networks, + requested_networks) + + def test_requested_networks_api_enabled_with_v2_subclass(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network, 'port': port}] + res = self.controller._get_requested_networks(requested_networks) + self.assertEqual([(None, None, port, None)], res.as_tuples()) + + def test_requested_networks_neutronv2_subclass_with_port(self): + cls = ('nova.tests.unit.api.openstack.compute' + + '.test_servers.NeutronV2Subclass') + self.flags(network_api_class=cls) + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'port': port}] + res = self.controller._get_requested_networks(requested_networks) + self.assertEqual([(None, None, port, None)], res.as_tuples()) + + def test_get_server_by_uuid(self): + req = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID) + res_dict = self.controller.show(req, FAKE_UUID) + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + + def test_get_server_joins_pci_devices(self): + self.expected_attrs = None + + def fake_get(_self, *args, **kwargs): + self.expected_attrs = kwargs['expected_attrs'] + ctxt = context.RequestContext('fake', 'fake') + return fake_instance.fake_instance_obj(ctxt) + + self.stubs.Set(compute_api.API, 'get', fake_get) + + req = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID) + self.controller.show(req, FAKE_UUID) + + self.assertIn('pci_devices', self.expected_attrs) + + def test_unique_host_id(self): + """Create two servers with the same host and different + project_ids and check that the host_id's are unique. + """ + def return_instance_with_host(self, *args, **kwargs): + project_id = str(uuid.uuid4()) + return fakes.stub_instance(id=1, uuid=FAKE_UUID, + project_id=project_id, + host='fake_host') + + self.stubs.Set(db, 'instance_get_by_uuid', + return_instance_with_host) + self.stubs.Set(db, 'instance_get', + return_instance_with_host) + + req = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID) + server1 = self.controller.show(req, FAKE_UUID) + server2 = self.controller.show(req, FAKE_UUID) + + self.assertNotEqual(server1['server']['hostId'], + server2['server']['hostId']) + + def _get_server_data_dict(self, uuid, image_bookmark, flavor_bookmark, + status="ACTIVE", progress=100): + return { + "server": { + "id": uuid, + "user_id": "fake_user", + "tenant_id": "fake_project", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": progress, + "name": "server1", + "status": status, + "hostId": '', + "image": { + "id": "10", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'test1': [ + {'version': 4, 'addr': '192.168.1.100', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}, + {'version': 6, 'addr': '2001:db8:0:1::1', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'} + ] + }, + "metadata": { + "seq": "1", + }, + "links": [ + { + "rel": "self", + "href": "http://localhost/v3/servers/%s" % uuid, + }, + { + "rel": "bookmark", + "href": "http://localhost/servers/%s" % uuid, + }, + ], + } + } + + def test_get_server_by_id(self): + self.flags(use_ipv6=True) + image_bookmark = "http://localhost/images/10" + flavor_bookmark = "http://localhost/flavors/1" + + uuid = FAKE_UUID + req = fakes.HTTPRequestV3.blank('/servers/%s' % uuid) + res_dict = self.controller.show(req, uuid) + + expected_server = self._get_server_data_dict(uuid, + image_bookmark, + flavor_bookmark, + status="BUILD", + progress=0) + + self.assertThat(res_dict, matchers.DictMatches(expected_server)) + + def test_get_server_with_active_status_by_id(self): + image_bookmark = "http://localhost/images/10" + flavor_bookmark = "http://localhost/flavors/1" + + new_return_server = fakes.fake_instance_get( + vm_state=vm_states.ACTIVE, progress=100) + self.stubs.Set(db, 'instance_get_by_uuid', new_return_server) + + uuid = FAKE_UUID + req = fakes.HTTPRequestV3.blank('/servers/%s' % uuid) + res_dict = self.controller.show(req, uuid) + expected_server = self._get_server_data_dict(uuid, + image_bookmark, + flavor_bookmark) + self.assertThat(res_dict, matchers.DictMatches(expected_server)) + + def test_get_server_with_id_image_ref_by_id(self): + image_ref = "10" + image_bookmark = "http://localhost/images/10" + flavor_id = "1" + flavor_bookmark = "http://localhost/flavors/1" + + new_return_server = fakes.fake_instance_get( + vm_state=vm_states.ACTIVE, image_ref=image_ref, + flavor_id=flavor_id, progress=100) + self.stubs.Set(db, 'instance_get_by_uuid', new_return_server) + + uuid = FAKE_UUID + req = fakes.HTTPRequestV3.blank('/servers/%s' % uuid) + res_dict = self.controller.show(req, uuid) + expected_server = self._get_server_data_dict(uuid, + image_bookmark, + flavor_bookmark) + + self.assertThat(res_dict, matchers.DictMatches(expected_server)) + + def test_get_server_addresses_from_cache(self): + pub0 = ('172.19.0.1', '172.19.0.2',) + pub1 = ('1.2.3.4',) + pub2 = ('b33f::fdee:ddff:fecc:bbaa',) + priv0 = ('192.168.0.3', '192.168.0.4',) + + def _ip(ip): + return {'address': ip, 'type': 'fixed'} + + nw_cache = [ + {'address': 'aa:aa:aa:aa:aa:aa', + 'id': 1, + 'network': {'bridge': 'br0', + 'id': 1, + 'label': 'public', + 'subnets': [{'cidr': '172.19.0.0/24', + 'ips': [_ip(ip) for ip in pub0]}, + {'cidr': '1.2.3.0/16', + 'ips': [_ip(ip) for ip in pub1]}, + {'cidr': 'b33f::/64', + 'ips': [_ip(ip) for ip in pub2]}]}}, + {'address': 'bb:bb:bb:bb:bb:bb', + 'id': 2, + 'network': {'bridge': 'br1', + 'id': 2, + 'label': 'private', + 'subnets': [{'cidr': '192.168.0.0/24', + 'ips': [_ip(ip) for ip in priv0]}]}}] + + return_server = fakes.fake_instance_get(nw_cache=nw_cache) + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + req = fakes.HTTPRequestV3.blank('/servers/%s/ips' % FAKE_UUID) + res_dict = self.ips_controller.index(req, FAKE_UUID) + + expected = { + 'addresses': { + 'private': [ + {'version': 4, 'addr': '192.168.0.3', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'bb:bb:bb:bb:bb:bb'}, + {'version': 4, 'addr': '192.168.0.4', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'bb:bb:bb:bb:bb:bb'}, + ], + 'public': [ + {'version': 4, 'addr': '172.19.0.1', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}, + {'version': 4, 'addr': '172.19.0.2', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}, + {'version': 4, 'addr': '1.2.3.4', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}, + {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}, + ], + }, + } + self.assertThat(res_dict, matchers.DictMatches(expected)) + + def test_get_server_addresses_nonexistent_network(self): + url = '/v3/servers/%s/ips/network_0' % FAKE_UUID + req = fakes.HTTPRequestV3.blank(url) + self.assertRaises(webob.exc.HTTPNotFound, self.ips_controller.show, + req, FAKE_UUID, 'network_0') + + def test_get_server_addresses_nonexistent_server(self): + def fake_instance_get(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) + + server_id = str(uuid.uuid4()) + req = fakes.HTTPRequestV3.blank('/servers/%s/ips' % server_id) + self.assertRaises(webob.exc.HTTPNotFound, + self.ips_controller.index, req, server_id) + + def test_get_server_list_empty(self): + self.stubs.Set(db, 'instance_get_all_by_filters', + return_servers_empty) + + req = fakes.HTTPRequestV3.blank('/servers') + res_dict = self.controller.index(req) + + num_servers = len(res_dict['servers']) + self.assertEqual(0, num_servers) + + def test_get_server_list_with_reservation_id(self): + req = fakes.HTTPRequestV3.blank('/servers?reservation_id=foo') + res_dict = self.controller.index(req) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s.get('name'), 'server%d' % (i + 1)) + i += 1 + + def test_get_server_list_with_reservation_id_empty(self): + req = fakes.HTTPRequestV3.blank('/servers/detail?' + 'reservation_id=foo') + res_dict = self.controller.detail(req) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s.get('name'), 'server%d' % (i + 1)) + i += 1 + + def test_get_server_list_with_reservation_id_details(self): + req = fakes.HTTPRequestV3.blank('/servers/detail?' + 'reservation_id=foo') + res_dict = self.controller.detail(req) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s.get('name'), 'server%d' % (i + 1)) + i += 1 + + def test_get_server_list(self): + req = fakes.HTTPRequestV3.blank('/servers') + res_dict = self.controller.index(req) + + self.assertEqual(len(res_dict['servers']), 5) + for i, s in enumerate(res_dict['servers']): + self.assertEqual(s['id'], fakes.get_fake_uuid(i)) + self.assertEqual(s['name'], 'server%d' % (i + 1)) + self.assertIsNone(s.get('image', None)) + + expected_links = [ + { + "rel": "self", + "href": "http://localhost/v3/servers/%s" % s['id'], + }, + { + "rel": "bookmark", + "href": "http://localhost/servers/%s" % s['id'], + }, + ] + + self.assertEqual(s['links'], expected_links) + + def test_get_servers_with_limit(self): + req = fakes.HTTPRequestV3.blank('/servers?limit=3') + res_dict = self.controller.index(req) + + servers = res_dict['servers'] + self.assertEqual([s['id'] for s in servers], + [fakes.get_fake_uuid(i) for i in xrange(len(servers))]) + + servers_links = res_dict['servers_links'] + self.assertEqual(servers_links[0]['rel'], 'next') + href_parts = urlparse.urlparse(servers_links[0]['href']) + self.assertEqual('/v3/servers', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + expected_params = {'limit': ['3'], + 'marker': [fakes.get_fake_uuid(2)]} + self.assertThat(params, matchers.DictMatches(expected_params)) + + def test_get_servers_with_limit_bad_value(self): + req = fakes.HTTPRequestV3.blank('/servers?limit=aaa') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_get_server_details_empty(self): + self.stubs.Set(db, 'instance_get_all_by_filters', + return_servers_empty) + + req = fakes.HTTPRequestV3.blank('/servers/detail') + res_dict = self.controller.detail(req) + + num_servers = len(res_dict['servers']) + self.assertEqual(0, num_servers) + + def test_get_server_details_with_limit(self): + req = fakes.HTTPRequestV3.blank('/servers/detail?limit=3') + res = self.controller.detail(req) + + servers = res['servers'] + self.assertEqual([s['id'] for s in servers], + [fakes.get_fake_uuid(i) for i in xrange(len(servers))]) + + servers_links = res['servers_links'] + self.assertEqual(servers_links[0]['rel'], 'next') + + href_parts = urlparse.urlparse(servers_links[0]['href']) + self.assertEqual('/v3/servers/detail', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + expected = {'limit': ['3'], 'marker': [fakes.get_fake_uuid(2)]} + self.assertThat(params, matchers.DictMatches(expected)) + + def test_get_server_details_with_limit_bad_value(self): + req = fakes.HTTPRequestV3.blank('/servers/detail?limit=aaa') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.detail, req) + + def test_get_server_details_with_limit_and_other_params(self): + req = fakes.HTTPRequestV3.blank('/servers/detail' + '?limit=3&blah=2:t') + res = self.controller.detail(req) + + servers = res['servers'] + self.assertEqual([s['id'] for s in servers], + [fakes.get_fake_uuid(i) for i in xrange(len(servers))]) + + servers_links = res['servers_links'] + self.assertEqual(servers_links[0]['rel'], 'next') + + href_parts = urlparse.urlparse(servers_links[0]['href']) + self.assertEqual('/v3/servers/detail', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + expected = {'limit': ['3'], 'blah': ['2:t'], + 'marker': [fakes.get_fake_uuid(2)]} + self.assertThat(params, matchers.DictMatches(expected)) + + def test_get_servers_with_too_big_limit(self): + req = fakes.HTTPRequestV3.blank('/servers?limit=30') + res_dict = self.controller.index(req) + self.assertNotIn('servers_links', res_dict) + + def test_get_servers_with_bad_limit(self): + req = fakes.HTTPRequestV3.blank('/servers?limit=asdf') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_get_servers_with_marker(self): + url = '/v3/servers?marker=%s' % fakes.get_fake_uuid(2) + req = fakes.HTTPRequestV3.blank(url) + servers = self.controller.index(req)['servers'] + self.assertEqual([s['name'] for s in servers], ["server4", "server5"]) + + def test_get_servers_with_limit_and_marker(self): + url = '/v3/servers?limit=2&marker=%s' % fakes.get_fake_uuid(1) + req = fakes.HTTPRequestV3.blank(url) + servers = self.controller.index(req)['servers'] + self.assertEqual([s['name'] for s in servers], ['server3', 'server4']) + + def test_get_servers_with_bad_marker(self): + req = fakes.HTTPRequestV3.blank('/servers?limit=2&marker=asdf') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_get_servers_with_bad_option(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?unknownoption=whee') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_allows_image(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.assertIsNotNone(search_opts) + self.assertIn('image', search_opts) + self.assertEqual(search_opts['image'], '12345') + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?image=12345') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_tenant_id_filter_converts_to_project_id_for_admin(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False, + expected_attrs=None): + self.assertIsNotNone(filters) + self.assertEqual(filters['project_id'], 'newfake') + self.assertFalse(filters.get('tenant_id')) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers' + '?all_tenants=1&tenant_id=newfake', + use_admin_context=True) + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_tenant_id_filter_no_admin_context(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False, + expected_attrs=None): + self.assertNotEqual(filters, None) + self.assertEqual(filters['project_id'], 'fake') + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?tenant_id=newfake') + res = self.controller.index(req) + self.assertIn('servers', res) + + def test_tenant_id_filter_implies_all_tenants(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False, + expected_attrs=None): + self.assertNotEqual(filters, None) + # The project_id assertion checks that the project_id + # filter is set to that specified in the request url and + # not that of the context, verifying that the all_tenants + # flag was enabled + self.assertEqual(filters['project_id'], 'newfake') + self.assertFalse(filters.get('tenant_id')) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?tenant_id=newfake', + use_admin_context=True) + res = self.controller.index(req) + self.assertIn('servers', res) + + def test_all_tenants_param_normal(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False, + expected_attrs=None): + self.assertNotIn('project_id', filters) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?all_tenants', + use_admin_context=True) + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_param_one(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False, + expected_attrs=None): + self.assertNotIn('project_id', filters) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?all_tenants=1', + use_admin_context=True) + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_param_zero(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False, + expected_attrs=None): + self.assertNotIn('all_tenants', filters) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?all_tenants=0', + use_admin_context=True) + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_param_false(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False, + expected_attrs=None): + self.assertNotIn('all_tenants', filters) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?all_tenants=false', + use_admin_context=True) + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_param_invalid(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, + expected_attrs=None): + self.assertNotIn('all_tenants', filters) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?all_tenants=xxx', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_admin_restricted_tenant(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False, + expected_attrs=None): + self.assertIsNotNone(filters) + self.assertEqual(filters['project_id'], 'fake') + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers', + use_admin_context=True) + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_pass_policy(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False, + expected_attrs=None): + self.assertIsNotNone(filters) + self.assertNotIn('project_id', filters) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + rules = { + "compute:get_all_tenants": + common_policy.parse_rule("project_id:fake"), + "compute:get_all": + common_policy.parse_rule("project_id:fake"), + } + + policy.set_rules(rules) + + req = fakes.HTTPRequestV3.blank('/servers?all_tenants=1') + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_fail_policy(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None): + self.assertIsNotNone(filters) + return [fakes.stub_instance(100)] + + rules = { + "compute:get_all_tenants": + common_policy.parse_rule("project_id:non_fake"), + "compute:get_all": + common_policy.parse_rule("project_id:fake"), + } + + policy.set_rules(rules) + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?all_tenants=1') + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.index, req) + + def test_get_servers_allows_flavor(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.assertIsNotNone(search_opts) + self.assertIn('flavor', search_opts) + # flavor is an integer ID + self.assertEqual(search_opts['flavor'], '12345') + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?flavor=12345') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_with_bad_flavor(self): + req = fakes.HTTPRequestV3.blank('/servers?flavor=abcde') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 0) + + def test_get_server_details_with_bad_flavor(self): + req = fakes.HTTPRequestV3.blank('/servers?flavor=abcde') + servers = self.controller.detail(req)['servers'] + + self.assertThat(servers, testtools.matchers.HasLength(0)) + + def test_get_servers_allows_status(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.assertIsNotNone(search_opts) + self.assertIn('vm_state', search_opts) + self.assertEqual(search_opts['vm_state'], [vm_states.ACTIVE]) + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?status=active') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_allows_task_status(self): + server_uuid = str(uuid.uuid4()) + task_state = task_states.REBOOTING + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.assertIsNotNone(search_opts) + self.assertIn('task_state', search_opts) + self.assertEqual([task_states.REBOOT_PENDING, + task_states.REBOOT_STARTED, + task_states.REBOOTING], + search_opts['task_state']) + db_list = [fakes.stub_instance(100, uuid=server_uuid, + task_state=task_state)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?status=reboot') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_resize_status(self): + # Test when resize status, it maps list of vm states. + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.assertIn('vm_state', search_opts) + self.assertEqual(search_opts['vm_state'], + [vm_states.ACTIVE, vm_states.STOPPED]) + + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?status=resize') + + servers = self.controller.detail(req)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_invalid_status(self): + # Test getting servers by invalid status. + req = fakes.HTTPRequestV3.blank('/servers?status=baloney', + use_admin_context=False) + servers = self.controller.index(req)['servers'] + self.assertEqual(len(servers), 0) + + def test_get_servers_deleted_status_as_user(self): + req = fakes.HTTPRequestV3.blank('/servers?status=deleted', + use_admin_context=False) + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.detail, req) + + def test_get_servers_deleted_status_as_admin(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.assertIn('vm_state', search_opts) + self.assertEqual(search_opts['vm_state'], ['deleted']) + + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?status=deleted', + use_admin_context=True) + + servers = self.controller.detail(req)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_allows_name(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.assertIsNotNone(search_opts) + self.assertIn('name', search_opts) + self.assertEqual(search_opts['name'], 'whee.*') + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?name=whee.*') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + @mock.patch.object(compute_api.API, 'get_all') + def test_get_servers_flavor_not_found(self, get_all_mock): + get_all_mock.side_effect = exception.FlavorNotFound(flavor_id=1) + + req = fakes.HTTPRequest.blank( + '/fake/servers?status=active&flavor=abc') + servers = self.controller.index(req)['servers'] + self.assertEqual(0, len(servers)) + + def test_get_servers_allows_changes_since(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.assertIsNotNone(search_opts) + self.assertIn('changes-since', search_opts) + changes_since = datetime.datetime(2011, 1, 24, 17, 8, 1, + tzinfo=iso8601.iso8601.UTC) + self.assertEqual(search_opts['changes-since'], changes_since) + self.assertNotIn('deleted', search_opts) + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + params = 'changes-since=2011-01-24T17:08:01Z' + req = fakes.HTTPRequestV3.blank('/servers?%s' % params) + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_allows_changes_since_bad_value(self): + params = 'changes-since=asdf' + req = fakes.HTTPRequestV3.blank('/servers?%s' % params) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) + + def test_get_servers_admin_filters_as_user(self): + """Test getting servers by admin-only or unknown options when + context is not admin. Make sure the admin and unknown options + are stripped before they get to compute_api.get_all() + """ + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.assertIsNotNone(search_opts) + # Allowed by user + self.assertIn('name', search_opts) + self.assertIn('ip', search_opts) + # OSAPI converts status to vm_state + self.assertIn('vm_state', search_opts) + # Allowed only by admins with admin API on + self.assertNotIn('unknown_option', search_opts) + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" + req = fakes.HTTPRequest.blank('/servers?%s' % query_str) + res = self.controller.index(req) + + servers = res['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_admin_options_as_admin(self): + """Test getting servers by admin-only or unknown options when + context is admin. All options should be passed + """ + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.assertIsNotNone(search_opts) + # Allowed by user + self.assertIn('name', search_opts) + # OSAPI converts status to vm_state + self.assertIn('vm_state', search_opts) + # Allowed only by admins with admin API on + self.assertIn('ip', search_opts) + self.assertIn('unknown_option', search_opts) + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" + req = fakes.HTTPRequestV3.blank('/servers?%s' % query_str, + use_admin_context=True) + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_allows_ip(self): + """Test getting servers by ip.""" + + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.assertIsNotNone(search_opts) + self.assertIn('ip', search_opts) + self.assertEqual(search_opts['ip'], '10\..*') + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?ip=10\..*') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_admin_allows_ip6(self): + """Test getting servers by ip6 with admin_api enabled and + admin context + """ + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.assertIsNotNone(search_opts) + self.assertIn('ip6', search_opts) + self.assertEqual(search_opts['ip6'], 'ffff.*') + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers?ip6=ffff.*', + use_admin_context=True) + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_all_server_details(self): + expected_flavor = { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": 'http://localhost/flavors/1', + }, + ], + } + expected_image = { + "id": "10", + "links": [ + { + "rel": "bookmark", + "href": 'http://localhost/images/10', + }, + ], + } + req = fakes.HTTPRequestV3.blank('/servers/detail') + res_dict = self.controller.detail(req) + + for i, s in enumerate(res_dict['servers']): + self.assertEqual(s['id'], fakes.get_fake_uuid(i)) + self.assertEqual(s['hostId'], '') + self.assertEqual(s['name'], 'server%d' % (i + 1)) + self.assertEqual(s['image'], expected_image) + self.assertEqual(s['flavor'], expected_flavor) + self.assertEqual(s['status'], 'BUILD') + self.assertEqual(s['metadata']['seq'], str(i + 1)) + + def test_get_all_server_details_with_host(self): + """We want to make sure that if two instances are on the same host, + then they return the same hostId. If two instances are on different + hosts, they should return different hostIds. In this test, + there are 5 instances - 2 on one host and 3 on another. + """ + + def return_servers_with_host(context, *args, **kwargs): + return [fakes.stub_instance(i + 1, 'fake', 'fake', host=i % 2, + uuid=fakes.get_fake_uuid(i)) + for i in xrange(5)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + return_servers_with_host) + + req = fakes.HTTPRequestV3.blank('/servers/detail') + res_dict = self.controller.detail(req) + + server_list = res_dict['servers'] + host_ids = [server_list[0]['hostId'], server_list[1]['hostId']] + self.assertTrue(host_ids[0] and host_ids[1]) + self.assertNotEqual(host_ids[0], host_ids[1]) + + for i, s in enumerate(server_list): + self.assertEqual(s['id'], fakes.get_fake_uuid(i)) + self.assertEqual(s['hostId'], host_ids[i % 2]) + self.assertEqual(s['name'], 'server%d' % (i + 1)) + + def test_get_servers_joins_pci_devices(self): + self.expected_attrs = None + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False, + expected_attrs=None): + self.expected_attrs = expected_attrs + return [] + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequestV3.blank('/servers', use_admin_context=True) + self.assertIn('servers', self.controller.index(req)) + self.assertIn('pci_devices', self.expected_attrs) + + +class ServersControllerDeleteTest(ControllerTest): + + def setUp(self): + super(ServersControllerDeleteTest, self).setUp() + self.server_delete_called = False + + def instance_destroy_mock(*args, **kwargs): + self.server_delete_called = True + deleted_at = timeutils.utcnow() + return fake_instance.fake_db_instance(deleted_at=deleted_at) + + self.stubs.Set(db, 'instance_destroy', instance_destroy_mock) + + def _create_delete_request(self, uuid): + fakes.stub_out_instance_quota(self.stubs, 0, 10) + req = fakes.HTTPRequestV3.blank('/servers/%s' % uuid) + req.method = 'DELETE' + return req + + def _delete_server_instance(self, uuid=FAKE_UUID): + req = self._create_delete_request(uuid) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE)) + self.controller.delete(req, uuid) + + def test_delete_server_instance(self): + self._delete_server_instance() + self.assertTrue(self.server_delete_called) + + def test_delete_server_instance_not_found(self): + self.assertRaises(webob.exc.HTTPNotFound, + self._delete_server_instance, + uuid='non-existent-uuid') + + def test_delete_server_instance_while_building(self): + req = self._create_delete_request(FAKE_UUID) + self.controller.delete(req, FAKE_UUID) + + self.assertTrue(self.server_delete_called) + + def test_delete_locked_server(self): + req = self._create_delete_request(FAKE_UUID) + self.stubs.Set(compute_api.API, delete_types.SOFT_DELETE, + fakes.fake_actions_to_locked_server) + self.stubs.Set(compute_api.API, delete_types.DELETE, + fakes.fake_actions_to_locked_server) + + self.assertRaises(webob.exc.HTTPConflict, self.controller.delete, + req, FAKE_UUID) + + def test_delete_server_instance_while_resize(self): + req = self._create_delete_request(FAKE_UUID) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE, + task_state=task_states.RESIZE_PREP)) + + self.controller.delete(req, FAKE_UUID) + # Delete shoud be allowed in any case, even during resizing, + # because it may get stuck. + self.assertTrue(self.server_delete_called) + + def test_delete_server_instance_if_not_launched(self): + self.flags(reclaim_instance_interval=3600) + req = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID) + req.method = 'DELETE' + + self.server_delete_called = False + + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(launched_at=None)) + + def instance_destroy_mock(*args, **kwargs): + self.server_delete_called = True + deleted_at = timeutils.utcnow() + return fake_instance.fake_db_instance(deleted_at=deleted_at) + self.stubs.Set(db, 'instance_destroy', instance_destroy_mock) + + self.controller.delete(req, FAKE_UUID) + # delete() should be called for instance which has never been active, + # even if reclaim_instance_interval has been set. + self.assertEqual(self.server_delete_called, True) + + +class ServersControllerRebuildInstanceTest(ControllerTest): + + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + image_href = 'http://localhost/v3/fake/images/%s' % image_uuid + + def setUp(self): + super(ServersControllerRebuildInstanceTest, self).setUp() + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE)) + self.body = { + 'rebuild': { + 'name': 'new_name', + 'imageRef': self.image_href, + 'metadata': { + 'open': 'stack', + }, + }, + } + self.req = fakes.HTTPRequest.blank('/fake/servers/a/action') + self.req.method = 'POST' + self.req.headers["content-type"] = "application/json" + + def test_rebuild_instance_with_blank_metadata_key(self): + self.body['rebuild']['metadata'][''] = 'world' + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + + def test_rebuild_instance_with_metadata_key_too_long(self): + self.body['rebuild']['metadata'][('a' * 260)] = 'world' + + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + + def test_rebuild_instance_with_metadata_value_too_long(self): + self.body['rebuild']['metadata']['key1'] = ('a' * 260) + + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, self.req, + FAKE_UUID, body=self.body) + + def test_rebuild_instance_with_metadata_value_not_string(self): + self.body['rebuild']['metadata']['key1'] = 1 + + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, self.req, + FAKE_UUID, body=self.body) + + def test_rebuild_instance_fails_when_min_ram_too_small(self): + # make min_ram larger than our instance ram size + def fake_get_image(self, context, image_href, **kwargs): + return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + name='public image', is_public=True, + status='active', properties={'key1': 'value1'}, + min_ram="4096", min_disk="10") + + self.stubs.Set(fake._FakeImageService, 'show', fake_get_image) + + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + + def test_rebuild_instance_fails_when_min_disk_too_small(self): + # make min_disk larger than our instance disk size + def fake_get_image(self, context, image_href, **kwargs): + return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + name='public image', is_public=True, + status='active', properties={'key1': 'value1'}, + min_ram="128", min_disk="100000") + + self.stubs.Set(fake._FakeImageService, 'show', fake_get_image) + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, self.req, + FAKE_UUID, body=self.body) + + def test_rebuild_instance_image_too_large(self): + # make image size larger than our instance disk size + size = str(1000 * (1024 ** 3)) + + def fake_get_image(self, context, image_href, **kwargs): + return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + name='public image', is_public=True, + status='active', size=size) + + self.stubs.Set(fake._FakeImageService, 'show', fake_get_image) + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + + def test_rebuild_instance_name_all_blank(self): + def fake_get_image(self, context, image_href, **kwargs): + return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + name='public image', is_public=True, status='active') + + self.stubs.Set(fake._FakeImageService, 'show', fake_get_image) + self.body['rebuild']['name'] = ' ' + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + + def test_rebuild_instance_with_deleted_image(self): + def fake_get_image(self, context, image_href, **kwargs): + return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + name='public image', is_public=True, + status='DELETED') + + self.stubs.Set(fake._FakeImageService, 'show', fake_get_image) + + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + + def test_rebuild_instance_onset_file_limit_over_quota(self): + def fake_get_image(self, context, image_href, **kwargs): + return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + name='public image', is_public=True, status='active') + + with contextlib.nested( + mock.patch.object(fake._FakeImageService, 'show', + side_effect=fake_get_image), + mock.patch.object(self.controller.compute_api, 'rebuild', + side_effect=exception.OnsetFileLimitExceeded) + ) as ( + show_mock, rebuild_mock + ): + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPForbidden, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + + def test_start(self): + self.mox.StubOutWithMock(compute_api.API, 'start') + compute_api.API.start(mox.IgnoreArg(), mox.IgnoreArg()) + self.mox.ReplayAll() + + req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID) + body = dict(start="") + self.controller._start_server(req, FAKE_UUID, body) + + def test_start_policy_failed(self): + rules = { + "compute:v3:servers:start": + common_policy.parse_rule("project_id:non_fake") + } + policy.set_rules(rules) + req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID) + body = dict(start="") + exc = self.assertRaises(exception.PolicyNotAuthorized, + self.controller._start_server, + req, FAKE_UUID, body) + self.assertIn("compute:v3:servers:start", exc.format_message()) + + def test_start_not_ready(self): + self.stubs.Set(compute_api.API, 'start', fake_start_stop_not_ready) + req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID) + body = dict(start="") + self.assertRaises(webob.exc.HTTPConflict, + self.controller._start_server, req, FAKE_UUID, body) + + def test_start_locked_server(self): + self.stubs.Set(compute_api.API, 'start', + fakes.fake_actions_to_locked_server) + req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID) + body = dict(start="") + self.assertRaises(webob.exc.HTTPConflict, + self.controller._start_server, req, FAKE_UUID, body) + + def test_start_invalid(self): + self.stubs.Set(compute_api.API, 'start', fake_start_stop_invalid_state) + req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID) + body = dict(start="") + self.assertRaises(webob.exc.HTTPConflict, + self.controller._start_server, req, FAKE_UUID, body) + + def test_stop(self): + self.mox.StubOutWithMock(compute_api.API, 'stop') + compute_api.API.stop(mox.IgnoreArg(), mox.IgnoreArg()) + self.mox.ReplayAll() + + req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID) + body = dict(stop="") + self.controller._stop_server(req, FAKE_UUID, body) + + def test_stop_policy_failed(self): + rules = { + "compute:v3:servers:stop": + common_policy.parse_rule("project_id:non_fake") + } + policy.set_rules(rules) + req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID) + body = dict(stop='') + exc = self.assertRaises(exception.PolicyNotAuthorized, + self.controller._stop_server, + req, FAKE_UUID, body) + self.assertIn("compute:v3:servers:stop", exc.format_message()) + + def test_stop_not_ready(self): + self.stubs.Set(compute_api.API, 'stop', fake_start_stop_not_ready) + req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID) + body = dict(stop="") + self.assertRaises(webob.exc.HTTPConflict, + self.controller._stop_server, req, FAKE_UUID, body) + + def test_stop_locked_server(self): + self.stubs.Set(compute_api.API, 'stop', + fakes.fake_actions_to_locked_server) + req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID) + body = dict(stop="") + self.assertRaises(webob.exc.HTTPConflict, + self.controller._stop_server, req, FAKE_UUID, body) + + def test_stop_invalid_state(self): + self.stubs.Set(compute_api.API, 'stop', fake_start_stop_invalid_state) + req = fakes.HTTPRequestV3.blank('/servers/%s/action' % FAKE_UUID) + body = dict(start="") + self.assertRaises(webob.exc.HTTPConflict, + self.controller._stop_server, req, FAKE_UUID, body) + + def test_start_with_bogus_id(self): + self.stubs.Set(db, 'instance_get_by_uuid', + fake_instance_get_by_uuid_not_found) + req = fakes.HTTPRequestV3.blank('/servers/test_inst/action') + body = dict(start="") + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._start_server, req, 'test_inst', body) + + def test_stop_with_bogus_id(self): + self.stubs.Set(db, 'instance_get_by_uuid', + fake_instance_get_by_uuid_not_found) + req = fakes.HTTPRequestV3.blank('/servers/test_inst/action') + body = dict(stop="") + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._stop_server, req, 'test_inst', body) + + +class ServersControllerUpdateTest(ControllerTest): + + def _get_request(self, body=None, options=None): + if options: + self.stubs.Set(db, 'instance_get', + fakes.fake_instance_get(**options)) + req = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID) + req.method = 'PUT' + req.content_type = 'application/json' + req.body = jsonutils.dumps(body) + return req + + def test_update_server_all_attributes(self): + body = {'server': { + 'name': 'server_test', + }} + req = self._get_request(body, {'name': 'server_test'}) + res_dict = self.controller.update(req, FAKE_UUID, body=body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['name'], 'server_test') + + def test_update_server_name(self): + body = {'server': {'name': 'server_test'}} + req = self._get_request(body, {'name': 'server_test'}) + res_dict = self.controller.update(req, FAKE_UUID, body=body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['name'], 'server_test') + + def test_update_server_name_too_long(self): + body = {'server': {'name': 'x' * 256}} + req = self._get_request(body, {'name': 'server_test'}) + self.assertRaises(exception.ValidationError, self.controller.update, + req, FAKE_UUID, body=body) + + def test_update_server_name_all_blank_spaces(self): + self.stubs.Set(db, 'instance_get', + fakes.fake_instance_get(name='server_test')) + req = fakes.HTTPRequest.blank('/v3/servers/%s' % FAKE_UUID) + req.method = 'PUT' + req.content_type = 'application/json' + body = {'server': {'name': ' ' * 64}} + req.body = jsonutils.dumps(body) + self.assertRaises(exception.ValidationError, self.controller.update, + req, FAKE_UUID, body=body) + + def test_update_server_admin_password_ignored(self): + inst_dict = dict(name='server_test', admin_password='bacon') + body = dict(server=inst_dict) + + def server_update(context, id, params): + filtered_dict = { + 'display_name': 'server_test', + } + self.assertEqual(params, filtered_dict) + filtered_dict['uuid'] = id + return filtered_dict + + self.stubs.Set(db, 'instance_update', server_update) + # FIXME (comstud) + # self.stubs.Set(db, 'instance_get', + # return_server_with_attributes(name='server_test')) + + req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID) + req.method = 'PUT' + req.content_type = "application/json" + req.body = jsonutils.dumps(body) + res_dict = self.controller.update(req, FAKE_UUID, body=body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['name'], 'server_test') + + def test_update_server_not_found(self): + def fake_get(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(compute_api.API, 'get', fake_get) + body = {'server': {'name': 'server_test'}} + req = self._get_request(body) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.update, + req, FAKE_UUID, body=body) + + def test_update_server_not_found_on_update(self): + def fake_update(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(db, 'instance_update_and_get_original', fake_update) + body = {'server': {'name': 'server_test'}} + req = self._get_request(body) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.update, + req, FAKE_UUID, body=body) + + def test_update_server_policy_fail(self): + rule = {'compute:update': common_policy.parse_rule('role:admin')} + policy.set_rules(rule) + body = {'server': {'name': 'server_test'}} + req = self._get_request(body, {'name': 'server_test'}) + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.update, req, FAKE_UUID, body=body) + + +class ServerStatusTest(test.TestCase): + + def setUp(self): + super(ServerStatusTest, self).setUp() + fakes.stub_out_nw_api(self.stubs) + + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers.ServersController(extension_info=ext_info) + + def _get_with_state(self, vm_state, task_state=None): + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_state, + task_state=task_state)) + + request = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID) + return self.controller.show(request, FAKE_UUID) + + def test_active(self): + response = self._get_with_state(vm_states.ACTIVE) + self.assertEqual(response['server']['status'], 'ACTIVE') + + def test_reboot(self): + response = self._get_with_state(vm_states.ACTIVE, + task_states.REBOOTING) + self.assertEqual(response['server']['status'], 'REBOOT') + + def test_reboot_hard(self): + response = self._get_with_state(vm_states.ACTIVE, + task_states.REBOOTING_HARD) + self.assertEqual(response['server']['status'], 'HARD_REBOOT') + + def test_reboot_resize_policy_fail(self): + def fake_get_server(context, req, id): + return fakes.stub_instance(id) + + self.stubs.Set(self.controller, '_get_server', fake_get_server) + + rule = {'compute:reboot': + common_policy.parse_rule('role:admin')} + policy.set_rules(rule) + req = fakes.HTTPRequestV3.blank('/servers/1234/action') + self.assertRaises(exception.PolicyNotAuthorized, + self.controller._action_reboot, req, '1234', + {'reboot': {'type': 'HARD'}}) + + def test_rebuild(self): + response = self._get_with_state(vm_states.ACTIVE, + task_states.REBUILDING) + self.assertEqual(response['server']['status'], 'REBUILD') + + def test_rebuild_error(self): + response = self._get_with_state(vm_states.ERROR) + self.assertEqual(response['server']['status'], 'ERROR') + + def test_resize(self): + response = self._get_with_state(vm_states.ACTIVE, + task_states.RESIZE_PREP) + self.assertEqual(response['server']['status'], 'RESIZE') + + def test_confirm_resize_policy_fail(self): + def fake_get_server(context, req, id): + return fakes.stub_instance(id) + + self.stubs.Set(self.controller, '_get_server', fake_get_server) + + rule = {'compute:confirm_resize': + common_policy.parse_rule('role:admin')} + policy.set_rules(rule) + req = fakes.HTTPRequestV3.blank('/servers/1234/action') + self.assertRaises(exception.PolicyNotAuthorized, + self.controller._action_confirm_resize, req, '1234', {}) + + def test_verify_resize(self): + response = self._get_with_state(vm_states.RESIZED, None) + self.assertEqual(response['server']['status'], 'VERIFY_RESIZE') + + def test_revert_resize(self): + response = self._get_with_state(vm_states.RESIZED, + task_states.RESIZE_REVERTING) + self.assertEqual(response['server']['status'], 'REVERT_RESIZE') + + def test_revert_resize_policy_fail(self): + def fake_get_server(context, req, id): + return fakes.stub_instance(id) + + self.stubs.Set(self.controller, '_get_server', fake_get_server) + + rule = {'compute:revert_resize': + common_policy.parse_rule('role:admin')} + policy.set_rules(rule) + req = fakes.HTTPRequestV3.blank('/servers/1234/action') + self.assertRaises(exception.PolicyNotAuthorized, + self.controller._action_revert_resize, req, '1234', {}) + + def test_password_update(self): + response = self._get_with_state(vm_states.ACTIVE, + task_states.UPDATING_PASSWORD) + self.assertEqual(response['server']['status'], 'PASSWORD') + + def test_stopped(self): + response = self._get_with_state(vm_states.STOPPED) + self.assertEqual(response['server']['status'], 'SHUTOFF') + + +class ServersControllerCreateTest(test.TestCase): + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + + def setUp(self): + """Shared implementation for tests below that create instance.""" + super(ServersControllerCreateTest, self).setUp() + + self.flags(verbose=True, + enable_instance_password=True) + self.instance_cache_num = 0 + self.instance_cache_by_id = {} + self.instance_cache_by_uuid = {} + + fakes.stub_out_nw_api(self.stubs) + + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers.ServersController(extension_info=ext_info) + + def instance_create(context, inst): + inst_type = flavors.get_flavor_by_flavor_id(3) + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + def_image_ref = 'http://localhost/images/%s' % image_uuid + self.instance_cache_num += 1 + instance = fake_instance.fake_db_instance(**{ + 'id': self.instance_cache_num, + 'display_name': inst['display_name'] or 'test', + 'uuid': FAKE_UUID, + 'instance_type': inst_type, + 'image_ref': inst.get('image_ref', def_image_ref), + 'user_id': 'fake', + 'project_id': 'fake', + 'reservation_id': inst['reservation_id'], + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "config_drive": None, + "progress": 0, + "fixed_ips": [], + "task_state": "", + "vm_state": "", + "root_device_name": inst.get('root_device_name', 'vda'), + }) + + self.instance_cache_by_id[instance['id']] = instance + self.instance_cache_by_uuid[instance['uuid']] = instance + return instance + + def instance_get(context, instance_id): + """Stub for compute/api create() pulling in instance after + scheduling + """ + return self.instance_cache_by_id[instance_id] + + def instance_update(context, uuid, values): + instance = self.instance_cache_by_uuid[uuid] + instance.update(values) + return instance + + def server_update(context, instance_uuid, params, update_cells=True): + inst = self.instance_cache_by_uuid[instance_uuid] + inst.update(params) + return inst + + def server_update_and_get_original( + context, instance_uuid, params, update_cells=False, + columns_to_join=None): + inst = self.instance_cache_by_uuid[instance_uuid] + inst.update(params) + return (inst, inst) + + def fake_method(*args, **kwargs): + pass + + def project_get_networks(context, user_id): + return dict(id='1', host='localhost') + + def queue_get_for(context, *args): + return 'network_topic' + + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + fake.stub_out_image_service(self.stubs) + self.stubs.Set(uuid, 'uuid4', fake_gen_uuid) + self.stubs.Set(db, 'project_get_networks', + project_get_networks) + self.stubs.Set(db, 'instance_create', instance_create) + self.stubs.Set(db, 'instance_system_metadata_update', + fake_method) + self.stubs.Set(db, 'instance_get', instance_get) + self.stubs.Set(db, 'instance_update', instance_update) + self.stubs.Set(db, 'instance_update_and_get_original', + server_update_and_get_original) + self.stubs.Set(manager.VlanManager, 'allocate_fixed_ip', + fake_method) + self.body = { + 'server': { + 'name': 'server_test', + 'imageRef': self.image_uuid, + 'flavorRef': self.flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + }, + } + self.bdm = [{'delete_on_termination': 1, + 'device_name': 123, + 'volume_size': 1, + 'volume_id': '11111111-1111-1111-1111-111111111111'}] + + self.req = fakes.HTTPRequest.blank('/fake/servers') + self.req.method = 'POST' + self.req.headers["content-type"] = "application/json" + + def _check_admin_password_len(self, server_dict): + """utility function - check server_dict for admin_password length.""" + self.assertEqual(CONF.password_length, + len(server_dict["adminPass"])) + + def _check_admin_password_missing(self, server_dict): + """utility function - check server_dict for admin_password absence.""" + self.assertNotIn("adminPass", server_dict) + + def _test_create_instance(self, flavor=2): + image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + self.body['server']['imageRef'] = image_uuid + self.body['server']['flavorRef'] = flavor + self.req.body = jsonutils.dumps(self.body) + server = self.controller.create(self.req, body=self.body).obj['server'] + self._check_admin_password_len(server) + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_private_flavor(self): + values = { + 'name': 'fake_name', + 'memory_mb': 512, + 'vcpus': 1, + 'root_gb': 10, + 'ephemeral_gb': 10, + 'flavorid': '1324', + 'swap': 0, + 'rxtx_factor': 0.5, + 'vcpu_weight': 1, + 'disabled': False, + 'is_public': False, + } + db.flavor_create(context.get_admin_context(), values) + self.assertRaises(webob.exc.HTTPBadRequest, self._test_create_instance, + flavor=1324) + + def test_create_server_bad_image_href(self): + image_href = 1 + self.body['server']['min_count'] = 1 + self.body['server']['imageRef'] = image_href, + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, + self.controller.create, + self.req, body=self.body) + # TODO(cyeoh): bp-v3-api-unittests + # This needs to be ported to the os-networks extension tests + # def test_create_server_with_invalid_networks_parameter(self): + # self.ext_mgr.extensions = {'os-networks': 'fake'} + # image_href = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + # flavor_ref = 'http://localhost/123/flavors/3' + # body = { + # 'server': { + # 'name': 'server_test', + # 'imageRef': image_href, + # 'flavorRef': flavor_ref, + # 'networks': {'uuid': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'}, + # } + # } + # req = fakes.HTTPRequest.blank('/v2/fake/servers') + # req.method = 'POST' + # req.body = jsonutils.dumps(body) + # req.headers["content-type"] = "application/json" + # self.assertRaises(webob.exc.HTTPBadRequest, + # self.controller.create, + # req, + # body) + + def test_create_server_with_deleted_image(self): + # Get the fake image service so we can set the status to deleted + (image_service, image_id) = glance.get_remote_image_service( + context, '') + image_service.update(context, self.image_uuid, {'status': 'DELETED'}) + self.addCleanup(image_service.update, context, self.image_uuid, + {'status': 'active'}) + + self.body['server']['flavorRef'] = 2 + self.req.body = jsonutils.dumps(self.body) + with testtools.ExpectedException( + webob.exc.HTTPBadRequest, + 'Image 76fa36fc-c930-4bf3-8c8a-ea2a2420deb6 is not active.'): + self.controller.create(self.req, body=self.body) + + def test_create_server_image_too_large(self): + # Get the fake image service so we can set the status to deleted + (image_service, image_id) = glance.get_remote_image_service( + context, self.image_uuid) + + image = image_service.show(context, image_id) + + orig_size = image['size'] + new_size = str(1000 * (1024 ** 3)) + image_service.update(context, self.image_uuid, {'size': new_size}) + + self.addCleanup(image_service.update, context, self.image_uuid, + {'size': orig_size}) + + self.body['server']['flavorRef'] = 2 + self.req.body = jsonutils.dumps(self.body) + + with testtools.ExpectedException( + webob.exc.HTTPBadRequest, + "Flavor's disk is too small for requested image."): + self.controller.create(self.req, body=self.body) + + def test_create_instance_image_ref_is_bookmark(self): + image_href = 'http://localhost/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, body=self.body).obj + + server = res['server'] + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_image_ref_is_invalid(self): + image_uuid = 'this_is_not_a_valid_uuid' + image_href = 'http://localhost/images/%s' % image_uuid + flavor_ref = 'http://localhost/flavors/3' + self.body['server']['imageRef'] = image_href + self.body['server']['flavorRef'] = flavor_ref + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + self.req, body=self.body) + + def test_create_instance_no_key_pair(self): + fakes.stub_out_key_pair_funcs(self.stubs, have_key_pair=False) + self._test_create_instance() + + def _test_create_extra(self, params, no_image=False): + self.body['server']['flavorRef'] = 2 + if no_image: + self.body['server'].pop('imageRef', None) + self.body['server'].update(params) + self.req.body = jsonutils.dumps(self.body) + self.req.headers["content-type"] = "application/json" + self.controller.create(self.req, body=self.body).obj['server'] + + # TODO(cyeoh): bp-v3-api-unittests + # This needs to be ported to the os-keypairs extension tests + # def test_create_instance_with_keypairs_enabled(self): + # self.ext_mgr.extensions = {'os-keypairs': 'fake'} + # key_name = 'green' + # + # params = {'key_name': key_name} + # old_create = compute_api.API.create + # + # # NOTE(sdague): key pair goes back to the database, + # # so we need to stub it out for tests + # def key_pair_get(context, user_id, name): + # return {'public_key': 'FAKE_KEY', + # 'fingerprint': 'FAKE_FINGERPRINT', + # 'name': name} + # + # def create(*args, **kwargs): + # self.assertEqual(kwargs['key_name'], key_name) + # return old_create(*args, **kwargs) + # + # self.stubs.Set(db, 'key_pair_get', key_pair_get) + # self.stubs.Set(compute_api.API, 'create', create) + # self._test_create_extra(params) + # + # TODO(cyeoh): bp-v3-api-unittests + # This needs to be ported to the os-networks extension tests + # def test_create_instance_with_networks_enabled(self): + # self.ext_mgr.extensions = {'os-networks': 'fake'} + # net_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + # requested_networks = [{'uuid': net_uuid}] + # params = {'networks': requested_networks} + # old_create = compute_api.API.create + + # def create(*args, **kwargs): + # result = [('76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', None)] + # self.assertEqual(kwargs['requested_networks'], result) + # return old_create(*args, **kwargs) + + # self.stubs.Set(compute_api.API, 'create', create) + # self._test_create_extra(params) + + def test_create_instance_with_port_with_no_fixed_ips(self): + port_id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'port': port_id}] + params = {'networks': requested_networks} + + def fake_create(*args, **kwargs): + raise exception.PortRequiresFixedIP(port_id=port_id) + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + @mock.patch.object(compute_api.API, 'create') + def test_create_instance_raise_user_data_too_large(self, mock_create): + mock_create.side_effect = exception.InstanceUserDataTooLarge( + maxsize=1, length=2) + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + self.req, body=self.body) + + def test_create_instance_with_network_with_no_subnet(self): + network = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network}] + params = {'networks': requested_networks} + + def fake_create(*args, **kwargs): + raise exception.NetworkRequiresSubnet(network_uuid=network) + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + def test_create_instance_with_non_unique_secgroup_name(self): + network = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network}] + params = {'networks': requested_networks, + 'security_groups': [{'name': 'dup'}, {'name': 'dup'}]} + + def fake_create(*args, **kwargs): + raise exception.NoUniqueMatch("No Unique match found for ...") + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPConflict, + self._test_create_extra, params) + + def test_create_instance_with_networks_disabled_neutronv2(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + net_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + requested_networks = [{'uuid': net_uuid}] + params = {'networks': requested_networks} + old_create = compute_api.API.create + + def create(*args, **kwargs): + result = [('76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', None, + None, None)] + self.assertEqual(result, kwargs['requested_networks'].as_tuples()) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_networks_disabled(self): + net_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + requested_networks = [{'uuid': net_uuid}] + params = {'networks': requested_networks} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertIsNone(kwargs['requested_networks']) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_pass_disabled(self): + # test with admin passwords disabled See lp bug 921814 + self.flags(enable_instance_password=False) + + # proper local hrefs must start with 'http://localhost/v3/' + self.flags(enable_instance_password=False) + image_href = 'http://localhost/v2/fake/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, body=self.body).obj + + server = res['server'] + self._check_admin_password_missing(server) + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_name_too_long(self): + # proper local hrefs must start with 'http://localhost/v3/' + image_href = 'http://localhost/v2/images/%s' % self.image_uuid + self.body['server']['name'] = 'X' * 256 + self.body['server']['imageRef'] = image_href + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, self.controller.create, + self.req, body=self.body) + + def test_create_instance_name_all_blank_spaces(self): + # proper local hrefs must start with 'http://localhost/v2/' + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + image_href = 'http://localhost/v3/images/%s' % image_uuid + flavor_ref = 'http://localhost/flavors/3' + body = { + 'server': { + 'name': ' ' * 64, + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + }, + } + + req = fakes.HTTPRequest.blank('/v3/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + self.assertRaises(exception.ValidationError, + self.controller.create, req, body=body) + + def test_create_instance(self): + # proper local hrefs must start with 'http://localhost/v3/' + image_href = 'http://localhost/v2/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, body=self.body).obj + + server = res['server'] + self._check_admin_password_len(server) + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_extension_create_exception(self): + def fake_keypair_server_create(self, server_dict, + create_kwargs): + raise KeyError + + self.stubs.Set(keypairs.Keypairs, 'server_create', + fake_keypair_server_create) + # proper local hrefs must start with 'http://localhost/v3/' + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + image_href = 'http://localhost/v3/images/%s' % image_uuid + flavor_ref = 'http://localhost/123/flavors/3' + body = { + 'server': { + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + }, + } + + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + self.assertRaises(webob.exc.HTTPInternalServerError, + self.controller.create, req, body=body) + + def test_create_instance_pass_disabled(self): + self.flags(enable_instance_password=False) + # proper local hrefs must start with 'http://localhost/v3/' + image_href = 'http://localhost/v2/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, body=self.body).obj + + server = res['server'] + self._check_admin_password_missing(server) + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_too_much_metadata(self): + self.flags(quota_metadata_items=1) + image_href = 'http://localhost/v2/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.body['server']['metadata']['vote'] = 'fiddletown' + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.create, self.req, body=self.body) + + def test_create_instance_metadata_key_too_long(self): + self.flags(quota_metadata_items=1) + image_href = 'http://localhost/v2/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.body['server']['metadata'] = {('a' * 260): '12345'} + + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, + self.controller.create, self.req, body=self.body) + + def test_create_instance_metadata_value_too_long(self): + self.flags(quota_metadata_items=1) + image_href = 'http://localhost/v2/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.body['server']['metadata'] = {'key1': ('a' * 260)} + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, + self.controller.create, self.req, body=self.body) + + def test_create_instance_metadata_key_blank(self): + self.flags(quota_metadata_items=1) + image_href = 'http://localhost/v2/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.body['server']['metadata'] = {'': 'abcd'} + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, + self.controller.create, self.req, body=self.body) + + def test_create_instance_metadata_not_dict(self): + self.flags(quota_metadata_items=1) + image_href = 'http://localhost/v2/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.body['server']['metadata'] = 'string' + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, + self.controller.create, self.req, body=self.body) + + def test_create_instance_metadata_key_not_string(self): + self.flags(quota_metadata_items=1) + image_href = 'http://localhost/v2/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.body['server']['metadata'] = {1: 'test'} + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, + self.controller.create, self.req, body=self.body) + + def test_create_instance_metadata_value_not_string(self): + self.flags(quota_metadata_items=1) + image_href = 'http://localhost/v2/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.body['server']['metadata'] = {'test': ['a', 'list']} + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(exception.ValidationError, + self.controller.create, self.req, body=self.body) + + def test_create_user_data_malformed_bad_request(self): + params = {'user_data': 'u1234'} + self.assertRaises(exception.ValidationError, + self._test_create_extra, params) + + def test_create_instance_invalid_key_name(self): + image_href = 'http://localhost/v2/images/2' + self.body['server']['imageRef'] = image_href + self.body['server']['key_name'] = 'nonexistentkey' + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, body=self.body) + + def test_create_instance_valid_key_name(self): + self.body['server']['key_name'] = 'key' + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, body=self.body).obj + + self.assertEqual(FAKE_UUID, res["server"]["id"]) + self._check_admin_password_len(res["server"]) + + def test_create_instance_invalid_flavor_href(self): + image_href = 'http://localhost/v2/images/2' + flavor_ref = 'http://localhost/v2/flavors/asdf' + self.body['server']['imageRef'] = image_href + self.body['server']['flavorRef'] = flavor_ref + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, body=self.body) + + def test_create_instance_invalid_flavor_id_int(self): + image_href = 'http://localhost/v2/images/2' + flavor_ref = -1 + self.body['server']['imageRef'] = image_href + self.body['server']['flavorRef'] = flavor_ref + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, body=self.body) + + def test_create_instance_bad_flavor_href(self): + image_href = 'http://localhost/v2/images/2' + flavor_ref = 'http://localhost/v2/flavors/17' + self.body['server']['imageRef'] = image_href + self.body['server']['flavorRef'] = flavor_ref + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, body=self.body) + + def test_create_instance_bad_href(self): + image_href = 'asdf' + self.body['server']['imageRef'] = image_href + self.req.body = jsonutils.dumps(self.body) + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, body=self.body) + + def test_create_instance_local_href(self): + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, body=self.body).obj + + server = res['server'] + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_admin_password(self): + self.body['server']['flavorRef'] = 3 + self.body['server']['adminPass'] = 'testpass' + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, body=self.body).obj + + server = res['server'] + self.assertEqual(server['adminPass'], + self.body['server']['adminPass']) + + def test_create_instance_admin_password_pass_disabled(self): + self.flags(enable_instance_password=False) + self.body['server']['flavorRef'] = 3 + self.body['server']['adminPass'] = 'testpass' + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, body=self.body).obj + + self.assertIn('server', res) + self.assertIn('adminPass', self.body['server']) + + def test_create_instance_admin_password_empty(self): + self.body['server']['flavorRef'] = 3 + self.body['server']['adminPass'] = '' + self.req.body = jsonutils.dumps(self.body) + + # The fact that the action doesn't raise is enough validation + self.controller.create(self.req, body=self.body) + + def test_create_location(self): + selfhref = 'http://localhost/v2/fake/servers/%s' % FAKE_UUID + self.req.body = jsonutils.dumps(self.body) + robj = self.controller.create(self.req, body=self.body) + + self.assertEqual(robj['Location'], selfhref) + + def _do_test_create_instance_above_quota(self, resource, allowed, quota, + expected_msg): + fakes.stub_out_instance_quota(self.stubs, allowed, quota, resource) + self.body['server']['flavorRef'] = 3 + self.req.body = jsonutils.dumps(self.body) + try: + self.controller.create(self.req, body=self.body).obj['server'] + self.fail('expected quota to be exceeded') + except webob.exc.HTTPForbidden as e: + self.assertEqual(e.explanation, expected_msg) + + def test_create_instance_above_quota_instances(self): + msg = _('Quota exceeded for instances: Requested 1, but' + ' already used 10 of 10 instances') + self._do_test_create_instance_above_quota('instances', 0, 10, msg) + + def test_create_instance_above_quota_ram(self): + msg = _('Quota exceeded for ram: Requested 4096, but' + ' already used 8192 of 10240 ram') + self._do_test_create_instance_above_quota('ram', 2048, 10 * 1024, msg) + + def test_create_instance_above_quota_cores(self): + msg = _('Quota exceeded for cores: Requested 2, but' + ' already used 9 of 10 cores') + self._do_test_create_instance_above_quota('cores', 1, 10, msg) + + def test_create_instance_above_quota_server_group_members(self): + ctxt = context.get_admin_context() + fake_group = objects.InstanceGroup(ctxt) + fake_group.create() + + def fake_count(context, name, group, user_id): + self.assertEqual(name, "server_group_members") + self.assertEqual(group.uuid, fake_group.uuid) + self.assertEqual(user_id, + self.req.environ['nova.context'].user_id) + return 10 + + def fake_limit_check(context, **kwargs): + if 'server_group_members' in kwargs: + raise exception.OverQuota(overs={}) + + def fake_instance_destroy(context, uuid, constraint): + return fakes.stub_instance(1) + + self.stubs.Set(fakes.QUOTAS, 'count', fake_count) + self.stubs.Set(fakes.QUOTAS, 'limit_check', fake_limit_check) + self.stubs.Set(db, 'instance_destroy', fake_instance_destroy) + self.body['os:scheduler_hints'] = {'group': fake_group.uuid} + self.req.body = jsonutils.dumps(self.body) + expected_msg = "Quota exceeded, too many servers in group" + + try: + self.controller.create(self.req, body=self.body).obj + self.fail('expected quota to be exceeded') + except webob.exc.HTTPForbidden as e: + self.assertEqual(e.explanation, expected_msg) + + def test_create_instance_above_quota_server_groups(self): + + def fake_reserve(contex, **deltas): + if 'server_groups' in deltas: + raise exception.OverQuota(overs={}) + + def fake_instance_destroy(context, uuid, constraint): + return fakes.stub_instance(1) + + self.stubs.Set(fakes.QUOTAS, 'reserve', fake_reserve) + self.stubs.Set(db, 'instance_destroy', fake_instance_destroy) + self.body['os:scheduler_hints'] = {'group': 'fake_group'} + self.req.body = jsonutils.dumps(self.body) + + expected_msg = "Quota exceeded, too many server groups." + + try: + self.controller.create(self.req, body=self.body).obj + self.fail('expected quota to be exceeded') + except webob.exc.HTTPForbidden as e: + self.assertEqual(e.explanation, expected_msg) + + def test_create_instance_with_neutronv2_port_in_use(self): + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network, 'port': port}] + params = {'networks': requested_networks} + + def fake_create(*args, **kwargs): + raise exception.PortInUse(port_id=port) + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPConflict, + self._test_create_extra, params) + + @mock.patch.object(compute_api.API, 'create') + def test_create_instance_public_network_non_admin(self, mock_create): + public_network_uuid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + params = {'networks': [{'uuid': public_network_uuid}]} + self.req.body = jsonutils.dumps(self.body) + mock_create.side_effect = exception.ExternalNetworkAttachForbidden( + network_uuid=public_network_uuid) + self.assertRaises(webob.exc.HTTPForbidden, + self._test_create_extra, params) + + @mock.patch.object(compute_api.API, 'create') + def test_create_multiple_instance_with_specified_ip_neutronv2(self, + _api_mock): + _api_mock.side_effect = exception.InvalidFixedIpAndMaxCountRequest( + reason="") + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + address = '10.0.0.1' + requested_networks = [{'uuid': network, 'fixed_ip': address, + 'port': port}] + params = {'networks': requested_networks} + self.body['server']['max_count'] = 2 + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + def test_create_multiple_instance_with_neutronv2_port(self): + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network, 'port': port}] + params = {'networks': requested_networks} + self.body['server']['max_count'] = 2 + + def fake_create(*args, **kwargs): + msg = _("Unable to launch multiple instances with" + " a single configured port ID. Please launch your" + " instance one by one with different ports.") + raise exception.MultiplePortsNotApplicable(reason=msg) + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + def test_create_instance_with_neturonv2_not_found_network(self): + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + requested_networks = [{'uuid': network}] + params = {'networks': requested_networks} + + def fake_create(*args, **kwargs): + raise exception.NetworkNotFound(network_id=network) + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + def test_create_instance_with_neutronv2_port_not_found(self): + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network, 'port': port}] + params = {'networks': requested_networks} + + def fake_create(*args, **kwargs): + raise exception.PortNotFound(port_id=port) + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + @mock.patch.object(compute_api.API, 'create') + def test_create_instance_with_network_ambiguous(self, mock_create): + mock_create.side_effect = exception.NetworkAmbiguous() + self.assertRaises(webob.exc.HTTPConflict, + self._test_create_extra, {}) + + @mock.patch.object(compute_api.API, 'create', + side_effect=exception.InstanceExists( + name='instance-name')) + def test_create_instance_raise_instance_exists(self, mock_create): + self.assertRaises(webob.exc.HTTPConflict, + self.controller.create, + self.req, body=self.body) + + +class ServersControllerCreateTestWithMock(test.TestCase): + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + + def setUp(self): + """Shared implementation for tests below that create instance.""" + super(ServersControllerCreateTestWithMock, self).setUp() + + self.flags(verbose=True, + enable_instance_password=True) + self.instance_cache_num = 0 + self.instance_cache_by_id = {} + self.instance_cache_by_uuid = {} + + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers.ServersController(extension_info=ext_info) + + self.body = { + 'server': { + 'name': 'server_test', + 'imageRef': self.image_uuid, + 'flavorRef': self.flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + }, + } + self.req = fakes.HTTPRequest.blank('/fake/servers') + self.req.method = 'POST' + self.req.headers["content-type"] = "application/json" + + def _test_create_extra(self, params, no_image=False): + self.body['server']['flavorRef'] = 2 + if no_image: + self.body['server'].pop('imageRef', None) + self.body['server'].update(params) + self.req.body = jsonutils.dumps(self.body) + self.req.headers["content-type"] = "application/json" + self.controller.create(self.req, body=self.body).obj['server'] + + @mock.patch.object(compute_api.API, 'create') + def test_create_instance_with_neutronv2_fixed_ip_already_in_use(self, + create_mock): + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + address = '10.0.2.3' + requested_networks = [{'uuid': network, 'fixed_ip': address}] + params = {'networks': requested_networks} + create_mock.side_effect = exception.FixedIpAlreadyInUse( + address=address, + instance_uuid=network) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + self.assertEqual(1, len(create_mock.call_args_list)) + + @mock.patch.object(compute_api.API, 'create', + side_effect=exception.InvalidVolume(reason='error')) + def test_create_instance_with_invalid_volume_error(self, create_mock): + # Tests that InvalidVolume is translated to a 400 error. + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, {}) + + +class ServersViewBuilderTest(test.TestCase): + + def setUp(self): + super(ServersViewBuilderTest, self).setUp() + CONF.set_override('host', 'localhost', group='glance') + self.flags(use_ipv6=True) + db_inst = fakes.stub_instance( + id=1, + image_ref="5", + uuid="deadbeef-feed-edee-beef-d0ea7beefedd", + display_name="test_server", + include_fake_metadata=False) + + privates = ['172.19.0.1'] + publics = ['192.168.0.3'] + public6s = ['b33f::fdee:ddff:fecc:bbaa'] + + def nw_info(*args, **kwargs): + return [(None, {'label': 'public', + 'ips': [dict(ip=ip) for ip in publics], + 'ip6s': [dict(ip=ip) for ip in public6s]}), + (None, {'label': 'private', + 'ips': [dict(ip=ip) for ip in privates]})] + + def floaters(*args, **kwargs): + return [] + + fakes.stub_out_nw_api_get_instance_nw_info(self.stubs, nw_info) + fakes.stub_out_nw_api_get_floating_ips_by_fixed_address(self.stubs, + floaters) + + self.uuid = db_inst['uuid'] + self.view_builder = views.servers.ViewBuilderV3() + self.request = fakes.HTTPRequestV3.blank("") + self.request.context = context.RequestContext('fake', 'fake') + self.instance = fake_instance.fake_instance_obj( + self.request.context, + expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS, + **db_inst) + + def test_get_flavor_valid_instance_type(self): + flavor_bookmark = "http://localhost/flavors/1" + expected = {"id": "1", + "links": [{"rel": "bookmark", + "href": flavor_bookmark}]} + result = self.view_builder._get_flavor(self.request, self.instance) + self.assertEqual(result, expected) + + def test_build_server(self): + self_link = "http://localhost/v3/servers/%s" % self.uuid + bookmark_link = "http://localhost/servers/%s" % self.uuid + expected_server = { + "server": { + "id": self.uuid, + "name": "test_server", + "links": [ + { + "rel": "self", + "href": self_link, + }, + { + "rel": "bookmark", + "href": bookmark_link, + }, + ], + } + } + + output = self.view_builder.basic(self.request, self.instance) + self.assertThat(output, matchers.DictMatches(expected_server)) + + def test_build_server_with_project_id(self): + expected_server = { + "server": { + "id": self.uuid, + "name": "test_server", + "links": [ + { + "rel": "self", + "href": "http://localhost/v3/servers/%s" % + self.uuid, + }, + { + "rel": "bookmark", + "href": "http://localhost/servers/%s" % self.uuid, + }, + ], + } + } + + output = self.view_builder.basic(self.request, self.instance) + self.assertThat(output, matchers.DictMatches(expected_server)) + + def test_build_server_detail(self): + image_bookmark = "http://localhost/images/5" + flavor_bookmark = "http://localhost/flavors/1" + self_link = "http://localhost/v3/servers/%s" % self.uuid + bookmark_link = "http://localhost/servers/%s" % self.uuid + expected_server = { + "server": { + "id": self.uuid, + "user_id": "fake_user", + "tenant_id": "fake_project", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 0, + "name": "test_server", + "status": "BUILD", + "hostId": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'test1': [ + {'version': 4, 'addr': '192.168.1.100', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}, + {'version': 6, 'addr': '2001:db8:0:1::1', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'} + ] + }, + "metadata": {}, + "links": [ + { + "rel": "self", + "href": self_link, + }, + { + "rel": "bookmark", + "href": bookmark_link, + }, + ], + } + } + + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output, matchers.DictMatches(expected_server)) + + def test_build_server_detail_with_fault(self): + self.instance['vm_state'] = vm_states.ERROR + self.instance['fault'] = fake_instance.fake_fault_obj( + self.request.context, self.uuid) + + image_bookmark = "http://localhost/images/5" + flavor_bookmark = "http://localhost/flavors/1" + self_link = "http://localhost/v3/servers/%s" % self.uuid + bookmark_link = "http://localhost/servers/%s" % self.uuid + expected_server = { + "server": { + "id": self.uuid, + "user_id": "fake_user", + "tenant_id": "fake_project", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "name": "test_server", + "status": "ERROR", + "hostId": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'test1': [ + {'version': 4, 'addr': '192.168.1.100', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}, + {'version': 6, 'addr': '2001:db8:0:1::1', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'} + ] + }, + "metadata": {}, + "links": [ + { + "rel": "self", + "href": self_link, + }, + { + "rel": "bookmark", + "href": bookmark_link, + }, + ], + "fault": { + "code": 404, + "created": "2010-10-10T12:00:00Z", + "message": "HTTPNotFound", + "details": "Stock details for test", + }, + } + } + + self.request.context = context.RequestContext('fake', 'fake') + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output, matchers.DictMatches(expected_server)) + + def test_build_server_detail_with_fault_that_has_been_deleted(self): + self.instance['deleted'] = 1 + self.instance['vm_state'] = vm_states.ERROR + fault = fake_instance.fake_fault_obj(self.request.context, + self.uuid, code=500, + message="No valid host was found") + self.instance['fault'] = fault + + expected_fault = {"code": 500, + "created": "2010-10-10T12:00:00Z", + "message": "No valid host was found"} + + self.request.context = context.RequestContext('fake', 'fake') + output = self.view_builder.show(self.request, self.instance) + # Regardless of vm_state deleted servers sholud be DELETED + self.assertEqual("DELETED", output['server']['status']) + self.assertThat(output['server']['fault'], + matchers.DictMatches(expected_fault)) + + def test_build_server_detail_with_fault_no_details_not_admin(self): + self.instance['vm_state'] = vm_states.ERROR + self.instance['fault'] = fake_instance.fake_fault_obj( + self.request.context, + self.uuid, + code=500, + message='Error') + + expected_fault = {"code": 500, + "created": "2010-10-10T12:00:00Z", + "message": "Error"} + + self.request.context = context.RequestContext('fake', 'fake') + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output['server']['fault'], + matchers.DictMatches(expected_fault)) + + def test_build_server_detail_with_fault_admin(self): + self.instance['vm_state'] = vm_states.ERROR + self.instance['fault'] = fake_instance.fake_fault_obj( + self.request.context, + self.uuid, + code=500, + message='Error') + + expected_fault = {"code": 500, + "created": "2010-10-10T12:00:00Z", + "message": "Error", + 'details': 'Stock details for test'} + + self.request.environ['nova.context'].is_admin = True + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output['server']['fault'], + matchers.DictMatches(expected_fault)) + + def test_build_server_detail_with_fault_no_details_admin(self): + self.instance['vm_state'] = vm_states.ERROR + self.instance['fault'] = fake_instance.fake_fault_obj( + self.request.context, + self.uuid, + code=500, + message='Error', + details='') + + expected_fault = {"code": 500, + "created": "2010-10-10T12:00:00Z", + "message": "Error"} + + self.request.environ['nova.context'].is_admin = True + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output['server']['fault'], + matchers.DictMatches(expected_fault)) + + def test_build_server_detail_with_fault_but_active(self): + self.instance['vm_state'] = vm_states.ACTIVE + self.instance['progress'] = 100 + self.instance['fault'] = fake_instance.fake_fault_obj( + self.request.context, self.uuid) + + output = self.view_builder.show(self.request, self.instance) + self.assertNotIn('fault', output['server']) + + def test_build_server_detail_active_status(self): + # set the power state of the instance to running + self.instance['vm_state'] = vm_states.ACTIVE + self.instance['progress'] = 100 + image_bookmark = "http://localhost/images/5" + flavor_bookmark = "http://localhost/flavors/1" + self_link = "http://localhost/v3/servers/%s" % self.uuid + bookmark_link = "http://localhost/servers/%s" % self.uuid + expected_server = { + "server": { + "id": self.uuid, + "user_id": "fake_user", + "tenant_id": "fake_project", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 100, + "name": "test_server", + "status": "ACTIVE", + "hostId": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'test1': [ + {'version': 4, 'addr': '192.168.1.100', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}, + {'version': 6, 'addr': '2001:db8:0:1::1', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'} + ] + }, + "metadata": {}, + "links": [ + { + "rel": "self", + "href": self_link, + }, + { + "rel": "bookmark", + "href": bookmark_link, + }, + ], + } + } + + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output, matchers.DictMatches(expected_server)) + + def test_build_server_detail_with_metadata(self): + + metadata = [] + metadata.append(models.InstanceMetadata(key="Open", value="Stack")) + metadata = nova_utils.metadata_to_dict(metadata) + self.instance['metadata'] = metadata + + image_bookmark = "http://localhost/images/5" + flavor_bookmark = "http://localhost/flavors/1" + self_link = "http://localhost/v3/servers/%s" % self.uuid + bookmark_link = "http://localhost/servers/%s" % self.uuid + expected_server = { + "server": { + "id": self.uuid, + "user_id": "fake_user", + "tenant_id": "fake_project", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 0, + "name": "test_server", + "status": "BUILD", + "hostId": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'test1': [ + {'version': 4, 'addr': '192.168.1.100', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}, + {'version': 6, 'addr': '2001:db8:0:1::1', + 'OS-EXT-IPS:type': 'fixed', + 'OS-EXT-IPS-MAC:mac_addr': 'aa:aa:aa:aa:aa:aa'}, + ] + }, + "metadata": {"Open": "Stack"}, + "links": [ + { + "rel": "self", + "href": self_link, + }, + { + "rel": "bookmark", + "href": bookmark_link, + }, + ], + } + } + + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output, matchers.DictMatches(expected_server)) + + +class ServersAllExtensionsTestCase(test.TestCase): + """Servers tests using default API router with all extensions enabled. + + The intent here is to catch cases where extensions end up throwing + an exception because of a malformed request before the core API + gets a chance to validate the request and return a 422 response. + + For example, AccessIPsController extends servers.Controller:: + + | @wsgi.extends + | def create(self, req, resp_obj, body): + | context = req.environ['nova.context'] + | if authorize(context) and 'server' in resp_obj.obj: + | resp_obj.attach(xml=AccessIPTemplate()) + | server = resp_obj.obj['server'] + | self._extend_server(req, server) + + we want to ensure that the extension isn't barfing on an invalid + body. + """ + + def setUp(self): + super(ServersAllExtensionsTestCase, self).setUp() + self.app = compute.APIRouterV3() + + def test_create_missing_server(self): + # Test create with malformed body. + + def fake_create(*args, **kwargs): + raise test.TestingException("Should not reach the compute API.") + + self.stubs.Set(compute_api.API, 'create', fake_create) + + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.content_type = 'application/json' + body = {'foo': {'a': 'b'}} + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(400, res.status_int) + + def test_update_missing_server(self): + # Test update with malformed body. + + def fake_update(*args, **kwargs): + raise test.TestingException("Should not reach the compute API.") + + self.stubs.Set(compute_api.API, 'update', fake_update) + + req = fakes.HTTPRequestV3.blank('/servers/1') + req.method = 'PUT' + req.content_type = 'application/json' + body = {'foo': {'a': 'b'}} + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(400, res.status_int) + + +class ServersInvalidRequestTestCase(test.TestCase): + """Tests of places we throw 400 Bad Request from.""" + + def setUp(self): + super(ServersInvalidRequestTestCase, self).setUp() + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers.ServersController(extension_info=ext_info) + + def _invalid_server_create(self, body): + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + + self.assertRaises(exception.ValidationError, + self.controller.create, req, body=body) + + def test_create_server_no_body(self): + self._invalid_server_create(body=None) + + def test_create_server_missing_server(self): + body = {'foo': {'a': 'b'}} + self._invalid_server_create(body=body) + + def test_create_server_malformed_entity(self): + body = {'server': 'string'} + self._invalid_server_create(body=body) + + def _unprocessable_server_update(self, body): + req = fakes.HTTPRequestV3.blank('/servers/%s' % FAKE_UUID) + req.method = 'PUT' + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, FAKE_UUID, body=body) + + def test_update_server_no_body(self): + self._invalid_server_create(body=None) + + def test_update_server_missing_server(self): + body = {'foo': {'a': 'b'}} + self._invalid_server_create(body=body) + + def test_create_update_malformed_entity(self): + body = {'server': 'string'} + self._invalid_server_create(body=body) + + +class FakeExt(extensions.V3APIExtensionBase): + name = "DiskConfig" + alias = 'os-disk-config' + version = 1 + + def fake_extension_point(self, *args, **kwargs): + pass + + def get_controller_extensions(self): + return [] + + def get_resources(self): + return [] + + +class TestServersExtensionPoint(test.NoDBTestCase): + def setUp(self): + super(TestServersExtensionPoint, self).setUp() + CONF.set_override('extensions_whitelist', ['os-disk-config'], + 'osapi_v3') + self.stubs.Set(disk_config, 'DiskConfig', FakeExt) + + def _test_load_extension_point(self, name): + setattr(FakeExt, 'server_%s' % name, + FakeExt.fake_extension_point) + ext_info = plugins.LoadedExtensionInfo() + controller = servers.ServersController(extension_info=ext_info) + self.assertEqual( + 'os-disk-config', + list(getattr(controller, + '%s_extension_manager' % name))[0].obj.alias) + delattr(FakeExt, 'server_%s' % name) + + def test_load_update_extension_point(self): + self._test_load_extension_point('update') + + def test_load_rebuild_extension_point(self): + self._test_load_extension_point('rebuild') + + def test_load_create_extension_point(self): + self._test_load_extension_point('create') + + def test_load_resize_extension_point(self): + self._test_load_extension_point('resize') + + +class TestServersExtensionSchema(test.NoDBTestCase): + def setUp(self): + super(TestServersExtensionSchema, self).setUp() + CONF.set_override('extensions_whitelist', ['disk_config'], 'osapi_v3') + + def _test_load_extension_schema(self, name): + setattr(FakeExt, 'get_server_%s_schema' % name, + FakeExt.fake_extension_point) + ext_info = plugins.LoadedExtensionInfo() + controller = servers.ServersController(extension_info=ext_info) + self.assertTrue(hasattr(controller, '%s_schema_manager' % name)) + + delattr(FakeExt, 'get_server_%s_schema' % name) + return getattr(controller, 'schema_server_%s' % name) + + def test_load_create_extension_point(self): + # The expected is the schema combination of base and keypairs + # because of the above extensions_whitelist. + expected_schema = copy.deepcopy(servers_schema.base_create) + expected_schema['properties']['server']['properties'].update( + disk_config_schema.server_create) + + actual_schema = self._test_load_extension_schema('create') + self.assertEqual(expected_schema, actual_schema) + + def test_load_update_extension_point(self): + # keypair extension does not contain update_server() and + # here checks that any extension is not added to the schema. + expected_schema = copy.deepcopy(servers_schema.base_update) + expected_schema['properties']['server']['properties'].update( + disk_config_schema.server_create) + + actual_schema = self._test_load_extension_schema('update') + self.assertEqual(expected_schema, actual_schema) + + def test_load_rebuild_extension_point(self): + # keypair extension does not contain rebuild_server() and + # here checks that any extension is not added to the schema. + expected_schema = copy.deepcopy(servers_schema.base_rebuild) + expected_schema['properties']['rebuild']['properties'].update( + disk_config_schema.server_create) + + actual_schema = self._test_load_extension_schema('rebuild') + self.assertEqual(expected_schema, actual_schema) + + def test_load_resize_extension_point(self): + # keypair extension does not contain resize_server() and + # here checks that any extension is not added to the schema. + expected_schema = copy.deepcopy(servers_schema.base_resize) + expected_schema['properties']['resize']['properties'].update( + disk_config_schema.server_create) + + actual_schema = self._test_load_extension_schema('resize') + self.assertEqual(expected_schema, actual_schema) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_services.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_services.py new file mode 100644 index 0000000000..072992cbb6 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_services.py @@ -0,0 +1,453 @@ +# Copyright 2012 IBM Corp. +# +# 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 calendar +import datetime + +import iso8601 +import mock +from oslo.utils import timeutils +import webob.exc + +from nova.api.openstack.compute.plugins.v3 import services +from nova import availability_zones +from nova.compute import cells_api +from nova import context +from nova import db +from nova import exception +from nova.servicegroup.drivers import db as db_driver +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit.objects import test_service + + +fake_services_list = [ + dict(test_service.fake_service, + binary='nova-scheduler', + host='host1', + id=1, + disabled=True, + topic='scheduler', + updated_at=datetime.datetime(2012, 10, 29, 13, 42, 2), + created_at=datetime.datetime(2012, 9, 18, 2, 46, 27), + disabled_reason='test1'), + dict(test_service.fake_service, + binary='nova-compute', + host='host1', + id=2, + disabled=True, + topic='compute', + updated_at=datetime.datetime(2012, 10, 29, 13, 42, 5), + created_at=datetime.datetime(2012, 9, 18, 2, 46, 27), + disabled_reason='test2'), + dict(test_service.fake_service, + binary='nova-scheduler', + host='host2', + id=3, + disabled=False, + topic='scheduler', + updated_at=datetime.datetime(2012, 9, 19, 6, 55, 34), + created_at=datetime.datetime(2012, 9, 18, 2, 46, 28), + disabled_reason=None), + dict(test_service.fake_service, + binary='nova-compute', + host='host2', + id=4, + disabled=True, + topic='compute', + updated_at=datetime.datetime(2012, 9, 18, 8, 3, 38), + created_at=datetime.datetime(2012, 9, 18, 2, 46, 28), + disabled_reason='test4'), + ] + + +class FakeRequest(object): + environ = {"nova.context": context.get_admin_context()} + GET = {} + + +class FakeRequestWithService(object): + environ = {"nova.context": context.get_admin_context()} + GET = {"binary": "nova-compute"} + + +class FakeRequestWithHost(object): + environ = {"nova.context": context.get_admin_context()} + GET = {"host": "host1"} + + +class FakeRequestWithHostService(object): + environ = {"nova.context": context.get_admin_context()} + GET = {"host": "host1", "binary": "nova-compute"} + + +def fake_service_get_all(services): + def service_get_all(context, filters=None, set_zones=False): + if set_zones or 'availability_zone' in filters: + return availability_zones.set_availability_zones(context, + services) + return services + return service_get_all + + +def fake_db_api_service_get_all(context, disabled=None): + return fake_services_list + + +def fake_db_service_get_by_host_binary(services): + def service_get_by_host_binary(context, host, binary): + for service in services: + if service['host'] == host and service['binary'] == binary: + return service + raise exception.HostBinaryNotFound(host=host, binary=binary) + return service_get_by_host_binary + + +def fake_service_get_by_host_binary(context, host, binary): + fake = fake_db_service_get_by_host_binary(fake_services_list) + return fake(context, host, binary) + + +def _service_get_by_id(services, value): + for service in services: + if service['id'] == value: + return service + return None + + +def fake_db_service_update(services): + def service_update(context, service_id, values): + service = _service_get_by_id(services, service_id) + if service is None: + raise exception.ServiceNotFound(service_id=service_id) + return service + return service_update + + +def fake_service_update(context, service_id, values): + fake = fake_db_service_update(fake_services_list) + return fake(context, service_id, values) + + +def fake_utcnow(): + return datetime.datetime(2012, 10, 29, 13, 42, 11) + + +fake_utcnow.override_time = None + + +def fake_utcnow_ts(): + d = fake_utcnow() + return calendar.timegm(d.utctimetuple()) + + +class ServicesTest(test.TestCase): + + def setUp(self): + super(ServicesTest, self).setUp() + + self.controller = services.ServiceController() + + self.stubs.Set(timeutils, "utcnow", fake_utcnow) + self.stubs.Set(timeutils, "utcnow_ts", fake_utcnow_ts) + + self.stubs.Set(self.controller.host_api, "service_get_all", + fake_service_get_all(fake_services_list)) + + self.stubs.Set(db, "service_get_by_args", + fake_db_service_get_by_host_binary(fake_services_list)) + self.stubs.Set(db, "service_update", + fake_db_service_update(fake_services_list)) + + def test_services_list(self): + req = FakeRequest() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-scheduler', + 'id': 1, + 'host': 'host1', + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + 'disabled_reason': 'test1'}, + {'binary': 'nova-compute', + 'host': 'host1', + 'id': 2, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), + 'disabled_reason': 'test2'}, + {'binary': 'nova-scheduler', + 'host': 'host2', + 'id': 3, + 'zone': 'internal', + 'status': 'enabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34), + 'disabled_reason': None}, + {'binary': 'nova-compute', + 'host': 'host2', + 'id': 4, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38), + 'disabled_reason': 'test4'}]} + self.assertEqual(res_dict, response) + + def test_service_list_with_host(self): + req = FakeRequestWithHost() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-scheduler', + 'host': 'host1', + 'id': 1, + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + 'disabled_reason': 'test1'}, + {'binary': 'nova-compute', + 'host': 'host1', + 'id': 2, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), + 'disabled_reason': 'test2'}]} + self.assertEqual(res_dict, response) + + def test_service_list_with_service(self): + req = FakeRequestWithService() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-compute', + 'host': 'host1', + 'id': 2, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), + 'disabled_reason': 'test2'}, + {'binary': 'nova-compute', + 'host': 'host2', + 'id': 4, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38), + 'disabled_reason': 'test4'}]} + self.assertEqual(res_dict, response) + + def test_service_list_with_host_service(self): + req = FakeRequestWithHostService() + res_dict = self.controller.index(req) + response = {'services': [ + {'binary': 'nova-compute', + 'host': 'host1', + 'id': 2, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5), + 'disabled_reason': 'test2'}]} + self.assertEqual(res_dict, response) + + def test_services_enable(self): + def _service_update(context, service_id, values): + self.assertIsNone(values['disabled_reason']) + return dict(test_service.fake_service, id=service_id) + + self.stubs.Set(db, "service_update", _service_update) + + body = {'service': {'host': 'host1', + 'binary': 'nova-compute'}} + req = fakes.HTTPRequestV3.blank('/os-services/enable') + res_dict = self.controller.update(req, "enable", body) + + self.assertEqual(res_dict['service']['status'], 'enabled') + self.assertNotIn('disabled_reason', res_dict['service']) + + def test_services_enable_with_invalid_host(self): + body = {'service': {'host': 'invalid', + 'binary': 'nova-compute'}} + req = fakes.HTTPRequestV3.blank('/os-services/enable') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, + req, + "enable", + body) + + def test_services_enable_with_invalid_binary(self): + body = {'service': {'host': 'host1', + 'binary': 'invalid'}} + req = fakes.HTTPRequestV3.blank('/os-services/enable') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, + req, + "enable", + body) + + # This test is just to verify that the servicegroup API gets used when + # calling this API. + def test_services_with_exception(self): + def dummy_is_up(self, dummy): + raise KeyError() + + self.stubs.Set(db_driver.DbDriver, 'is_up', dummy_is_up) + req = FakeRequestWithHostService() + self.assertRaises(webob.exc.HTTPInternalServerError, + self.controller.index, req) + + def test_services_disable(self): + req = fakes.HTTPRequestV3.blank('/os-services/disable') + body = {'service': {'host': 'host1', + 'binary': 'nova-compute'}} + res_dict = self.controller.update(req, "disable", body) + + self.assertEqual(res_dict['service']['status'], 'disabled') + self.assertNotIn('disabled_reason', res_dict['service']) + + def test_services_disable_with_invalid_host(self): + body = {'service': {'host': 'invalid', + 'binary': 'nova-compute'}} + req = fakes.HTTPRequestV3.blank('/os-services/disable') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, + req, + "disable", + body) + + def test_services_disable_with_invalid_binary(self): + body = {'service': {'host': 'host1', + 'binary': 'invalid'}} + req = fakes.HTTPRequestV3.blank('/os-services/disable') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, + req, + "disable", + body) + + def test_services_disable_log_reason(self): + req = \ + fakes.HTTPRequestV3.blank('/os-services/disable-log-reason') + body = {'service': {'host': 'host1', + 'binary': 'nova-compute', + 'disabled_reason': 'test-reason'}} + res_dict = self.controller.update(req, "disable-log-reason", body) + + self.assertEqual(res_dict['service']['status'], 'disabled') + self.assertEqual(res_dict['service']['disabled_reason'], 'test-reason') + + def test_mandatory_reason_field(self): + req = \ + fakes.HTTPRequestV3.blank('/os-services/disable-log-reason') + body = {'service': {'host': 'host1', + 'binary': 'nova-compute'}} + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, "disable-log-reason", body) + + def test_invalid_reason_field(self): + reason = ' ' + self.assertFalse(self.controller._is_valid_as_reason(reason)) + reason = 'a' * 256 + self.assertFalse(self.controller._is_valid_as_reason(reason)) + reason = 'it\'s a valid reason.' + self.assertTrue(self.controller._is_valid_as_reason(reason)) + + def test_services_delete(self): + request = fakes.HTTPRequestV3.blank('/v3/os-services/1', + use_admin_context=True) + request.method = 'DELETE' + + with mock.patch.object(self.controller.host_api, + 'service_delete') as service_delete: + self.controller.delete(request, '1') + service_delete.assert_called_once_with( + request.environ['nova.context'], '1') + self.assertEqual(self.controller.delete.wsgi_code, 204) + + def test_services_delete_not_found(self): + request = fakes.HTTPRequestV3.blank('/v3/os-services/abc', + use_admin_context=True) + request.method = 'DELETE' + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, request, 'abc') + + +class ServicesCellsTest(test.TestCase): + def setUp(self): + super(ServicesCellsTest, self).setUp() + + host_api = cells_api.HostAPI() + + self.controller = services.ServiceController() + self.controller.host_api = host_api + + self.stubs.Set(timeutils, "utcnow", fake_utcnow) + self.stubs.Set(timeutils, "utcnow_ts", fake_utcnow_ts) + + services_list = [] + for service in fake_services_list: + service = service.copy() + service['id'] = 'cell1@%d' % service['id'] + services_list.append(service) + + self.stubs.Set(host_api.cells_rpcapi, "service_get_all", + fake_service_get_all(services_list)) + + def test_services_detail(self): + req = FakeRequest() + res_dict = self.controller.index(req) + utc = iso8601.iso8601.Utc() + response = {'services': [ + {'id': 'cell1@1', + 'binary': 'nova-scheduler', + 'host': 'host1', + 'zone': 'internal', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2, + tzinfo=utc), + 'disabled_reason': 'test1'}, + {'id': 'cell1@2', + 'binary': 'nova-compute', + 'host': 'host1', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'up', + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 5, + tzinfo=utc), + 'disabled_reason': 'test2'}, + {'id': 'cell1@3', + 'binary': 'nova-scheduler', + 'host': 'host2', + 'zone': 'internal', + 'status': 'enabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 19, 6, 55, 34, + tzinfo=utc), + 'disabled_reason': None}, + {'id': 'cell1@4', + 'binary': 'nova-compute', + 'host': 'host2', + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime(2012, 9, 18, 8, 3, 38, + tzinfo=utc), + 'disabled_reason': 'test4'}]} + self.assertEqual(res_dict, response) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_suspend_server.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_suspend_server.py new file mode 100644 index 0000000000..b0b71a0229 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_suspend_server.py @@ -0,0 +1,48 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +from nova.api.openstack.compute.plugins.v3 import suspend_server +from nova.tests.unit.api.openstack.compute.plugins.v3 import \ + admin_only_action_common +from nova.tests.unit.api.openstack import fakes + + +class SuspendServerTests(admin_only_action_common.CommonTests): + def setUp(self): + super(SuspendServerTests, self).setUp() + self.controller = suspend_server.SuspendServerController() + self.compute_api = self.controller.compute_api + + def _fake_controller(*args, **kwargs): + return self.controller + + self.stubs.Set(suspend_server, 'SuspendServerController', + _fake_controller) + self.app = fakes.wsgi_app_v21(init_only=('servers', + 'os-suspend-server'), + fake_auth_context=self.context) + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_suspend_resume(self): + self._test_actions(['suspend', 'resume']) + + def test_suspend_resume_with_non_existed_instance(self): + self._test_actions_with_non_existed_instance(['suspend', 'resume']) + + def test_suspend_resume_raise_conflict_on_invalid_state(self): + self._test_actions_raise_conflict_on_invalid_state(['suspend', + 'resume']) + + def test_actions_with_locked_instance(self): + self._test_actions_with_locked_instance(['suspend', 'resume']) diff --git a/nova/tests/unit/api/openstack/compute/plugins/v3/test_user_data.py b/nova/tests/unit/api/openstack/compute/plugins/v3/test_user_data.py new file mode 100644 index 0000000000..0e10c283f7 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/plugins/v3/test_user_data.py @@ -0,0 +1,195 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2013 IBM Corp. +# 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 base64 +import datetime +import uuid + +from oslo.config import cfg +from oslo.serialization import jsonutils + +from nova.api.openstack.compute import plugins +from nova.api.openstack.compute.plugins.v3 import servers +from nova.api.openstack.compute.plugins.v3 import user_data +from nova.compute import api as compute_api +from nova.compute import flavors +from nova import db +from nova import exception +from nova.network import manager +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance +from nova.tests.unit.image import fake + + +CONF = cfg.CONF +FAKE_UUID = fakes.FAKE_UUID + + +def fake_gen_uuid(): + return FAKE_UUID + + +def return_security_group(context, instance_id, security_group_id): + pass + + +class ServersControllerCreateTest(test.TestCase): + + def setUp(self): + """Shared implementation for tests below that create instance.""" + super(ServersControllerCreateTest, self).setUp() + + self.flags(verbose=True, + enable_instance_password=True) + self.instance_cache_num = 0 + self.instance_cache_by_id = {} + self.instance_cache_by_uuid = {} + + ext_info = plugins.LoadedExtensionInfo() + self.controller = servers.ServersController(extension_info=ext_info) + CONF.set_override('extensions_blacklist', 'os-user-data', + 'osapi_v3') + self.no_user_data_controller = servers.ServersController( + extension_info=ext_info) + + def instance_create(context, inst): + inst_type = flavors.get_flavor_by_flavor_id(3) + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + def_image_ref = 'http://localhost/images/%s' % image_uuid + self.instance_cache_num += 1 + instance = fake_instance.fake_db_instance(**{ + 'id': self.instance_cache_num, + 'display_name': inst['display_name'] or 'test', + 'uuid': FAKE_UUID, + 'instance_type': inst_type, + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': 'fead::1234', + 'image_ref': inst.get('image_ref', def_image_ref), + 'user_id': 'fake', + 'project_id': 'fake', + 'reservation_id': inst['reservation_id'], + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + user_data.ATTRIBUTE_NAME: None, + "progress": 0, + "fixed_ips": [], + "task_state": "", + "vm_state": "", + "root_device_name": inst.get('root_device_name', 'vda'), + }) + + self.instance_cache_by_id[instance['id']] = instance + self.instance_cache_by_uuid[instance['uuid']] = instance + return instance + + def instance_get(context, instance_id): + """Stub for compute/api create() pulling in instance after + scheduling + """ + return self.instance_cache_by_id[instance_id] + + def instance_update(context, uuid, values): + instance = self.instance_cache_by_uuid[uuid] + instance.update(values) + return instance + + def server_update(context, instance_uuid, params): + inst = self.instance_cache_by_uuid[instance_uuid] + inst.update(params) + return (inst, inst) + + def fake_method(*args, **kwargs): + pass + + def project_get_networks(context, user_id): + return dict(id='1', host='localhost') + + def queue_get_for(context, *args): + return 'network_topic' + + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + fake.stub_out_image_service(self.stubs) + fakes.stub_out_nw_api(self.stubs) + self.stubs.Set(uuid, 'uuid4', fake_gen_uuid) + self.stubs.Set(db, 'instance_add_security_group', + return_security_group) + self.stubs.Set(db, 'project_get_networks', + project_get_networks) + self.stubs.Set(db, 'instance_create', instance_create) + self.stubs.Set(db, 'instance_system_metadata_update', + fake_method) + self.stubs.Set(db, 'instance_get', instance_get) + self.stubs.Set(db, 'instance_update', instance_update) + self.stubs.Set(db, 'instance_update_and_get_original', + server_update) + self.stubs.Set(manager.VlanManager, 'allocate_fixed_ip', + fake_method) + + def _test_create_extra(self, params, no_image=False, + override_controller=None): + image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + server = dict(name='server_test', imageRef=image_uuid, flavorRef=2) + if no_image: + server.pop('imageRef', None) + server.update(params) + body = dict(server=server) + req = fakes.HTTPRequestV3.blank('/servers') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + if override_controller: + server = override_controller.create(req, body=body).obj['server'] + else: + server = self.controller.create(req, body=body).obj['server'] + return server + + def test_create_instance_with_user_data_disabled(self): + params = {user_data.ATTRIBUTE_NAME: base64.b64encode('fake')} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertNotIn('user_data', kwargs) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra( + params, + override_controller=self.no_user_data_controller) + + def test_create_instance_with_user_data_enabled(self): + params = {user_data.ATTRIBUTE_NAME: base64.b64encode('fake')} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertIn('user_data', kwargs) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_user_data(self): + value = base64.b64encode("A random string") + params = {user_data.ATTRIBUTE_NAME: value} + server = self._test_create_extra(params) + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_with_bad_user_data(self): + value = "A random string" + params = {user_data.ATTRIBUTE_NAME: value} + self.assertRaises(exception.ValidationError, + self._test_create_extra, params) diff --git a/nova/tests/unit/api/openstack/compute/schemas/__init__.py b/nova/tests/unit/api/openstack/compute/schemas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/__init__.py diff --git a/nova/tests/unit/api/openstack/compute/schemas/test_schemas.py b/nova/tests/unit/api/openstack/compute/schemas/test_schemas.py new file mode 100644 index 0000000000..c6ce82057e --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/test_schemas.py @@ -0,0 +1,106 @@ +# 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 glob +import os + +import lxml.etree + +from nova import test + +SCHEMAS = "nova/api/openstack/compute/schemas" + + +class RelaxNGSchemaTestCase(test.NoDBTestCase): + """various validation tasks for the RelaxNG schemas + + lxml.etree has no built-in way to validate an entire namespace + (i.e., multiple RelaxNG schema files defining elements in the same + namespace), so we define a few tests that should hopefully reduce + the risk of an inconsistent namespace + """ + + def _load_schema(self, schemafile): + return lxml.etree.RelaxNG(lxml.etree.parse(schemafile)) + + def _load_test_cases(self, path): + """load test cases from the given path.""" + rv = dict(valid=[], invalid=[]) + path = os.path.join(os.path.dirname(__file__), path) + for ctype in rv.keys(): + for cfile in glob.glob(os.path.join(path, ctype, "*.xml")): + rv[ctype].append(lxml.etree.parse(cfile)) + return rv + + def _validate_schema(self, schemafile): + """validate a single RelaxNG schema file.""" + try: + self._load_schema(schemafile) + except lxml.etree.RelaxNGParseError as err: + self.fail("%s is not a valid RelaxNG schema: %s" % + (schemafile, err)) + + def _api_versions(self): + """get a list of API versions.""" + return [''] + [os.path.basename(v) + for v in glob.glob(os.path.join(SCHEMAS, "v*"))] + + def _schema_files(self, api_version): + return glob.glob(os.path.join(SCHEMAS, api_version, "*.rng")) + + def test_schema_validity(self): + for api_version in self._api_versions(): + for schema in self._schema_files(api_version): + self._validate_schema(schema) + + def test_schema_duplicate_elements(self): + for api_version in self._api_versions(): + elements = dict() + duplicates = dict() + for schemafile in self._schema_files(api_version): + schema = lxml.etree.parse(schemafile) + fname = os.path.basename(schemafile) + if schema.getroot().tag != "element": + # we don't do any sort of validation on grammars + # yet + continue + el_name = schema.getroot().get("name") + if el_name in elements: + duplicates.setdefault(el_name, + [elements[el_name]]).append(fname) + else: + elements[el_name] = fname + self.assertEqual(len(duplicates), 0, + "Duplicate element definitions found: %s" % + "; ".join("%s in %s" % dup + for dup in duplicates.items())) + + def test_schema_explicit_cases(self): + cases = {'v1.1/flavors.rng': self._load_test_cases("v1.1/flavors"), + 'v1.1/images.rng': self._load_test_cases("v1.1/images"), + 'v1.1/servers.rng': self._load_test_cases("v1.1/servers")} + + for schemafile, caselists in cases.items(): + schema = self._load_schema(os.path.join(SCHEMAS, schemafile)) + for case in caselists['valid']: + self.assertTrue(schema.validate(case), + "Schema validation failed against %s: %s\n%s" % + (schemafile, schema.error_log, case)) + + for case in caselists['invalid']: + self.assertFalse( + schema.validate(case), + "Schema validation succeeded unexpectedly against %s: %s" + "\n%s" % (schemafile, schema.error_log, case)) diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/mixed.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/mixed.xml new file mode 100644 index 0000000000..df4368bf41 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/mixed.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<flavors xmlns="http://docs.openstack.org/compute/api/v1.1"> + <!-- you cannot mix flavor references (i.e., with only name and id) + and specifications (with all attributes) in the same document + --> + <flavor name="foo" id="foo"/> + <flavor name="bar" id="bar" ram="bar" disk="bar" vcpus="bar"/> +</flavors> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial.xml new file mode 100644 index 0000000000..3343a7be59 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<flavors xmlns="http://docs.openstack.org/compute/api/v1.1"> + <!-- this flavor is only partially specified --> + <flavor name="foo"/> +</flavors> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial2.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial2.xml new file mode 100644 index 0000000000..f67c5a82fe --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/invalid/partial2.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<flavors xmlns="http://docs.openstack.org/compute/api/v1.1"> + <!-- a flavor can either have *only* name and id, or needs to also + have disk and vcpus in addition to ram --> + <flavor name="foo" id="foo" ram="foo"/> +</flavors> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/empty.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/empty.xml new file mode 100644 index 0000000000..36aa3936e7 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/empty.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<flavors xmlns="http://docs.openstack.org/compute/api/v1.1"/> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/full.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/full.xml new file mode 100644 index 0000000000..59eafc8608 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/full.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<flavors xmlns="http://docs.openstack.org/compute/api/v1.1"> + <flavor name="foo" id="foo" ram="foo" disk="foo" vcpus="foo"/> +</flavors> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/refs.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/refs.xml new file mode 100644 index 0000000000..751b626258 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/flavors/valid/refs.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<flavors xmlns="http://docs.openstack.org/compute/api/v1.1"> + <flavor name="foo" id="foo"/> + <flavor name="bar" id="bar"/> +</flavors> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/mixed.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/mixed.xml new file mode 100644 index 0000000000..8f7bf208ae --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/mixed.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<images xmlns="http://docs.openstack.org/compute/api/v1.1"> + <!-- cannot mix refs and specs in the same document --> + <image name="foo" id="foo"/> + <image name="bar" id="bar" updated="1401991486" created="1401991486" + status="foo"> + <metadata/> + </image> +</images> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/no-metadata.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/no-metadata.xml new file mode 100644 index 0000000000..435294e27c --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/no-metadata.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<images xmlns="http://docs.openstack.org/compute/api/v1.1"> + <!-- image specs require a metadata child --> + <image name="foo" id="foo" updated="1401991486" created="1401991486" + status="foo"/> +</images> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial.xml new file mode 100644 index 0000000000..5637cce787 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<images xmlns="http://docs.openstack.org/compute/api/v1.1"> + <!-- image refs require id --> + <image name="foo"/> +</images> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial2.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial2.xml new file mode 100644 index 0000000000..db5e974621 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/invalid/partial2.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<images xmlns="http://docs.openstack.org/compute/api/v1.1"> + <!-- an image must be either a ref, with *only* name and id attrs, + or fully specified, with name, id, updated, created, status, + and a metadata child --> + <image name="foo" id="foo" updated="foo"/> +</images> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/empty.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/empty.xml new file mode 100644 index 0000000000..05e0b8241c --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/empty.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<images xmlns="http://docs.openstack.org/compute/api/v1.1"/> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/full.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/full.xml new file mode 100644 index 0000000000..4f148db625 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/full.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<images xmlns="http://docs.openstack.org/compute/api/v1.1"> + <image name="foo" id="foo" updated="1401991486" created="1401991486" + status="foo"> + <metadata/> + </image> + <image name="bar" id="bar" updated="1401991486" created="1401991486" + status="bar" progress="bar" minDisk="100" minRam="100"> + <server id="bar"/> + <metadata> + <meta key="baz">baz</meta> + </metadata> + </image> +</images> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/refs.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/refs.xml new file mode 100644 index 0000000000..1dfedd2c77 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/images/valid/refs.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<images xmlns="http://docs.openstack.org/compute/api/v1.1"> + <image name="foo" id="foo"/> + <image name="bar" id="bar"/> +</images> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/mixed.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/mixed.xml new file mode 100644 index 0000000000..c941472beb --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/mixed.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<servers xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom"> + <!-- you cannot mix server refs and specs in the same document --> + <server name="foo" id="foo"/> + <server name="foo" userId="foo" tenantId="foo" id="foo" updated="1401991486" + created="1401991486" hostId="foo" accessIPv4="1.2.3.4" + accessIPv6="::1" status="foo"> + <image id="foo"> + <atom:link href="/compute/api/v1.1/image/foo"/> + </image> + <flavor id="foo"> + <atom:link href="/compute/api/v1.1/flavor/foo"/> + </flavor> + <metadata/> + <addresses/> + </server> +</servers> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial.xml new file mode 100644 index 0000000000..721ce84327 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<servers xmlns="http://docs.openstack.org/compute/api/v1.1"> + <!-- server refs require the id attr --> + <server name="foo"/> +</servers> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial2.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial2.xml new file mode 100644 index 0000000000..474b3a084e --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial2.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<servers xmlns="http://docs.openstack.org/compute/api/v1.1"> + <!-- server tags must either be refs, with *only* name and id, or + full specifications, with loads more detail --> + <server name="foo" id="foo" updated="foo"/> +</servers> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial3.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial3.xml new file mode 100644 index 0000000000..6455fe899a --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/invalid/partial3.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<servers xmlns="http://docs.openstack.org/compute/api/v1.1"> + <!-- the server specification requires a number of children --> + <server name="foo" userId="foo" tenantId="foo" id="foo" updated="1401991486" + created="1401991486" hostId="foo" accessIPv4="1.2.3.4" + accessIPv6="::1" status="foo"/> +</servers> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/detailed.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/detailed.xml new file mode 100644 index 0000000000..97f5ee44e6 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/detailed.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<servers xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom"> + <server name="bar" userId="bar" tenantId="bar" id="bar" updated="1401991486" + created="1401991486" hostId="bar" accessIPv4="1.2.3.4" + accessIPv6="::1" status="bar" progress="10" adminPass="bar"> + <image id="foo"> + <atom:link href="/compute/api/v1.1/image/foo"/> + </image> + <flavor id="foo"> + <atom:link href="/compute/api/v1.1/flavor/foo"/> + </flavor> + <fault code="1" created="1401991486"> + <message>fault</message> + <details>fault</details> + </fault> + <metadata> + <meta key="bar">bar</meta> + </metadata> + <addresses> + <network id="bar"/> + <network id="baz"> + <ip version="4" addr="1.2.3.4"/> + </network> + </addresses> + </server> +</servers> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/empty.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/empty.xml new file mode 100644 index 0000000000..b2f3666245 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/empty.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<servers xmlns="http://docs.openstack.org/compute/api/v1.1"/> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/full.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/full.xml new file mode 100644 index 0000000000..fbd6202a76 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/full.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<servers xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:atom="http://www.w3.org/2005/Atom"> + <server name="foo" userId="foo" tenantId="foo" id="foo" updated="1401991486" + created="1401991486" hostId="foo" accessIPv4="1.2.3.4" + accessIPv6="::1" status="foo"> + <image id="foo"> + <atom:link href="/compute/api/v1.1/image/foo"/> + </image> + <flavor id="foo"> + <atom:link href="/compute/api/v1.1/flavor/foo"/> + </flavor> + <metadata/> + <addresses/> + </server> +</servers> diff --git a/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/refs.xml b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/refs.xml new file mode 100644 index 0000000000..e1212e985f --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/schemas/v1.1/servers/valid/refs.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<servers xmlns="http://docs.openstack.org/compute/api/v1.1"> + <server name="foo" id="foo"/> + <server name="bar" id="bar"/> +</servers> diff --git a/nova/tests/unit/api/openstack/compute/test_api.py b/nova/tests/unit/api/openstack/compute/test_api.py new file mode 100644 index 0000000000..f86c04d4bd --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_api.py @@ -0,0 +1,186 @@ +# Copyright 2010 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. + +from lxml import etree +from oslo.serialization import jsonutils +import webob +import webob.dec +import webob.exc + +from nova.api import openstack as openstack_api +from nova.api.openstack import wsgi +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes + + +class APITest(test.NoDBTestCase): + + def _wsgi_app(self, inner_app): + # simpler version of the app than fakes.wsgi_app + return openstack_api.FaultWrapper(inner_app) + + def test_malformed_json(self): + req = webob.Request.blank('/') + req.method = 'POST' + req.body = '{' + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_malformed_xml(self): + req = webob.Request.blank('/') + req.method = 'POST' + req.body = '<hi im not xml>' + req.headers["content-type"] = "application/xml" + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_vendor_content_type_json(self): + ctype = 'application/vnd.openstack.compute+json' + + req = webob.Request.blank('/') + req.headers['Accept'] = ctype + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, ctype) + + jsonutils.loads(res.body) + + def test_vendor_content_type_xml(self): + ctype = 'application/vnd.openstack.compute+xml' + + req = webob.Request.blank('/') + req.headers['Accept'] = ctype + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, ctype) + + etree.XML(res.body) + + def test_exceptions_are_converted_to_faults_webob_exc(self): + @webob.dec.wsgify + def raise_webob_exc(req): + raise webob.exc.HTTPNotFound(explanation='Raised a webob.exc') + + # api.application = raise_webob_exc + api = self._wsgi_app(raise_webob_exc) + resp = webob.Request.blank('/').get_response(api) + self.assertEqual(resp.status_int, 404, resp.body) + + def test_exceptions_are_converted_to_faults_api_fault(self): + @webob.dec.wsgify + def raise_api_fault(req): + exc = webob.exc.HTTPNotFound(explanation='Raised a webob.exc') + return wsgi.Fault(exc) + + # api.application = raise_api_fault + api = self._wsgi_app(raise_api_fault) + resp = webob.Request.blank('/').get_response(api) + self.assertIn('itemNotFound', resp.body) + self.assertEqual(resp.status_int, 404, resp.body) + + def test_exceptions_are_converted_to_faults_exception(self): + @webob.dec.wsgify + def fail(req): + raise Exception("Threw an exception") + + # api.application = fail + api = self._wsgi_app(fail) + resp = webob.Request.blank('/').get_response(api) + self.assertIn('{"computeFault', resp.body) + self.assertEqual(resp.status_int, 500, resp.body) + + def test_exceptions_are_converted_to_faults_exception_xml(self): + @webob.dec.wsgify + def fail(req): + raise Exception("Threw an exception") + + # api.application = fail + api = self._wsgi_app(fail) + resp = webob.Request.blank('/.xml').get_response(api) + self.assertIn('<computeFault', resp.body) + self.assertEqual(resp.status_int, 500, resp.body) + + def _do_test_exception_safety_reflected_in_faults(self, expose): + class ExceptionWithSafety(exception.NovaException): + safe = expose + + @webob.dec.wsgify + def fail(req): + raise ExceptionWithSafety('some explanation') + + api = self._wsgi_app(fail) + resp = webob.Request.blank('/').get_response(api) + self.assertIn('{"computeFault', resp.body) + expected = ('ExceptionWithSafety: some explanation' if expose else + 'The server has either erred or is incapable ' + 'of performing the requested operation.') + self.assertIn(expected, resp.body) + self.assertEqual(resp.status_int, 500, resp.body) + + def test_safe_exceptions_are_described_in_faults(self): + self._do_test_exception_safety_reflected_in_faults(True) + + def test_unsafe_exceptions_are_not_described_in_faults(self): + self._do_test_exception_safety_reflected_in_faults(False) + + def _do_test_exception_mapping(self, exception_type, msg): + @webob.dec.wsgify + def fail(req): + raise exception_type(msg) + + api = self._wsgi_app(fail) + resp = webob.Request.blank('/').get_response(api) + self.assertIn(msg, resp.body) + self.assertEqual(resp.status_int, exception_type.code, resp.body) + + if hasattr(exception_type, 'headers'): + for (key, value) in exception_type.headers.iteritems(): + self.assertIn(key, resp.headers) + self.assertEqual(resp.headers[key], str(value)) + + def test_quota_error_mapping(self): + self._do_test_exception_mapping(exception.QuotaError, 'too many used') + + def test_non_nova_notfound_exception_mapping(self): + class ExceptionWithCode(Exception): + code = 404 + + self._do_test_exception_mapping(ExceptionWithCode, + 'NotFound') + + def test_non_nova_exception_mapping(self): + class ExceptionWithCode(Exception): + code = 417 + + self._do_test_exception_mapping(ExceptionWithCode, + 'Expectation failed') + + def test_exception_with_none_code_throws_500(self): + class ExceptionWithNoneCode(Exception): + code = None + + @webob.dec.wsgify + def fail(req): + raise ExceptionWithNoneCode() + + api = self._wsgi_app(fail) + resp = webob.Request.blank('/').get_response(api) + self.assertEqual(500, resp.status_int) diff --git a/nova/tests/unit/api/openstack/compute/test_auth.py b/nova/tests/unit/api/openstack/compute/test_auth.py new file mode 100644 index 0000000000..0386623b5d --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_auth.py @@ -0,0 +1,61 @@ +# Copyright 2010 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 webob +import webob.dec + +from nova import context +from nova import test +from nova.tests.unit.api.openstack import fakes + + +class TestNoAuthMiddleware(test.NoDBTestCase): + + def setUp(self): + super(TestNoAuthMiddleware, self).setUp() + self.stubs.Set(context, 'RequestContext', fakes.FakeRequestContext) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_networking(self.stubs) + + def test_authorize_user(self): + req = webob.Request.blank('/v2') + req.headers['X-Auth-User'] = 'user1' + req.headers['X-Auth-Key'] = 'user1_key' + req.headers['X-Auth-Project-Id'] = 'user1_project' + result = req.get_response(fakes.wsgi_app(use_no_auth=True)) + self.assertEqual(result.status, '204 No Content') + self.assertEqual(result.headers['X-Server-Management-Url'], + "http://localhost/v2/user1_project") + + def test_authorize_user_trailing_slash(self): + # make sure it works with trailing slash on the request + req = webob.Request.blank('/v2/') + req.headers['X-Auth-User'] = 'user1' + req.headers['X-Auth-Key'] = 'user1_key' + req.headers['X-Auth-Project-Id'] = 'user1_project' + result = req.get_response(fakes.wsgi_app(use_no_auth=True)) + self.assertEqual(result.status, '204 No Content') + self.assertEqual(result.headers['X-Server-Management-Url'], + "http://localhost/v2/user1_project") + + def test_auth_token_no_empty_headers(self): + req = webob.Request.blank('/v2') + req.headers['X-Auth-User'] = 'user1' + req.headers['X-Auth-Key'] = 'user1_key' + req.headers['X-Auth-Project-Id'] = 'user1_project' + result = req.get_response(fakes.wsgi_app(use_no_auth=True)) + self.assertEqual(result.status, '204 No Content') + self.assertNotIn('X-CDN-Management-Url', result.headers) + self.assertNotIn('X-Storage-Url', result.headers) diff --git a/nova/tests/unit/api/openstack/compute/test_consoles.py b/nova/tests/unit/api/openstack/compute/test_consoles.py new file mode 100644 index 0000000000..3ba99899c0 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_consoles.py @@ -0,0 +1,293 @@ +# Copyright 2010-2011 OpenStack Foundation +# Copyright 2011 Piston Cloud Computing, Inc. +# 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 datetime +import uuid as stdlib_uuid + +from lxml import etree +from oslo.utils import timeutils +import webob + +from nova.api.openstack.compute import consoles +from nova.compute import vm_states +from nova import console +from nova import db +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import matchers + + +FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + + +class FakeInstanceDB(object): + + def __init__(self): + self.instances_by_id = {} + self.ids_by_uuid = {} + self.max_id = 0 + + def return_server_by_id(self, context, id): + if id not in self.instances_by_id: + self._add_server(id=id) + return dict(self.instances_by_id[id]) + + def return_server_by_uuid(self, context, uuid): + if uuid not in self.ids_by_uuid: + self._add_server(uuid=uuid) + return dict(self.instances_by_id[self.ids_by_uuid[uuid]]) + + def _add_server(self, id=None, uuid=None): + if id is None: + id = self.max_id + 1 + if uuid is None: + uuid = str(stdlib_uuid.uuid4()) + instance = stub_instance(id, uuid=uuid) + self.instances_by_id[id] = instance + self.ids_by_uuid[uuid] = id + if id > self.max_id: + self.max_id = id + + +def stub_instance(id, user_id='fake', project_id='fake', host=None, + vm_state=None, task_state=None, + reservation_id="", uuid=FAKE_UUID, image_ref="10", + flavor_id="1", name=None, key_name='', + access_ipv4=None, access_ipv6=None, progress=0): + + if host is not None: + host = str(host) + + if key_name: + key_data = 'FAKE' + else: + key_data = '' + + # ReservationID isn't sent back, hack it in there. + server_name = name or "server%s" % id + if reservation_id != "": + server_name = "reservation_%s" % (reservation_id, ) + + instance = { + "id": int(id), + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "admin_pass": "", + "user_id": user_id, + "project_id": project_id, + "image_ref": image_ref, + "kernel_id": "", + "ramdisk_id": "", + "launch_index": 0, + "key_name": key_name, + "key_data": key_data, + "vm_state": vm_state or vm_states.BUILDING, + "task_state": task_state, + "memory_mb": 0, + "vcpus": 0, + "root_gb": 0, + "hostname": "", + "host": host, + "instance_type": {}, + "user_data": "", + "reservation_id": reservation_id, + "mac_address": "", + "scheduled_at": timeutils.utcnow(), + "launched_at": timeutils.utcnow(), + "terminated_at": timeutils.utcnow(), + "availability_zone": "", + "display_name": server_name, + "display_description": "", + "locked": False, + "metadata": [], + "access_ip_v4": access_ipv4, + "access_ip_v6": access_ipv6, + "uuid": uuid, + "progress": progress} + + return instance + + +class ConsolesControllerTest(test.NoDBTestCase): + def setUp(self): + super(ConsolesControllerTest, self).setUp() + self.flags(verbose=True) + self.instance_db = FakeInstanceDB() + self.stubs.Set(db, 'instance_get', + self.instance_db.return_server_by_id) + self.stubs.Set(db, 'instance_get_by_uuid', + self.instance_db.return_server_by_uuid) + self.uuid = str(stdlib_uuid.uuid4()) + self.url = '/v2/fake/servers/%s/consoles' % self.uuid + self.controller = consoles.Controller() + + def test_create_console(self): + def fake_create_console(cons_self, context, instance_id): + self.assertEqual(instance_id, self.uuid) + return {} + self.stubs.Set(console.api.API, 'create_console', fake_create_console) + + req = fakes.HTTPRequest.blank(self.url) + self.controller.create(req, self.uuid, None) + + def test_show_console(self): + def fake_get_console(cons_self, context, instance_id, console_id): + self.assertEqual(instance_id, self.uuid) + self.assertEqual(console_id, 20) + pool = dict(console_type='fake_type', + public_hostname='fake_hostname') + return dict(id=console_id, password='fake_password', + port='fake_port', pool=pool, instance_name='inst-0001') + + expected = {'console': {'id': 20, + 'port': 'fake_port', + 'host': 'fake_hostname', + 'password': 'fake_password', + 'instance_name': 'inst-0001', + 'console_type': 'fake_type'}} + + self.stubs.Set(console.api.API, 'get_console', fake_get_console) + + req = fakes.HTTPRequest.blank(self.url + '/20') + res_dict = self.controller.show(req, self.uuid, '20') + self.assertThat(res_dict, matchers.DictMatches(expected)) + + def test_show_console_unknown_console(self): + def fake_get_console(cons_self, context, instance_id, console_id): + raise exception.ConsoleNotFound(console_id=console_id) + + self.stubs.Set(console.api.API, 'get_console', fake_get_console) + + req = fakes.HTTPRequest.blank(self.url + '/20') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, self.uuid, '20') + + def test_show_console_unknown_instance(self): + def fake_get_console(cons_self, context, instance_id, console_id): + raise exception.InstanceNotFound(instance_id=instance_id) + + self.stubs.Set(console.api.API, 'get_console', fake_get_console) + + req = fakes.HTTPRequest.blank(self.url + '/20') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, self.uuid, '20') + + def test_list_consoles(self): + def fake_get_consoles(cons_self, context, instance_id): + self.assertEqual(instance_id, self.uuid) + + pool1 = dict(console_type='fake_type', + public_hostname='fake_hostname') + cons1 = dict(id=10, password='fake_password', + port='fake_port', pool=pool1) + pool2 = dict(console_type='fake_type2', + public_hostname='fake_hostname2') + cons2 = dict(id=11, password='fake_password2', + port='fake_port2', pool=pool2) + return [cons1, cons2] + + expected = {'consoles': + [{'console': {'id': 10, 'console_type': 'fake_type'}}, + {'console': {'id': 11, 'console_type': 'fake_type2'}}]} + + self.stubs.Set(console.api.API, 'get_consoles', fake_get_consoles) + + req = fakes.HTTPRequest.blank(self.url) + res_dict = self.controller.index(req, self.uuid) + self.assertThat(res_dict, matchers.DictMatches(expected)) + + def test_delete_console(self): + def fake_get_console(cons_self, context, instance_id, console_id): + self.assertEqual(instance_id, self.uuid) + self.assertEqual(console_id, 20) + pool = dict(console_type='fake_type', + public_hostname='fake_hostname') + return dict(id=console_id, password='fake_password', + port='fake_port', pool=pool) + + def fake_delete_console(cons_self, context, instance_id, console_id): + self.assertEqual(instance_id, self.uuid) + self.assertEqual(console_id, 20) + + self.stubs.Set(console.api.API, 'get_console', fake_get_console) + self.stubs.Set(console.api.API, 'delete_console', fake_delete_console) + + req = fakes.HTTPRequest.blank(self.url + '/20') + self.controller.delete(req, self.uuid, '20') + + def test_delete_console_unknown_console(self): + def fake_delete_console(cons_self, context, instance_id, console_id): + raise exception.ConsoleNotFound(console_id=console_id) + + self.stubs.Set(console.api.API, 'delete_console', fake_delete_console) + + req = fakes.HTTPRequest.blank(self.url + '/20') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, self.uuid, '20') + + def test_delete_console_unknown_instance(self): + def fake_delete_console(cons_self, context, instance_id, console_id): + raise exception.InstanceNotFound(instance_id=instance_id) + + self.stubs.Set(console.api.API, 'delete_console', fake_delete_console) + + req = fakes.HTTPRequest.blank(self.url + '/20') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, + req, self.uuid, '20') + + +class TestConsolesXMLSerializer(test.NoDBTestCase): + def test_show(self): + fixture = {'console': {'id': 20, + 'password': 'fake_password', + 'port': 'fake_port', + 'host': 'fake_hostname', + 'console_type': 'fake_type'}} + + output = consoles.ConsoleTemplate().serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, 'console') + self.assertEqual(res_tree.xpath('id')[0].text, '20') + self.assertEqual(res_tree.xpath('port')[0].text, 'fake_port') + self.assertEqual(res_tree.xpath('host')[0].text, 'fake_hostname') + self.assertEqual(res_tree.xpath('password')[0].text, 'fake_password') + self.assertEqual(res_tree.xpath('console_type')[0].text, 'fake_type') + + def test_index(self): + fixture = {'consoles': [{'console': {'id': 10, + 'console_type': 'fake_type'}}, + {'console': {'id': 11, + 'console_type': 'fake_type2'}}]} + + output = consoles.ConsolesTemplate().serialize(fixture) + res_tree = etree.XML(output) + + self.assertEqual(res_tree.tag, 'consoles') + self.assertEqual(len(res_tree), 2) + self.assertEqual(res_tree[0].tag, 'console') + self.assertEqual(res_tree[1].tag, 'console') + self.assertEqual(len(res_tree[0]), 1) + self.assertEqual(res_tree[0][0].tag, 'console') + self.assertEqual(len(res_tree[1]), 1) + self.assertEqual(res_tree[1][0].tag, 'console') + self.assertEqual(res_tree[0][0].xpath('id')[0].text, '10') + self.assertEqual(res_tree[1][0].xpath('id')[0].text, '11') + self.assertEqual(res_tree[0][0].xpath('console_type')[0].text, + 'fake_type') + self.assertEqual(res_tree[1][0].xpath('console_type')[0].text, + 'fake_type2') diff --git a/nova/tests/unit/api/openstack/compute/test_extensions.py b/nova/tests/unit/api/openstack/compute/test_extensions.py new file mode 100644 index 0000000000..cf84fc1f84 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_extensions.py @@ -0,0 +1,747 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2011 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 iso8601 +from lxml import etree +from oslo.config import cfg +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack import compute +from nova.api.openstack.compute import extensions as compute_extensions +from nova.api.openstack import extensions as base_extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import exception +import nova.policy +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import matchers + +CONF = cfg.CONF + +NS = "{http://docs.openstack.org/common/api/v1.0}" +ATOMNS = "{http://www.w3.org/2005/Atom}" +response_body = "Try to say this Mr. Knox, sir..." +extension_body = "I am not a fox!" + + +class StubController(object): + + def __init__(self, body): + self.body = body + + def index(self, req): + return self.body + + def create(self, req, body): + msg = 'All aboard the fail train!' + raise webob.exc.HTTPBadRequest(explanation=msg) + + def show(self, req, id): + raise webob.exc.HTTPNotFound() + + +class StubActionController(wsgi.Controller): + def __init__(self, body): + self.body = body + + @wsgi.action('fooAction') + def _action_foo(self, req, id, body): + return self.body + + +class StubControllerExtension(base_extensions.ExtensionDescriptor): + name = 'twaadle' + + def __init__(self): + pass + + +class StubEarlyExtensionController(wsgi.Controller): + def __init__(self, body): + self.body = body + + @wsgi.extends + def index(self, req): + yield self.body + + @wsgi.extends(action='fooAction') + def _action_foo(self, req, id, body): + yield self.body + + +class StubLateExtensionController(wsgi.Controller): + def __init__(self, body): + self.body = body + + @wsgi.extends + def index(self, req, resp_obj): + return self.body + + @wsgi.extends(action='fooAction') + def _action_foo(self, req, resp_obj, id, body): + return self.body + + +class StubExtensionManager(object): + """Provides access to Tweedle Beetles.""" + + name = "Tweedle Beetle Extension" + alias = "TWDLBETL" + + def __init__(self, resource_ext=None, action_ext=None, request_ext=None, + controller_ext=None): + self.resource_ext = resource_ext + self.action_ext = action_ext + self.request_ext = request_ext + self.controller_ext = controller_ext + self.extra_resource_ext = None + + def get_resources(self): + resource_exts = [] + if self.resource_ext: + resource_exts.append(self.resource_ext) + if self.extra_resource_ext: + resource_exts.append(self.extra_resource_ext) + return resource_exts + + def get_actions(self): + action_exts = [] + if self.action_ext: + action_exts.append(self.action_ext) + return action_exts + + def get_request_extensions(self): + request_extensions = [] + if self.request_ext: + request_extensions.append(self.request_ext) + return request_extensions + + def get_controller_extensions(self): + controller_extensions = [] + if self.controller_ext: + controller_extensions.append(self.controller_ext) + return controller_extensions + + +class ExtensionTestCase(test.TestCase): + def setUp(self): + super(ExtensionTestCase, self).setUp() + ext_list = CONF.osapi_compute_extension[:] + fox = ('nova.tests.unit.api.openstack.compute.extensions.' + 'foxinsocks.Foxinsocks') + if fox not in ext_list: + ext_list.append(fox) + self.flags(osapi_compute_extension=ext_list) + self.fake_context = nova.context.RequestContext('fake', 'fake') + + def test_extension_authorizer_throws_exception_if_policy_fails(self): + target = {'project_id': '1234', + 'user_id': '5678'} + self.mox.StubOutWithMock(nova.policy, 'enforce') + nova.policy.enforce(self.fake_context, + "compute_extension:used_limits_for_admin", + target).AndRaise( + exception.PolicyNotAuthorized( + action="compute_extension:used_limits_for_admin")) + self.mox.ReplayAll() + authorize = base_extensions.extension_authorizer('compute', + 'used_limits_for_admin' + ) + self.assertRaises(exception.PolicyNotAuthorized, authorize, + self.fake_context, target=target) + + def test_core_authorizer_throws_exception_if_policy_fails(self): + target = {'project_id': '1234', + 'user_id': '5678'} + self.mox.StubOutWithMock(nova.policy, 'enforce') + nova.policy.enforce(self.fake_context, + "compute:used_limits_for_admin", + target).AndRaise( + exception.PolicyNotAuthorized( + action="compute:used_limits_for_admin")) + self.mox.ReplayAll() + authorize = base_extensions.core_authorizer('compute', + 'used_limits_for_admin' + ) + self.assertRaises(exception.PolicyNotAuthorized, authorize, + self.fake_context, target=target) + + +class ExtensionControllerTest(ExtensionTestCase): + + def setUp(self): + super(ExtensionControllerTest, self).setUp() + self.ext_list = [ + "AdminActions", + "Aggregates", + "AssistedVolumeSnapshots", + "AvailabilityZone", + "Agents", + "Certificates", + "Cloudpipe", + "CloudpipeUpdate", + "ConsoleOutput", + "Consoles", + "Createserverext", + "DeferredDelete", + "DiskConfig", + "ExtendedAvailabilityZone", + "ExtendedFloatingIps", + "ExtendedIps", + "ExtendedIpsMac", + "ExtendedVIFNet", + "Evacuate", + "ExtendedStatus", + "ExtendedVolumes", + "ExtendedServerAttributes", + "FixedIPs", + "FlavorAccess", + "FlavorDisabled", + "FlavorExtraSpecs", + "FlavorExtraData", + "FlavorManage", + "FlavorRxtx", + "FlavorSwap", + "FloatingIps", + "FloatingIpDns", + "FloatingIpPools", + "FloatingIpsBulk", + "Fox In Socks", + "Hosts", + "ImageSize", + "InstanceActions", + "Keypairs", + "Multinic", + "MultipleCreate", + "QuotaClasses", + "Quotas", + "ExtendedQuotas", + "Rescue", + "SchedulerHints", + "SecurityGroupDefaultRules", + "SecurityGroups", + "ServerDiagnostics", + "ServerListMultiStatus", + "ServerPassword", + "ServerStartStop", + "Services", + "SimpleTenantUsage", + "UsedLimits", + "UserData", + "VirtualInterfaces", + "VolumeAttachmentUpdate", + "Volumes", + ] + self.ext_list.sort() + + def test_list_extensions_json(self): + app = compute.APIRouter(init_only=('extensions',)) + request = webob.Request.blank("/fake/extensions") + response = request.get_response(app) + self.assertEqual(200, response.status_int) + + # Make sure we have all the extensions, extra extensions being OK. + data = jsonutils.loads(response.body) + names = [str(x['name']) for x in data['extensions'] + if str(x['name']) in self.ext_list] + names.sort() + self.assertEqual(names, self.ext_list) + + # Ensure all the timestamps are valid according to iso8601 + for ext in data['extensions']: + iso8601.parse_date(ext['updated']) + + # Make sure that at least Fox in Sox is correct. + (fox_ext, ) = [ + x for x in data['extensions'] if x['alias'] == 'FOXNSOX'] + self.assertEqual(fox_ext, { + 'namespace': 'http://www.fox.in.socks/api/ext/pie/v1.0', + 'name': 'Fox In Socks', + 'updated': '2011-01-22T13:25:27-06:00', + 'description': 'The Fox In Socks Extension.', + 'alias': 'FOXNSOX', + 'links': [] + }, + ) + + for ext in data['extensions']: + url = '/fake/extensions/%s' % ext['alias'] + request = webob.Request.blank(url) + response = request.get_response(app) + output = jsonutils.loads(response.body) + self.assertEqual(output['extension']['alias'], ext['alias']) + + def test_get_extension_json(self): + app = compute.APIRouter(init_only=('extensions',)) + request = webob.Request.blank("/fake/extensions/FOXNSOX") + response = request.get_response(app) + self.assertEqual(200, response.status_int) + + data = jsonutils.loads(response.body) + self.assertEqual(data['extension'], { + "namespace": "http://www.fox.in.socks/api/ext/pie/v1.0", + "name": "Fox In Socks", + "updated": "2011-01-22T13:25:27-06:00", + "description": "The Fox In Socks Extension.", + "alias": "FOXNSOX", + "links": []}) + + def test_get_non_existing_extension_json(self): + app = compute.APIRouter(init_only=('extensions',)) + request = webob.Request.blank("/fake/extensions/4") + response = request.get_response(app) + self.assertEqual(404, response.status_int) + + def test_list_extensions_xml(self): + app = compute.APIRouter(init_only=('servers', 'flavors', 'extensions')) + request = webob.Request.blank("/fake/extensions") + request.accept = "application/xml" + response = request.get_response(app) + self.assertEqual(200, response.status_int) + + root = etree.XML(response.body) + self.assertEqual(root.tag.split('extensions')[0], NS) + + # Make sure we have all the extensions, extras extensions being OK. + exts = root.findall('{0}extension'.format(NS)) + self.assertTrue(len(exts) >= len(self.ext_list)) + + # Make sure that at least Fox in Sox is correct. + (fox_ext, ) = [x for x in exts if x.get('alias') == 'FOXNSOX'] + self.assertEqual(fox_ext.get('name'), 'Fox In Socks') + self.assertEqual(fox_ext.get('namespace'), + 'http://www.fox.in.socks/api/ext/pie/v1.0') + self.assertEqual(fox_ext.get('updated'), '2011-01-22T13:25:27-06:00') + self.assertEqual(fox_ext.findtext('{0}description'.format(NS)), + 'The Fox In Socks Extension.') + + xmlutil.validate_schema(root, 'extensions') + + def test_get_extension_xml(self): + app = compute.APIRouter(init_only=('servers', 'flavors', 'extensions')) + request = webob.Request.blank("/fake/extensions/FOXNSOX") + request.accept = "application/xml" + response = request.get_response(app) + self.assertEqual(200, response.status_int) + xml = response.body + + root = etree.XML(xml) + self.assertEqual(root.tag.split('extension')[0], NS) + self.assertEqual(root.get('alias'), 'FOXNSOX') + self.assertEqual(root.get('name'), 'Fox In Socks') + self.assertEqual(root.get('namespace'), + 'http://www.fox.in.socks/api/ext/pie/v1.0') + self.assertEqual(root.get('updated'), '2011-01-22T13:25:27-06:00') + self.assertEqual(root.findtext('{0}description'.format(NS)), + 'The Fox In Socks Extension.') + + xmlutil.validate_schema(root, 'extension') + + +class ResourceExtensionTest(ExtensionTestCase): + + def test_no_extension_present(self): + manager = StubExtensionManager(None) + app = compute.APIRouter(manager) + request = webob.Request.blank("/blah") + response = request.get_response(app) + self.assertEqual(404, response.status_int) + + def test_get_resources(self): + res_ext = base_extensions.ResourceExtension('tweedles', + StubController(response_body)) + manager = StubExtensionManager(res_ext) + app = compute.APIRouter(manager) + request = webob.Request.blank("/fake/tweedles") + response = request.get_response(app) + self.assertEqual(200, response.status_int) + self.assertEqual(response_body, response.body) + + def test_get_resources_with_controller(self): + res_ext = base_extensions.ResourceExtension('tweedles', + StubController(response_body)) + manager = StubExtensionManager(res_ext) + app = compute.APIRouter(manager) + request = webob.Request.blank("/fake/tweedles") + response = request.get_response(app) + self.assertEqual(200, response.status_int) + self.assertEqual(response_body, response.body) + + def test_bad_request(self): + res_ext = base_extensions.ResourceExtension('tweedles', + StubController(response_body)) + manager = StubExtensionManager(res_ext) + app = compute.APIRouter(manager) + request = webob.Request.blank("/fake/tweedles") + request.method = "POST" + response = request.get_response(app) + self.assertEqual(400, response.status_int) + self.assertEqual('application/json', response.content_type) + body = jsonutils.loads(response.body) + expected = { + "badRequest": { + "message": "All aboard the fail train!", + "code": 400 + } + } + self.assertThat(expected, matchers.DictMatches(body)) + + def test_non_exist_resource(self): + res_ext = base_extensions.ResourceExtension('tweedles', + StubController(response_body)) + manager = StubExtensionManager(res_ext) + app = compute.APIRouter(manager) + request = webob.Request.blank("/fake/tweedles/1") + response = request.get_response(app) + self.assertEqual(404, response.status_int) + self.assertEqual('application/json', response.content_type) + body = jsonutils.loads(response.body) + expected = { + "itemNotFound": { + "message": "The resource could not be found.", + "code": 404 + } + } + self.assertThat(expected, matchers.DictMatches(body)) + + +class InvalidExtension(object): + + alias = "THIRD" + + +class ExtensionManagerTest(ExtensionTestCase): + + response_body = "Try to say this Mr. Knox, sir..." + + def test_get_resources(self): + app = compute.APIRouter() + request = webob.Request.blank("/fake/foxnsocks") + response = request.get_response(app) + self.assertEqual(200, response.status_int) + self.assertEqual(response_body, response.body) + + def test_invalid_extensions(self): + # Don't need the serialization middleware here because we're + # not testing any serialization + compute.APIRouter() + ext_mgr = compute_extensions.ExtensionManager() + ext_mgr.register(InvalidExtension()) + self.assertTrue(ext_mgr.is_loaded('FOXNSOX')) + self.assertFalse(ext_mgr.is_loaded('THIRD')) + + +class ActionExtensionTest(ExtensionTestCase): + + def _send_server_action_request(self, url, body): + app = compute.APIRouter(init_only=('servers',)) + request = webob.Request.blank(url) + request.method = 'POST' + request.content_type = 'application/json' + request.body = jsonutils.dumps(body) + response = request.get_response(app) + return response + + def test_extended_action(self): + body = dict(add_tweedle=dict(name="test")) + url = "/fake/servers/abcd/action" + response = self._send_server_action_request(url, body) + self.assertEqual(200, response.status_int) + self.assertEqual("Tweedle Beetle Added.", response.body) + + body = dict(delete_tweedle=dict(name="test")) + response = self._send_server_action_request(url, body) + self.assertEqual(200, response.status_int) + self.assertEqual("Tweedle Beetle Deleted.", response.body) + + def test_invalid_action(self): + body = dict(blah=dict(name="test")) # Doesn't exist + url = "/fake/servers/abcd/action" + response = self._send_server_action_request(url, body) + self.assertEqual(400, response.status_int) + self.assertEqual('application/json', response.content_type) + body = jsonutils.loads(response.body) + expected = { + "badRequest": { + "message": "There is no such action: blah", + "code": 400 + } + } + self.assertThat(expected, matchers.DictMatches(body)) + + def test_non_exist_action(self): + body = dict(blah=dict(name="test")) + url = "/fake/fdsa/1/action" + response = self._send_server_action_request(url, body) + self.assertEqual(404, response.status_int) + + def test_failed_action(self): + body = dict(fail=dict(name="test")) + url = "/fake/servers/abcd/action" + response = self._send_server_action_request(url, body) + self.assertEqual(400, response.status_int) + self.assertEqual('application/json', response.content_type) + body = jsonutils.loads(response.body) + expected = { + "badRequest": { + "message": "Tweedle fail", + "code": 400 + } + } + self.assertThat(expected, matchers.DictMatches(body)) + + +class RequestExtensionTest(ExtensionTestCase): + + def test_get_resources_with_stub_mgr(self): + class GooGoose(wsgi.Controller): + @wsgi.extends + def show(self, req, resp_obj, id): + # only handle JSON responses + resp_obj.obj['flavor']['googoose'] = req.GET.get('chewing') + + req_ext = base_extensions.ControllerExtension( + StubControllerExtension(), 'flavors', GooGoose()) + + manager = StubExtensionManager(None, None, None, req_ext) + app = fakes.wsgi_app(ext_mgr=manager) + request = webob.Request.blank("/v2/fake/flavors/1?chewing=bluegoo") + request.environ['api.version'] = '2' + response = request.get_response(app) + self.assertEqual(200, response.status_int) + response_data = jsonutils.loads(response.body) + self.assertEqual('bluegoo', response_data['flavor']['googoose']) + + def test_get_resources_with_mgr(self): + + app = fakes.wsgi_app(init_only=('flavors',)) + request = webob.Request.blank("/v2/fake/flavors/1?chewing=newblue") + request.environ['api.version'] = '2' + response = request.get_response(app) + self.assertEqual(200, response.status_int) + response_data = jsonutils.loads(response.body) + self.assertEqual('newblue', response_data['flavor']['googoose']) + self.assertEqual("Pig Bands!", response_data['big_bands']) + + +class ControllerExtensionTest(ExtensionTestCase): + def test_controller_extension_early(self): + controller = StubController(response_body) + res_ext = base_extensions.ResourceExtension('tweedles', controller) + ext_controller = StubEarlyExtensionController(extension_body) + extension = StubControllerExtension() + cont_ext = base_extensions.ControllerExtension(extension, 'tweedles', + ext_controller) + manager = StubExtensionManager(resource_ext=res_ext, + controller_ext=cont_ext) + app = compute.APIRouter(manager) + request = webob.Request.blank("/fake/tweedles") + response = request.get_response(app) + self.assertEqual(200, response.status_int) + self.assertEqual(extension_body, response.body) + + def test_controller_extension_late(self): + # Need a dict for the body to convert to a ResponseObject + controller = StubController(dict(foo=response_body)) + res_ext = base_extensions.ResourceExtension('tweedles', controller) + + ext_controller = StubLateExtensionController(extension_body) + extension = StubControllerExtension() + cont_ext = base_extensions.ControllerExtension(extension, 'tweedles', + ext_controller) + + manager = StubExtensionManager(resource_ext=res_ext, + controller_ext=cont_ext) + app = compute.APIRouter(manager) + request = webob.Request.blank("/fake/tweedles") + response = request.get_response(app) + self.assertEqual(200, response.status_int) + self.assertEqual(extension_body, response.body) + + def test_controller_extension_late_inherited_resource(self): + # Need a dict for the body to convert to a ResponseObject + controller = StubController(dict(foo=response_body)) + parent_ext = base_extensions.ResourceExtension('tweedles', controller) + + ext_controller = StubLateExtensionController(extension_body) + extension = StubControllerExtension() + cont_ext = base_extensions.ControllerExtension(extension, 'tweedles', + ext_controller) + + manager = StubExtensionManager(resource_ext=parent_ext, + controller_ext=cont_ext) + child_ext = base_extensions.ResourceExtension('beetles', controller, + inherits='tweedles') + manager.extra_resource_ext = child_ext + app = compute.APIRouter(manager) + request = webob.Request.blank("/fake/beetles") + response = request.get_response(app) + self.assertEqual(200, response.status_int) + self.assertEqual(extension_body, response.body) + + def test_controller_action_extension_early(self): + controller = StubActionController(response_body) + actions = dict(action='POST') + res_ext = base_extensions.ResourceExtension('tweedles', controller, + member_actions=actions) + ext_controller = StubEarlyExtensionController(extension_body) + extension = StubControllerExtension() + cont_ext = base_extensions.ControllerExtension(extension, 'tweedles', + ext_controller) + manager = StubExtensionManager(resource_ext=res_ext, + controller_ext=cont_ext) + app = compute.APIRouter(manager) + request = webob.Request.blank("/fake/tweedles/foo/action") + request.method = 'POST' + request.headers['Content-Type'] = 'application/json' + request.body = jsonutils.dumps(dict(fooAction=True)) + response = request.get_response(app) + self.assertEqual(200, response.status_int) + self.assertEqual(extension_body, response.body) + + def test_controller_action_extension_late(self): + # Need a dict for the body to convert to a ResponseObject + controller = StubActionController(dict(foo=response_body)) + actions = dict(action='POST') + res_ext = base_extensions.ResourceExtension('tweedles', controller, + member_actions=actions) + + ext_controller = StubLateExtensionController(extension_body) + extension = StubControllerExtension() + cont_ext = base_extensions.ControllerExtension(extension, 'tweedles', + ext_controller) + + manager = StubExtensionManager(resource_ext=res_ext, + controller_ext=cont_ext) + app = compute.APIRouter(manager) + request = webob.Request.blank("/fake/tweedles/foo/action") + request.method = 'POST' + request.headers['Content-Type'] = 'application/json' + request.body = jsonutils.dumps(dict(fooAction=True)) + response = request.get_response(app) + self.assertEqual(200, response.status_int) + self.assertEqual(extension_body, response.body) + + +class ExtensionsXMLSerializerTest(test.TestCase): + + def test_serialize_extension(self): + serializer = base_extensions.ExtensionTemplate() + data = {'extension': { + 'name': 'ext1', + 'namespace': 'http://docs.rack.com/servers/api/ext/pie/v1.0', + 'alias': 'RS-PIE', + 'updated': '2011-01-22T13:25:27-06:00', + 'description': 'Adds the capability to share an image.', + 'links': [{'rel': 'describedby', + 'type': 'application/pdf', + 'href': 'http://docs.rack.com/servers/api/ext/cs.pdf'}, + {'rel': 'describedby', + 'type': 'application/vnd.sun.wadl+xml', + 'href': 'http://docs.rack.com/servers/api/ext/cs.wadl'}]}} + + xml = serializer.serialize(data) + root = etree.XML(xml) + ext_dict = data['extension'] + self.assertEqual(root.findtext('{0}description'.format(NS)), + ext_dict['description']) + + for key in ['name', 'namespace', 'alias', 'updated']: + self.assertEqual(root.get(key), ext_dict[key]) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(ext_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + xmlutil.validate_schema(root, 'extension') + + def test_serialize_extensions(self): + serializer = base_extensions.ExtensionsTemplate() + data = {"extensions": [{ + "name": "Public Image Extension", + "namespace": "http://foo.com/api/ext/pie/v1.0", + "alias": "RS-PIE", + "updated": "2011-01-22T13:25:27-06:00", + "description": "Adds the capability to share an image.", + "links": [{"rel": "describedby", + "type": "application/pdf", + "href": "http://foo.com/api/ext/cs-pie.pdf"}, + {"rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://foo.com/api/ext/cs-pie.wadl"}]}, + {"name": "Cloud Block Storage", + "namespace": "http://foo.com/api/ext/cbs/v1.0", + "alias": "RS-CBS", + "updated": "2011-01-12T11:22:33-06:00", + "description": "Allows mounting cloud block storage.", + "links": [{"rel": "describedby", + "type": "application/pdf", + "href": "http://foo.com/api/ext/cs-cbs.pdf"}, + {"rel": "describedby", + "type": "application/vnd.sun.wadl+xml", + "href": "http://foo.com/api/ext/cs-cbs.wadl"}]}]} + + xml = serializer.serialize(data) + root = etree.XML(xml) + ext_elems = root.findall('{0}extension'.format(NS)) + self.assertEqual(len(ext_elems), 2) + for i, ext_elem in enumerate(ext_elems): + ext_dict = data['extensions'][i] + self.assertEqual(ext_elem.findtext('{0}description'.format(NS)), + ext_dict['description']) + + for key in ['name', 'namespace', 'alias', 'updated']: + self.assertEqual(ext_elem.get(key), ext_dict[key]) + + link_nodes = ext_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(ext_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + xmlutil.validate_schema(root, 'extensions') + + +class ExtensionControllerIdFormatTest(test.TestCase): + + def _bounce_id(self, test_id): + + class BounceController(object): + def show(self, req, id): + return id + res_ext = base_extensions.ResourceExtension('bounce', + BounceController()) + manager = StubExtensionManager(res_ext) + app = compute.APIRouter(manager) + request = webob.Request.blank("/fake/bounce/%s" % test_id) + response = request.get_response(app) + return response.body + + def test_id_with_xml_format(self): + result = self._bounce_id('foo.xml') + self.assertEqual(result, 'foo') + + def test_id_with_json_format(self): + result = self._bounce_id('foo.json') + self.assertEqual(result, 'foo') + + def test_id_with_bad_format(self): + result = self._bounce_id('foo.bad') + self.assertEqual(result, 'foo.bad') diff --git a/nova/tests/unit/api/openstack/compute/test_flavors.py b/nova/tests/unit/api/openstack/compute/test_flavors.py new file mode 100644 index 0000000000..265b50ac85 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_flavors.py @@ -0,0 +1,943 @@ +# 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. + +from lxml import etree +import six.moves.urllib.parse as urlparse +import webob + +from nova.api.openstack import common +from nova.api.openstack.compute import flavors as flavors_v2 +from nova.api.openstack.compute.plugins.v3 import flavors as flavors_v3 +from nova.api.openstack import xmlutil +import nova.compute.flavors +from nova import context +from nova import db +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import matchers + +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" + + +FAKE_FLAVORS = { + 'flavor 1': { + "flavorid": '1', + "name": 'flavor 1', + "memory_mb": '256', + "root_gb": '10', + "ephemeral_gb": '20', + "swap": '10', + "disabled": False, + "vcpus": '', + }, + 'flavor 2': { + "flavorid": '2', + "name": 'flavor 2', + "memory_mb": '512', + "root_gb": '20', + "ephemeral_gb": '10', + "swap": '5', + "disabled": False, + "vcpus": '', + }, +} + + +def fake_flavor_get_by_flavor_id(flavorid, ctxt=None): + return FAKE_FLAVORS['flavor %s' % flavorid] + + +def fake_get_all_flavors_sorted_list(context=None, inactive=False, + filters=None, sort_key='flavorid', + sort_dir='asc', limit=None, marker=None): + if marker in ['99999']: + raise exception.MarkerNotFound(marker) + + def reject_min(db_attr, filter_attr): + return (filter_attr in filters and + int(flavor[db_attr]) < int(filters[filter_attr])) + + filters = filters or {} + res = [] + for (flavor_name, flavor) in FAKE_FLAVORS.items(): + if reject_min('memory_mb', 'min_memory_mb'): + continue + elif reject_min('root_gb', 'min_root_gb'): + continue + + res.append(flavor) + + res = sorted(res, key=lambda item: item[sort_key]) + output = [] + marker_found = True if marker is None else False + for flavor in res: + if not marker_found and marker == flavor['flavorid']: + marker_found = True + elif marker_found: + if limit is None or len(output) < int(limit): + output.append(flavor) + + return output + + +def fake_get_limit_and_marker(request, max_limit=1): + params = common.get_pagination_params(request) + limit = params.get('limit', max_limit) + limit = min(max_limit, limit) + marker = params.get('marker') + + return limit, marker + + +def empty_get_all_flavors_sorted_list(context=None, inactive=False, + filters=None, sort_key='flavorid', + sort_dir='asc', limit=None, marker=None): + return [] + + +def return_flavor_not_found(flavor_id, ctxt=None): + raise exception.FlavorNotFound(flavor_id=flavor_id) + + +class FlavorsTestV21(test.TestCase): + _prefix = "/v3" + Controller = flavors_v3.FlavorsController + fake_request = fakes.HTTPRequestV3 + _rspv = "v3" + _fake = "" + + def setUp(self): + super(FlavorsTestV21, self).setUp() + self.flags(osapi_compute_extension=[]) + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + self.stubs.Set(nova.compute.flavors, "get_all_flavors_sorted_list", + fake_get_all_flavors_sorted_list) + self.stubs.Set(nova.compute.flavors, + "get_flavor_by_flavor_id", + fake_flavor_get_by_flavor_id) + self.controller = self.Controller() + + def _set_expected_body(self, expected, ephemeral, swap, disabled): + # NOTE(oomichi): On v2.1 API, some extensions of v2.0 are merged + # as core features and we can get the following parameters as the + # default. + expected['OS-FLV-EXT-DATA:ephemeral'] = ephemeral + expected['OS-FLV-DISABLED:disabled'] = disabled + expected['swap'] = swap + + def test_get_flavor_by_invalid_id(self): + self.stubs.Set(nova.compute.flavors, + "get_flavor_by_flavor_id", + return_flavor_not_found) + req = self.fake_request.blank(self._prefix + '/flavors/asdf') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, 'asdf') + + def test_get_flavor_by_id(self): + req = self.fake_request.blank(self._prefix + '/flavors/1') + flavor = self.controller.show(req, '1') + expected = { + "flavor": { + "id": "1", + "name": "flavor 1", + "ram": "256", + "disk": "10", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/" + self._rspv + + "/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost" + self._fake + + "/flavors/1", + }, + ], + }, + } + self._set_expected_body(expected['flavor'], ephemeral='20', + swap='10', disabled=False) + self.assertEqual(flavor, expected) + + def test_get_flavor_with_custom_link_prefix(self): + self.flags(osapi_compute_link_prefix='http://zoo.com:42', + osapi_glance_link_prefix='http://circus.com:34') + req = self.fake_request.blank(self._prefix + '/flavors/1') + flavor = self.controller.show(req, '1') + expected = { + "flavor": { + "id": "1", + "name": "flavor 1", + "ram": "256", + "disk": "10", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://zoo.com:42/" + self._rspv + + "/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://zoo.com:42" + self._fake + + "/flavors/1", + }, + ], + }, + } + self._set_expected_body(expected['flavor'], ephemeral='20', + swap='10', disabled=False) + self.assertEqual(expected, flavor) + + def test_get_flavor_list(self): + req = self.fake_request.blank(self._prefix + '/flavors') + flavor = self.controller.index(req) + expected = { + "flavors": [ + { + "id": "1", + "name": "flavor 1", + "links": [ + { + "rel": "self", + "href": "http://localhost/" + self._rspv + + "/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost" + self._fake + + "/flavors/1", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "links": [ + { + "rel": "self", + "href": "http://localhost/" + self._rspv + + "/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost" + self._fake + + "/flavors/2", + }, + ], + }, + ], + } + self.assertEqual(flavor, expected) + + def test_get_flavor_list_with_marker(self): + self.maxDiff = None + url = self._prefix + '/flavors?limit=1&marker=1' + req = self.fake_request.blank(url) + flavor = self.controller.index(req) + expected = { + "flavors": [ + { + "id": "2", + "name": "flavor 2", + "links": [ + { + "rel": "self", + "href": "http://localhost/" + self._rspv + + "/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost" + self._fake + + "/flavors/2", + }, + ], + }, + ], + 'flavors_links': [ + {'href': 'http://localhost/' + self._rspv + + '/flavors?limit=1&marker=2', + 'rel': 'next'} + ] + } + self.assertThat(flavor, matchers.DictMatches(expected)) + + def test_get_flavor_list_with_invalid_marker(self): + req = self.fake_request.blank(self._prefix + '/flavors?marker=99999') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_get_flavor_detail_with_limit(self): + url = self._prefix + '/flavors/detail?limit=1' + req = self.fake_request.blank(url) + response = self.controller.index(req) + response_list = response["flavors"] + response_links = response["flavors_links"] + + expected_flavors = [ + { + "id": "1", + "name": "flavor 1", + "links": [ + { + "rel": "self", + "href": "http://localhost/" + self._rspv + + "/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost" + self._fake + + "/flavors/1", + }, + ], + }, + ] + self.assertEqual(response_list, expected_flavors) + self.assertEqual(response_links[0]['rel'], 'next') + + href_parts = urlparse.urlparse(response_links[0]['href']) + self.assertEqual('/' + self._rspv + '/flavors', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + self.assertThat({'limit': ['1'], 'marker': ['1']}, + matchers.DictMatches(params)) + + def test_get_flavor_with_limit(self): + req = self.fake_request.blank(self._prefix + '/flavors?limit=2') + response = self.controller.index(req) + response_list = response["flavors"] + response_links = response["flavors_links"] + + expected_flavors = [ + { + "id": "1", + "name": "flavor 1", + "links": [ + { + "rel": "self", + "href": "http://localhost/" + self._rspv + + "/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost" + self._fake + + "/flavors/1", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "links": [ + { + "rel": "self", + "href": "http://localhost/" + self._rspv + + "/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost" + self._fake + + "/flavors/2", + }, + ], + } + ] + self.assertEqual(response_list, expected_flavors) + self.assertEqual(response_links[0]['rel'], 'next') + + href_parts = urlparse.urlparse(response_links[0]['href']) + self.assertEqual('/' + self._rspv + '/flavors', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + self.assertThat({'limit': ['2'], 'marker': ['2']}, + matchers.DictMatches(params)) + + def test_get_flavor_with_default_limit(self): + self.stubs.Set(common, "get_limit_and_marker", + fake_get_limit_and_marker) + self.flags(osapi_max_limit=1) + req = fakes.HTTPRequest.blank('/v2/fake/flavors?limit=2') + response = self.controller.index(req) + response_list = response["flavors"] + response_links = response["flavors_links"] + + expected_flavors = [ + { + "id": "1", + "name": "flavor 1", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/1", + } + ] + } + ] + + self.assertEqual(response_list, expected_flavors) + self.assertEqual(response_links[0]['rel'], 'next') + href_parts = urlparse.urlparse(response_links[0]['href']) + self.assertEqual('/v2/fake/flavors', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + self.assertThat({'limit': ['2'], 'marker': ['1']}, + matchers.DictMatches(params)) + + def test_get_flavor_list_detail(self): + req = self.fake_request.blank(self._prefix + '/flavors/detail') + flavor = self.controller.detail(req) + expected = { + "flavors": [ + { + "id": "1", + "name": "flavor 1", + "ram": "256", + "disk": "10", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/" + self._rspv + + "/flavors/1", + }, + { + "rel": "bookmark", + "href": "http://localhost" + self._fake + + "/flavors/1", + }, + ], + }, + { + "id": "2", + "name": "flavor 2", + "ram": "512", + "disk": "20", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/" + self._rspv + + "/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost" + self._fake + + "/flavors/2", + }, + ], + }, + ], + } + self._set_expected_body(expected['flavors'][0], ephemeral='20', + swap='10', disabled=False) + self._set_expected_body(expected['flavors'][1], ephemeral='10', + swap='5', disabled=False) + self.assertEqual(expected, flavor) + + def test_get_empty_flavor_list(self): + self.stubs.Set(nova.compute.flavors, "get_all_flavors_sorted_list", + empty_get_all_flavors_sorted_list) + + req = self.fake_request.blank(self._prefix + '/flavors') + flavors = self.controller.index(req) + expected = {'flavors': []} + self.assertEqual(flavors, expected) + + def test_get_flavor_list_filter_min_ram(self): + # Flavor lists may be filtered by minRam. + req = self.fake_request.blank(self._prefix + '/flavors?minRam=512') + flavor = self.controller.index(req) + expected = { + "flavors": [ + { + "id": "2", + "name": "flavor 2", + "links": [ + { + "rel": "self", + "href": "http://localhost/" + self._rspv + + "/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost" + self._fake + + "/flavors/2", + }, + ], + }, + ], + } + self.assertEqual(flavor, expected) + + def test_get_flavor_list_filter_invalid_min_ram(self): + # Ensure you cannot list flavors with invalid minRam param. + req = self.fake_request.blank(self._prefix + '/flavors?minRam=NaN') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_get_flavor_list_filter_min_disk(self): + # Flavor lists may be filtered by minDisk. + req = self.fake_request.blank(self._prefix + '/flavors?minDisk=20') + flavor = self.controller.index(req) + expected = { + "flavors": [ + { + "id": "2", + "name": "flavor 2", + "links": [ + { + "rel": "self", + "href": "http://localhost/" + self._rspv + + "/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost" + self._fake + + "/flavors/2", + }, + ], + }, + ], + } + self.assertEqual(flavor, expected) + + def test_get_flavor_list_filter_invalid_min_disk(self): + # Ensure you cannot list flavors with invalid minDisk param. + req = self.fake_request.blank(self._prefix + '/flavors?minDisk=NaN') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_get_flavor_list_detail_min_ram_and_min_disk(self): + """Tests that filtering work on flavor details and that minRam and + minDisk filters can be combined + """ + req = self.fake_request.blank(self._prefix + '/flavors/detail' + '?minRam=256&minDisk=20') + flavor = self.controller.detail(req) + expected = { + "flavors": [ + { + "id": "2", + "name": "flavor 2", + "ram": "512", + "disk": "20", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/" + self._rspv + + "/flavors/2", + }, + { + "rel": "bookmark", + "href": "http://localhost" + self._fake + + "/flavors/2", + }, + ], + }, + ], + } + self._set_expected_body(expected['flavors'][0], ephemeral='10', + swap='5', disabled=False) + self.assertEqual(expected, flavor) + + +class FlavorsTestV20(FlavorsTestV21): + _prefix = "/v2/fake" + Controller = flavors_v2.Controller + fake_request = fakes.HTTPRequest + _rspv = "v2/fake" + _fake = "/fake" + + def _set_expected_body(self, expected, ephemeral, swap, disabled): + pass + + +class FlavorsXMLSerializationTest(test.TestCase): + + def test_xml_declaration(self): + serializer = flavors_v2.FlavorTemplate() + + fixture = { + "flavor": { + "id": "12", + "name": "asdf", + "ram": "256", + "disk": "10", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/12", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/12", + }, + ], + }, + } + + output = serializer.serialize(fixture) + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) + + def test_show(self): + serializer = flavors_v2.FlavorTemplate() + + fixture = { + "flavor": { + "id": "12", + "name": "asdf", + "ram": "256", + "disk": "10", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/12", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/12", + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'flavor') + flavor_dict = fixture['flavor'] + + for key in ['name', 'id', 'ram', 'disk']: + self.assertEqual(root.get(key), str(flavor_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(flavor_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_show_handles_integers(self): + serializer = flavors_v2.FlavorTemplate() + + fixture = { + "flavor": { + "id": 12, + "name": "asdf", + "ram": 256, + "disk": 10, + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/12", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/12", + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'flavor') + flavor_dict = fixture['flavor'] + + for key in ['name', 'id', 'ram', 'disk']: + self.assertEqual(root.get(key), str(flavor_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(flavor_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_detail(self): + serializer = flavors_v2.FlavorsTemplate() + + fixture = { + "flavors": [ + { + "id": "23", + "name": "flavor 23", + "ram": "512", + "disk": "20", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/23", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/23", + }, + ], + }, + { + "id": "13", + "name": "flavor 13", + "ram": "256", + "disk": "10", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/13", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/13", + }, + ], + }, + ], + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'flavors') + flavor_elems = root.findall('{0}flavor'.format(NS)) + self.assertEqual(len(flavor_elems), 2) + for i, flavor_elem in enumerate(flavor_elems): + flavor_dict = fixture['flavors'][i] + + for key in ['name', 'id', 'ram', 'disk']: + self.assertEqual(flavor_elem.get(key), str(flavor_dict[key])) + + link_nodes = flavor_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(flavor_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_index(self): + serializer = flavors_v2.MinimalFlavorsTemplate() + + fixture = { + "flavors": [ + { + "id": "23", + "name": "flavor 23", + "ram": "512", + "disk": "20", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/23", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/23", + }, + ], + }, + { + "id": "13", + "name": "flavor 13", + "ram": "256", + "disk": "10", + "vcpus": "", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/flavors/13", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/flavors/13", + }, + ], + }, + ], + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'flavors') + flavor_elems = root.findall('{0}flavor'.format(NS)) + self.assertEqual(len(flavor_elems), 2) + for i, flavor_elem in enumerate(flavor_elems): + flavor_dict = fixture['flavors'][i] + + for key in ['name', 'id']: + self.assertEqual(flavor_elem.get(key), str(flavor_dict[key])) + + link_nodes = flavor_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(flavor_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_index_empty(self): + serializer = flavors_v2.MinimalFlavorsTemplate() + + fixture = { + "flavors": [], + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'flavors') + flavor_elems = root.findall('{0}flavor'.format(NS)) + self.assertEqual(len(flavor_elems), 0) + + +class DisabledFlavorsWithRealDBTestV21(test.TestCase): + """Tests that disabled flavors should not be shown nor listed.""" + Controller = flavors_v3.FlavorsController + _prefix = "/v3" + fake_request = fakes.HTTPRequestV3 + + def setUp(self): + super(DisabledFlavorsWithRealDBTestV21, self).setUp() + + # Add a new disabled type to the list of flavors + self.req = self.fake_request.blank(self._prefix + '/flavors') + self.context = self.req.environ['nova.context'] + self.admin_context = context.get_admin_context() + + self.disabled_type = self._create_disabled_instance_type() + self.inst_types = db.flavor_get_all( + self.admin_context) + self.controller = self.Controller() + + def tearDown(self): + db.flavor_destroy( + self.admin_context, self.disabled_type['name']) + + super(DisabledFlavorsWithRealDBTestV21, self).tearDown() + + def _create_disabled_instance_type(self): + inst_types = db.flavor_get_all(self.admin_context) + + inst_type = inst_types[0] + + del inst_type['id'] + inst_type['name'] += '.disabled' + inst_type['flavorid'] = unicode(max( + [int(flavor['flavorid']) for flavor in inst_types]) + 1) + inst_type['disabled'] = True + + disabled_type = db.flavor_create( + self.admin_context, inst_type) + + return disabled_type + + def test_index_should_not_list_disabled_flavors_to_user(self): + self.context.is_admin = False + + flavor_list = self.controller.index(self.req)['flavors'] + api_flavorids = set(f['id'] for f in flavor_list) + + db_flavorids = set(i['flavorid'] for i in self.inst_types) + disabled_flavorid = str(self.disabled_type['flavorid']) + + self.assertIn(disabled_flavorid, db_flavorids) + self.assertEqual(db_flavorids - set([disabled_flavorid]), + api_flavorids) + + def test_index_should_list_disabled_flavors_to_admin(self): + self.context.is_admin = True + + flavor_list = self.controller.index(self.req)['flavors'] + api_flavorids = set(f['id'] for f in flavor_list) + + db_flavorids = set(i['flavorid'] for i in self.inst_types) + disabled_flavorid = str(self.disabled_type['flavorid']) + + self.assertIn(disabled_flavorid, db_flavorids) + self.assertEqual(db_flavorids, api_flavorids) + + def test_show_should_include_disabled_flavor_for_user(self): + """Counterintuitively we should show disabled flavors to all users and + not just admins. The reason is that, when a user performs a server-show + request, we want to be able to display the pretty flavor name ('512 MB + Instance') and not just the flavor-id even if the flavor id has been + marked disabled. + """ + self.context.is_admin = False + + flavor = self.controller.show( + self.req, self.disabled_type['flavorid'])['flavor'] + + self.assertEqual(flavor['name'], self.disabled_type['name']) + + def test_show_should_include_disabled_flavor_for_admin(self): + self.context.is_admin = True + + flavor = self.controller.show( + self.req, self.disabled_type['flavorid'])['flavor'] + + self.assertEqual(flavor['name'], self.disabled_type['name']) + + +class DisabledFlavorsWithRealDBTestV20(DisabledFlavorsWithRealDBTestV21): + """Tests that disabled flavors should not be shown nor listed.""" + Controller = flavors_v2.Controller + _prefix = "/v2/fake" + fake_request = fakes.HTTPRequest + + +class ParseIsPublicTestV21(test.TestCase): + Controller = flavors_v3.FlavorsController + + def setUp(self): + super(ParseIsPublicTestV21, self).setUp() + self.controller = self.Controller() + + def assertPublic(self, expected, is_public): + self.assertIs(expected, self.controller._parse_is_public(is_public), + '%s did not return %s' % (is_public, expected)) + + def test_None(self): + self.assertPublic(True, None) + + def test_truthy(self): + self.assertPublic(True, True) + self.assertPublic(True, 't') + self.assertPublic(True, 'true') + self.assertPublic(True, 'yes') + self.assertPublic(True, '1') + + def test_falsey(self): + self.assertPublic(False, False) + self.assertPublic(False, 'f') + self.assertPublic(False, 'false') + self.assertPublic(False, 'no') + self.assertPublic(False, '0') + + def test_string_none(self): + self.assertPublic(None, 'none') + self.assertPublic(None, 'None') + + def test_other(self): + self.assertRaises( + webob.exc.HTTPBadRequest, self.assertPublic, None, 'other') + + +class ParseIsPublicTestV20(ParseIsPublicTestV21): + Controller = flavors_v2.Controller diff --git a/nova/tests/unit/api/openstack/compute/test_image_metadata.py b/nova/tests/unit/api/openstack/compute/test_image_metadata.py new file mode 100644 index 0000000000..6de8ddf6f6 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_image_metadata.py @@ -0,0 +1,366 @@ +# Copyright 2011 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 copy + +import mock +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute import image_metadata +from nova.api.openstack.compute.plugins.v3 import image_metadata \ + as image_metadata_v21 +from nova import exception +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import image_fixtures + +IMAGE_FIXTURES = image_fixtures.get_image_fixtures() +CHK_QUOTA_STR = 'nova.api.openstack.common.check_img_metadata_properties_quota' + + +def get_image_123(): + return copy.deepcopy(IMAGE_FIXTURES)[0] + + +class ImageMetaDataTestV21(test.NoDBTestCase): + controller_class = image_metadata_v21.ImageMetadataController + + def setUp(self): + super(ImageMetaDataTestV21, self).setUp() + self.controller = self.controller_class() + + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_index(self, get_all_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata') + res_dict = self.controller.index(req, '123') + expected = {'metadata': {'key1': 'value1'}} + self.assertEqual(res_dict, expected) + get_all_mocked.assert_called_once_with(mock.ANY, '123') + + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_show(self, get_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') + res_dict = self.controller.show(req, '123', 'key1') + self.assertIn('meta', res_dict) + self.assertEqual(len(res_dict['meta']), 1) + self.assertEqual('value1', res_dict['meta']['key1']) + get_mocked.assert_called_once_with(mock.ANY, '123') + + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_show_not_found(self, _get_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key9') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, '123', 'key9') + + @mock.patch('nova.image.api.API.get', + side_effect=exception.ImageNotFound(image_id='100')) + def test_show_image_not_found(self, _get_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata/key1') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, '100', 'key9') + + @mock.patch(CHK_QUOTA_STR) + @mock.patch('nova.image.api.API.update') + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_create(self, get_mocked, update_mocked, quota_mocked): + mock_result = copy.deepcopy(get_image_123()) + mock_result['properties']['key7'] = 'value7' + update_mocked.return_value = mock_result + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata') + req.method = 'POST' + body = {"metadata": {"key7": "value7"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.create(req, '123', body) + get_mocked.assert_called_once_with(mock.ANY, '123') + expected = copy.deepcopy(get_image_123()) + expected['properties'] = { + 'key1': 'value1', # existing meta + 'key7': 'value7' # new meta + } + quota_mocked.assert_called_once_with(mock.ANY, expected["properties"]) + update_mocked.assert_called_once_with(mock.ANY, '123', expected, + data=None, purge_props=True) + + expected_output = {'metadata': {'key1': 'value1', 'key7': 'value7'}} + self.assertEqual(expected_output, res) + + @mock.patch(CHK_QUOTA_STR) + @mock.patch('nova.image.api.API.update') + @mock.patch('nova.image.api.API.get', + side_effect=exception.ImageNotFound(image_id='100')) + def test_create_image_not_found(self, _get_mocked, update_mocked, + quota_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata') + req.method = 'POST' + body = {"metadata": {"key7": "value7"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.create, req, '100', body) + self.assertFalse(quota_mocked.called) + self.assertFalse(update_mocked.called) + + @mock.patch(CHK_QUOTA_STR) + @mock.patch('nova.image.api.API.update') + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_update_all(self, get_mocked, update_mocked, quota_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata') + req.method = 'PUT' + body = {"metadata": {"key9": "value9"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.update_all(req, '123', body) + get_mocked.assert_called_once_with(mock.ANY, '123') + expected = copy.deepcopy(get_image_123()) + expected['properties'] = { + 'key9': 'value9' # replace meta + } + quota_mocked.assert_called_once_with(mock.ANY, expected["properties"]) + update_mocked.assert_called_once_with(mock.ANY, '123', expected, + data=None, purge_props=True) + + expected_output = {'metadata': {'key9': 'value9'}} + self.assertEqual(expected_output, res) + + @mock.patch(CHK_QUOTA_STR) + @mock.patch('nova.image.api.API.get', + side_effect=exception.ImageNotFound(image_id='100')) + def test_update_all_image_not_found(self, _get_mocked, quota_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata') + req.method = 'PUT' + body = {"metadata": {"key9": "value9"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update_all, req, '100', body) + self.assertFalse(quota_mocked.called) + + @mock.patch(CHK_QUOTA_STR) + @mock.patch('nova.image.api.API.update') + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_update_item(self, _get_mocked, update_mocked, quota_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') + req.method = 'PUT' + body = {"meta": {"key1": "zz"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + res = self.controller.update(req, '123', 'key1', body) + expected = copy.deepcopy(get_image_123()) + expected['properties'] = { + 'key1': 'zz' # changed meta + } + quota_mocked.assert_called_once_with(mock.ANY, expected["properties"]) + update_mocked.assert_called_once_with(mock.ANY, '123', expected, + data=None, purge_props=True) + + expected_output = {'meta': {'key1': 'zz'}} + self.assertEqual(res, expected_output) + + @mock.patch(CHK_QUOTA_STR) + @mock.patch('nova.image.api.API.get', + side_effect=exception.ImageNotFound(image_id='100')) + def test_update_item_image_not_found(self, _get_mocked, quota_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata/key1') + req.method = 'PUT' + body = {"meta": {"key1": "zz"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, req, '100', 'key1', body) + self.assertFalse(quota_mocked.called) + + @mock.patch(CHK_QUOTA_STR) + @mock.patch('nova.image.api.API.update') + @mock.patch('nova.image.api.API.get') + def test_update_item_bad_body(self, get_mocked, update_mocked, + quota_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') + req.method = 'PUT' + body = {"key1": "zz"} + req.body = '' + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, '123', 'key1', body) + self.assertFalse(get_mocked.called) + self.assertFalse(quota_mocked.called) + self.assertFalse(update_mocked.called) + + @mock.patch(CHK_QUOTA_STR, + side_effect=webob.exc.HTTPRequestEntityTooLarge( + explanation='', headers={'Retry-After': 0})) + @mock.patch('nova.image.api.API.update') + @mock.patch('nova.image.api.API.get') + def test_update_item_too_many_keys(self, get_mocked, update_mocked, + _quota_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') + req.method = 'PUT' + body = {"metadata": {"foo": "bar"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, '123', 'key1', body) + self.assertFalse(get_mocked.called) + self.assertFalse(update_mocked.called) + + @mock.patch(CHK_QUOTA_STR) + @mock.patch('nova.image.api.API.update') + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_update_item_body_uri_mismatch(self, _get_mocked, update_mocked, + quota_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/bad') + req.method = 'PUT' + body = {"meta": {"key1": "value1"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, '123', 'bad', body) + self.assertFalse(quota_mocked.called) + self.assertFalse(update_mocked.called) + + @mock.patch('nova.image.api.API.update') + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_delete(self, _get_mocked, update_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') + req.method = 'DELETE' + res = self.controller.delete(req, '123', 'key1') + expected = copy.deepcopy(get_image_123()) + expected['properties'] = {} + update_mocked.assert_called_once_with(mock.ANY, '123', expected, + data=None, purge_props=True) + + self.assertIsNone(res) + + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_delete_not_found(self, _get_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/blah') + req.method = 'DELETE' + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, req, '123', 'blah') + + @mock.patch('nova.image.api.API.get', + side_effect=exception.ImageNotFound(image_id='100')) + def test_delete_image_not_found(self, _get_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/100/metadata/key1') + req.method = 'DELETE' + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, req, '100', 'key1') + + @mock.patch(CHK_QUOTA_STR, + side_effect=webob.exc.HTTPForbidden( + explanation='', headers={'Retry-After': 0})) + @mock.patch('nova.image.api.API.update') + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_too_many_metadata_items_on_create(self, _get_mocked, + update_mocked, _quota_mocked): + body = {"metadata": {"foo": "bar"}} + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata') + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.create, req, '123', body) + self.assertFalse(update_mocked.called) + + @mock.patch(CHK_QUOTA_STR, + side_effect=webob.exc.HTTPForbidden( + explanation='', headers={'Retry-After': 0})) + @mock.patch('nova.image.api.API.update') + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_too_many_metadata_items_on_put(self, _get_mocked, + update_mocked, _quota_mocked): + body = {"metadata": {"foo": "bar"}} + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/blah') + req.method = 'PUT' + body = {"meta": {"blah": "blah"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.update, req, '123', 'blah', body) + self.assertFalse(update_mocked.called) + + @mock.patch('nova.image.api.API.get', + side_effect=exception.ImageNotAuthorized(image_id='123')) + def test_image_not_authorized_update(self, _get_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') + req.method = 'PUT' + body = {"meta": {"key1": "value1"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.update, req, '123', 'key1', body) + + @mock.patch('nova.image.api.API.get', + side_effect=exception.ImageNotAuthorized(image_id='123')) + def test_image_not_authorized_update_all(self, _get_mocked): + image_id = 131 + # see nova.tests.unit.api.openstack.fakes:_make_image_fixtures + + req = fakes.HTTPRequest.blank('/v2/fake/images/%s/metadata/key1' + % image_id) + req.method = 'PUT' + body = {"meta": {"key1": "value1"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.update_all, req, image_id, body) + + @mock.patch('nova.image.api.API.get', + side_effect=exception.ImageNotAuthorized(image_id='123')) + def test_image_not_authorized_create(self, _get_mocked): + image_id = 131 + # see nova.tests.unit.api.openstack.fakes:_make_image_fixtures + + req = fakes.HTTPRequest.blank('/v2/fake/images/%s/metadata/key1' + % image_id) + req.method = 'POST' + body = {"meta": {"key1": "value1"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.create, req, image_id, body) + + +class ImageMetaDataTestV2(ImageMetaDataTestV21): + controller_class = image_metadata.Controller + + # NOTE(cyeoh): This duplicate unittest is necessary for a race condition + # with the V21 unittests. It's mock issue. + @mock.patch('nova.image.api.API.update') + @mock.patch('nova.image.api.API.get', return_value=get_image_123()) + def test_delete(self, _get_mocked, update_mocked): + req = fakes.HTTPRequest.blank('/v2/fake/images/123/metadata/key1') + req.method = 'DELETE' + res = self.controller.delete(req, '123', 'key1') + expected = copy.deepcopy(get_image_123()) + expected['properties'] = {} + update_mocked.assert_called_once_with(mock.ANY, '123', expected, + data=None, purge_props=True) + + self.assertIsNone(res) diff --git a/nova/tests/unit/api/openstack/compute/test_images.py b/nova/tests/unit/api/openstack/compute/test_images.py new file mode 100644 index 0000000000..ad55f9a86e --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_images.py @@ -0,0 +1,1046 @@ +# Copyright 2010 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. + +""" +Tests of the new image services, both as a service layer, +and as a WSGI layer +""" + +import copy + +from lxml import etree +import mock +import webob + +from nova.api.openstack.compute import images +from nova.api.openstack.compute.plugins.v3 import images as images_v21 +from nova.api.openstack.compute.views import images as images_view +from nova.api.openstack import xmlutil +from nova import exception +from nova.image import glance +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import image_fixtures +from nova.tests.unit import matchers + +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" +NOW_API_FORMAT = "2010-10-11T10:30:22Z" +IMAGE_FIXTURES = image_fixtures.get_image_fixtures() + + +class ImagesControllerTestV21(test.NoDBTestCase): + """Test of the OpenStack API /images application controller w/Glance. + """ + image_controller_class = images_v21.ImagesController + url_base = '/v3' + bookmark_base = '' + http_request = fakes.HTTPRequestV3 + + def setUp(self): + """Run before each test.""" + super(ImagesControllerTestV21, self).setUp() + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + fakes.stub_out_compute_api_snapshot(self.stubs) + fakes.stub_out_compute_api_backup(self.stubs) + + self.controller = self.image_controller_class() + self.url_prefix = "http://localhost%s/images" % self.url_base + self.bookmark_prefix = "http://localhost%s/images" % self.bookmark_base + self.uuid = 'fa95aaf5-ab3b-4cd8-88c0-2be7dd051aaf' + self.server_uuid = "aa640691-d1a7-4a67-9d3c-d35ee6b3cc74" + self.server_href = ( + "http://localhost%s/servers/%s" % (self.url_base, + self.server_uuid)) + self.server_bookmark = ( + "http://localhost%s/servers/%s" % (self.bookmark_base, + self.server_uuid)) + self.alternate = "%s/images/%s" + + self.expected_image_123 = { + "image": {'id': '123', + 'name': 'public image', + 'metadata': {'key1': 'value1'}, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ACTIVE', + 'minDisk': 10, + 'progress': 100, + 'minRam': 128, + "links": [{ + "rel": "self", + "href": "%s/123" % self.url_prefix + }, + { + "rel": "bookmark", + "href": + "%s/123" % self.bookmark_prefix + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": self.alternate % + (glance.generate_glance_url(), + 123), + }], + }, + } + + self.expected_image_124 = { + "image": {'id': '124', + 'name': 'queued snapshot', + 'metadata': { + u'instance_uuid': self.server_uuid, + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'SAVING', + 'progress': 25, + 'minDisk': 0, + 'minRam': 0, + 'server': { + 'id': self.server_uuid, + "links": [{ + "rel": "self", + "href": self.server_href, + }, + { + "rel": "bookmark", + "href": self.server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "%s/124" % self.url_prefix + }, + { + "rel": "bookmark", + "href": + "%s/124" % self.bookmark_prefix + }, + { + "rel": "alternate", + "type": + "application/vnd.openstack.image", + "href": self.alternate % + (glance.generate_glance_url(), + 124), + }], + }, + } + + @mock.patch('nova.image.api.API.get', return_value=IMAGE_FIXTURES[0]) + def test_get_image(self, get_mocked): + request = self.http_request.blank(self.url_base + 'images/123') + actual_image = self.controller.show(request, '123') + self.assertThat(actual_image, + matchers.DictMatches(self.expected_image_123)) + get_mocked.assert_called_once_with(mock.ANY, '123') + + @mock.patch('nova.image.api.API.get', return_value=IMAGE_FIXTURES[1]) + def test_get_image_with_custom_prefix(self, _get_mocked): + self.flags(osapi_compute_link_prefix='https://zoo.com:42', + osapi_glance_link_prefix='http://circus.com:34') + fake_req = self.http_request.blank(self.url_base + 'images/124') + actual_image = self.controller.show(fake_req, '124') + + expected_image = self.expected_image_124 + expected_image["image"]["links"][0]["href"] = ( + "https://zoo.com:42%s/images/124" % self.url_base) + expected_image["image"]["links"][1]["href"] = ( + "https://zoo.com:42%s/images/124" % self.bookmark_base) + expected_image["image"]["links"][2]["href"] = ( + "http://circus.com:34/images/124") + expected_image["image"]["server"]["links"][0]["href"] = ( + "https://zoo.com:42%s/servers/%s" % (self.url_base, + self.server_uuid)) + expected_image["image"]["server"]["links"][1]["href"] = ( + "https://zoo.com:42%s/servers/%s" % (self.bookmark_base, + self.server_uuid)) + + self.assertThat(actual_image, matchers.DictMatches(expected_image)) + + @mock.patch('nova.image.api.API.get', side_effect=exception.NotFound) + def test_get_image_404(self, _get_mocked): + fake_req = self.http_request.blank(self.url_base + 'images/unknown') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, fake_req, 'unknown') + + @mock.patch('nova.image.api.API.get_all', return_value=IMAGE_FIXTURES) + def test_get_image_details(self, get_all_mocked): + request = self.http_request.blank(self.url_base + 'images/detail') + response = self.controller.detail(request) + + get_all_mocked.assert_called_once_with(mock.ANY, filters={}) + response_list = response["images"] + + image_125 = copy.deepcopy(self.expected_image_124["image"]) + image_125['id'] = '125' + image_125['name'] = 'saving snapshot' + image_125['progress'] = 50 + image_125["links"][0]["href"] = "%s/125" % self.url_prefix + image_125["links"][1]["href"] = "%s/125" % self.bookmark_prefix + image_125["links"][2]["href"] = ( + "%s/images/125" % glance.generate_glance_url()) + + image_126 = copy.deepcopy(self.expected_image_124["image"]) + image_126['id'] = '126' + image_126['name'] = 'active snapshot' + image_126['status'] = 'ACTIVE' + image_126['progress'] = 100 + image_126["links"][0]["href"] = "%s/126" % self.url_prefix + image_126["links"][1]["href"] = "%s/126" % self.bookmark_prefix + image_126["links"][2]["href"] = ( + "%s/images/126" % glance.generate_glance_url()) + + image_127 = copy.deepcopy(self.expected_image_124["image"]) + image_127['id'] = '127' + image_127['name'] = 'killed snapshot' + image_127['status'] = 'ERROR' + image_127['progress'] = 0 + image_127["links"][0]["href"] = "%s/127" % self.url_prefix + image_127["links"][1]["href"] = "%s/127" % self.bookmark_prefix + image_127["links"][2]["href"] = ( + "%s/images/127" % glance.generate_glance_url()) + + image_128 = copy.deepcopy(self.expected_image_124["image"]) + image_128['id'] = '128' + image_128['name'] = 'deleted snapshot' + image_128['status'] = 'DELETED' + image_128['progress'] = 0 + image_128["links"][0]["href"] = "%s/128" % self.url_prefix + image_128["links"][1]["href"] = "%s/128" % self.bookmark_prefix + image_128["links"][2]["href"] = ( + "%s/images/128" % glance.generate_glance_url()) + + image_129 = copy.deepcopy(self.expected_image_124["image"]) + image_129['id'] = '129' + image_129['name'] = 'pending_delete snapshot' + image_129['status'] = 'DELETED' + image_129['progress'] = 0 + image_129["links"][0]["href"] = "%s/129" % self.url_prefix + image_129["links"][1]["href"] = "%s/129" % self.bookmark_prefix + image_129["links"][2]["href"] = ( + "%s/images/129" % glance.generate_glance_url()) + + image_130 = copy.deepcopy(self.expected_image_123["image"]) + image_130['id'] = '130' + image_130['name'] = None + image_130['metadata'] = {} + image_130['minDisk'] = 0 + image_130['minRam'] = 0 + image_130["links"][0]["href"] = "%s/130" % self.url_prefix + image_130["links"][1]["href"] = "%s/130" % self.bookmark_prefix + image_130["links"][2]["href"] = ( + "%s/images/130" % glance.generate_glance_url()) + + image_131 = copy.deepcopy(self.expected_image_123["image"]) + image_131['id'] = '131' + image_131['name'] = None + image_131['metadata'] = {} + image_131['minDisk'] = 0 + image_131['minRam'] = 0 + image_131["links"][0]["href"] = "%s/131" % self.url_prefix + image_131["links"][1]["href"] = "%s/131" % self.bookmark_prefix + image_131["links"][2]["href"] = ( + "%s/images/131" % glance.generate_glance_url()) + + expected = [self.expected_image_123["image"], + self.expected_image_124["image"], + image_125, image_126, image_127, + image_128, image_129, image_130, + image_131] + + self.assertThat(expected, matchers.DictListMatches(response_list)) + + @mock.patch('nova.image.api.API.get_all') + def test_get_image_details_with_limit(self, get_all_mocked): + request = self.http_request.blank(self.url_base + + 'images/detail?limit=2') + self.controller.detail(request) + get_all_mocked.assert_called_once_with(mock.ANY, limit=2, filters={}) + + @mock.patch('nova.image.api.API.get_all') + def test_get_image_details_with_limit_and_page_size(self, get_all_mocked): + request = self.http_request.blank( + self.url_base + 'images/detail?limit=2&page_size=1') + self.controller.detail(request) + get_all_mocked.assert_called_once_with(mock.ANY, limit=2, filters={}, + page_size=1) + + @mock.patch('nova.image.api.API.get_all') + def _detail_request(self, filters, request, get_all_mocked): + self.controller.detail(request) + get_all_mocked.assert_called_once_with(mock.ANY, filters=filters) + + def test_image_detail_filter_with_name(self): + filters = {'name': 'testname'} + request = self.http_request.blank(self.url_base + 'images/detail' + '?name=testname') + self._detail_request(filters, request) + + def test_image_detail_filter_with_status(self): + filters = {'status': 'active'} + request = self.http_request.blank(self.url_base + 'images/detail' + '?status=ACTIVE') + self._detail_request(filters, request) + + def test_image_detail_filter_with_property(self): + filters = {'property-test': '3'} + request = self.http_request.blank(self.url_base + 'images/detail' + '?property-test=3') + self._detail_request(filters, request) + + def test_image_detail_filter_server_href(self): + filters = {'property-instance_uuid': self.uuid} + request = self.http_request.blank( + self.url_base + 'images/detail?server=' + self.uuid) + self._detail_request(filters, request) + + def test_image_detail_filter_server_uuid(self): + filters = {'property-instance_uuid': self.uuid} + request = self.http_request.blank( + self.url_base + 'images/detail?server=' + self.uuid) + self._detail_request(filters, request) + + def test_image_detail_filter_changes_since(self): + filters = {'changes-since': '2011-01-24T17:08Z'} + request = self.http_request.blank(self.url_base + 'images/detail' + '?changes-since=2011-01-24T17:08Z') + self._detail_request(filters, request) + + def test_image_detail_filter_with_type(self): + filters = {'property-image_type': 'BASE'} + request = self.http_request.blank( + self.url_base + 'images/detail?type=BASE') + self._detail_request(filters, request) + + def test_image_detail_filter_not_supported(self): + filters = {'status': 'active'} + request = self.http_request.blank( + self.url_base + 'images/detail?status=' + 'ACTIVE&UNSUPPORTEDFILTER=testname') + self._detail_request(filters, request) + + def test_image_detail_no_filters(self): + filters = {} + request = self.http_request.blank(self.url_base + 'images/detail') + self._detail_request(filters, request) + + @mock.patch('nova.image.api.API.get_all', side_effect=exception.Invalid) + def test_image_detail_invalid_marker(self, _get_all_mocked): + request = self.http_request.blank(self.url_base + '?marker=invalid') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.detail, + request) + + def test_generate_alternate_link(self): + view = images_view.ViewBuilder() + request = self.http_request.blank(self.url_base + 'images/1') + generated_url = view._get_alternate_link(request, 1) + actual_url = "%s/images/1" % glance.generate_glance_url() + self.assertEqual(generated_url, actual_url) + + def _check_response(self, controller_method, response, expected_code): + self.assertEqual(expected_code, controller_method.wsgi_code) + + @mock.patch('nova.image.api.API.delete') + def test_delete_image(self, delete_mocked): + request = self.http_request.blank(self.url_base + 'images/124') + request.method = 'DELETE' + response = self.controller.delete(request, '124') + self._check_response(self.controller.delete, response, 204) + delete_mocked.assert_called_once_with(mock.ANY, '124') + + @mock.patch('nova.image.api.API.delete', + side_effect=exception.ImageNotAuthorized(image_id='123')) + def test_delete_deleted_image(self, _delete_mocked): + # If you try to delete a deleted image, you get back 403 Forbidden. + request = self.http_request.blank(self.url_base + 'images/123') + request.method = 'DELETE' + self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete, + request, '123') + + @mock.patch('nova.image.api.API.delete', + side_effect=exception.ImageNotFound(image_id='123')) + def test_delete_image_not_found(self, _delete_mocked): + request = self.http_request.blank(self.url_base + 'images/300') + request.method = 'DELETE' + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, request, '300') + + +class ImagesControllerTestV2(ImagesControllerTestV21): + image_controller_class = images.Controller + url_base = '/v2/fake' + bookmark_base = '/fake' + http_request = fakes.HTTPRequest + + def _check_response(self, controller_method, response, expected_code): + self.assertEqual(expected_code, response.status_int) + + +class ImageXMLSerializationTest(test.NoDBTestCase): + + TIMESTAMP = "2010-10-11T10:30:22Z" + SERVER_UUID = 'aa640691-d1a7-4a67-9d3c-d35ee6b3cc74' + SERVER_HREF = 'http://localhost/v2/fake/servers/' + SERVER_UUID + SERVER_BOOKMARK = 'http://localhost/fake/servers/' + SERVER_UUID + IMAGE_HREF = 'http://localhost/v2/fake/images/%s' + IMAGE_NEXT = 'http://localhost/v2/fake/images?limit=%s&marker=%s' + IMAGE_BOOKMARK = 'http://localhost/fake/images/%s' + + def test_xml_declaration(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'progress': 80, + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) + + def test_show(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'progress': 80, + 'minRam': 10, + 'minDisk': 100, + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status', 'progress']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = image_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_show_zero_metadata(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': {}, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + meta_nodes = root.findall('{0}meta'.format(ATOMNS)) + self.assertEqual(len(meta_nodes), 0) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_show_image_no_metadata_key(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + meta_nodes = root.findall('{0}meta'.format(ATOMNS)) + self.assertEqual(len(meta_nodes), 0) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_show_no_server(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = image_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + server_root = root.find('{0}server'.format(NS)) + self.assertIsNone(server_root) + + def test_show_with_min_ram(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'progress': 80, + 'minRam': 256, + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status', 'progress', + 'minRam']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = image_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_show_with_min_disk(self): + serializer = images.ImageTemplate() + + fixture = { + 'image': { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'progress': 80, + 'minDisk': 5, + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'image') + image_dict = fixture['image'] + + for key in ['name', 'id', 'updated', 'created', 'status', 'progress', + 'minDisk']: + self.assertEqual(root.get(key), str(image_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = image_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + server_root = root.find('{0}server'.format(NS)) + self.assertEqual(server_root.get('id'), image_dict['server']['id']) + link_nodes = server_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['server']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_index(self): + serializer = images.MinimalImagesTemplate() + + fixture = { + 'images': [ + { + 'id': 1, + 'name': 'Image1', + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + { + 'id': 2, + 'name': 'Image2', + 'links': [ + { + 'href': self.IMAGE_HREF % 2, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 2, + 'rel': 'bookmark', + }, + ], + }, + ] + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'images') + image_elems = root.findall('{0}image'.format(NS)) + self.assertEqual(len(image_elems), 2) + for i, image_elem in enumerate(image_elems): + image_dict = fixture['images'][i] + + for key in ['name', 'id']: + self.assertEqual(image_elem.get(key), str(image_dict[key])) + + link_nodes = image_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_index_with_links(self): + serializer = images.MinimalImagesTemplate() + + fixture = { + 'images': [ + { + 'id': 1, + 'name': 'Image1', + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + { + 'id': 2, + 'name': 'Image2', + 'links': [ + { + 'href': self.IMAGE_HREF % 2, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 2, + 'rel': 'bookmark', + }, + ], + }, + ], + 'images_links': [ + { + 'rel': 'next', + 'href': self.IMAGE_NEXT % (2, 2), + } + ], + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'images') + image_elems = root.findall('{0}image'.format(NS)) + self.assertEqual(len(image_elems), 2) + for i, image_elem in enumerate(image_elems): + image_dict = fixture['images'][i] + + for key in ['name', 'id']: + self.assertEqual(image_elem.get(key), str(image_dict[key])) + + link_nodes = image_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + # Check images_links + images_links = root.findall('{0}link'.format(ATOMNS)) + for i, link in enumerate(fixture['images_links']): + for key, value in link.items(): + self.assertEqual(images_links[i].get(key), value) + + def test_index_zero_images(self): + serializer = images.MinimalImagesTemplate() + + fixtures = { + 'images': [], + } + + output = serializer.serialize(fixtures) + root = etree.XML(output) + xmlutil.validate_schema(root, 'images') + image_elems = root.findall('{0}image'.format(NS)) + self.assertEqual(len(image_elems), 0) + + def test_detail(self): + serializer = images.ImagesTemplate() + + fixture = { + 'images': [ + { + 'id': 1, + 'name': 'Image1', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'ACTIVE', + 'server': { + 'id': self.SERVER_UUID, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 1, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 1, + 'rel': 'bookmark', + }, + ], + }, + { + 'id': '2', + 'name': 'Image2', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + 'status': 'SAVING', + 'progress': 80, + 'metadata': { + 'key1': 'value1', + }, + 'links': [ + { + 'href': self.IMAGE_HREF % 2, + 'rel': 'self', + }, + { + 'href': self.IMAGE_BOOKMARK % 2, + 'rel': 'bookmark', + }, + ], + }, + ] + } + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'images') + image_elems = root.findall('{0}image'.format(NS)) + self.assertEqual(len(image_elems), 2) + for i, image_elem in enumerate(image_elems): + image_dict = fixture['images'][i] + + for key in ['name', 'id', 'updated', 'created', 'status']: + self.assertEqual(image_elem.get(key), str(image_dict[key])) + + link_nodes = image_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(image_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) diff --git a/nova/tests/unit/api/openstack/compute/test_limits.py b/nova/tests/unit/api/openstack/compute/test_limits.py new file mode 100644 index 0000000000..47da849b28 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_limits.py @@ -0,0 +1,1016 @@ +# Copyright 2011 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. + +""" +Tests dealing with HTTP rate-limiting. +""" + +import httplib +import StringIO +from xml.dom import minidom + +from lxml import etree +import mock +from oslo.serialization import jsonutils +import six +import webob + +from nova.api.openstack.compute import limits +from nova.api.openstack.compute.plugins.v3 import limits as limits_v3 +from nova.api.openstack.compute import views +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +import nova.context +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import matchers +from nova import utils + + +TEST_LIMITS = [ + limits.Limit("GET", "/delayed", "^/delayed", 1, + utils.TIME_UNITS['MINUTE']), + limits.Limit("POST", "*", ".*", 7, utils.TIME_UNITS['MINUTE']), + limits.Limit("POST", "/servers", "^/servers", 3, + utils.TIME_UNITS['MINUTE']), + limits.Limit("PUT", "*", "", 10, utils.TIME_UNITS['MINUTE']), + limits.Limit("PUT", "/servers", "^/servers", 5, + utils.TIME_UNITS['MINUTE']), +] +NS = { + 'atom': 'http://www.w3.org/2005/Atom', + 'ns': 'http://docs.openstack.org/common/api/v1.0' +} + + +class BaseLimitTestSuite(test.NoDBTestCase): + """Base test suite which provides relevant stubs and time abstraction.""" + + def setUp(self): + super(BaseLimitTestSuite, self).setUp() + self.time = 0.0 + self.stubs.Set(limits.Limit, "_get_time", self._get_time) + self.absolute_limits = {} + + def stub_get_project_quotas(context, project_id, usages=True): + return dict((k, dict(limit=v)) + for k, v in self.absolute_limits.items()) + + self.stubs.Set(nova.quota.QUOTAS, "get_project_quotas", + stub_get_project_quotas) + + def _get_time(self): + """Return the "time" according to this test suite.""" + return self.time + + +class LimitsControllerTestV21(BaseLimitTestSuite): + """Tests for `limits.LimitsController` class.""" + limits_controller = limits_v3.LimitsController + + def setUp(self): + """Run before each test.""" + super(LimitsControllerTestV21, self).setUp() + self.controller = wsgi.Resource(self.limits_controller()) + self.ctrler = self.limits_controller() + + def _get_index_request(self, accept_header="application/json", + tenant_id=None): + """Helper to set routing arguments.""" + request = webob.Request.blank("/") + if tenant_id: + request = webob.Request.blank("/?tenant_id=%s" % tenant_id) + + request.accept = accept_header + request.environ["wsgiorg.routing_args"] = (None, { + "action": "index", + "controller": "", + }) + context = nova.context.RequestContext('testuser', 'testproject') + request.environ["nova.context"] = context + return request + + def _populate_limits(self, request): + """Put limit info into a request.""" + _limits = [ + limits.Limit("GET", "*", ".*", 10, 60).display(), + limits.Limit("POST", "*", ".*", 5, 60 * 60).display(), + limits.Limit("GET", "changes-since*", "changes-since", + 5, 60).display(), + ] + request.environ["nova.limits"] = _limits + return request + + def test_empty_index_json(self): + # Test getting empty limit details in JSON. + request = self._get_index_request() + response = request.get_response(self.controller) + expected = { + "limits": { + "rate": [], + "absolute": {}, + }, + } + body = jsonutils.loads(response.body) + self.assertEqual(expected, body) + + def test_index_json(self): + self._test_index_json() + + def test_index_json_by_tenant(self): + self._test_index_json('faketenant') + + def _test_index_json(self, tenant_id=None): + # Test getting limit details in JSON. + request = self._get_index_request(tenant_id=tenant_id) + context = request.environ["nova.context"] + if tenant_id is None: + tenant_id = context.project_id + + request = self._populate_limits(request) + self.absolute_limits = { + 'ram': 512, + 'instances': 5, + 'cores': 21, + 'key_pairs': 10, + 'floating_ips': 10, + 'security_groups': 10, + 'security_group_rules': 20, + } + expected = { + "limits": { + "rate": [ + { + "regex": ".*", + "uri": "*", + "limit": [ + { + "verb": "GET", + "next-available": "1970-01-01T00:00:00Z", + "unit": "MINUTE", + "value": 10, + "remaining": 10, + }, + { + "verb": "POST", + "next-available": "1970-01-01T00:00:00Z", + "unit": "HOUR", + "value": 5, + "remaining": 5, + }, + ], + }, + { + "regex": "changes-since", + "uri": "changes-since*", + "limit": [ + { + "verb": "GET", + "next-available": "1970-01-01T00:00:00Z", + "unit": "MINUTE", + "value": 5, + "remaining": 5, + }, + ], + }, + + ], + "absolute": { + "maxTotalRAMSize": 512, + "maxTotalInstances": 5, + "maxTotalCores": 21, + "maxTotalKeypairs": 10, + "maxTotalFloatingIps": 10, + "maxSecurityGroups": 10, + "maxSecurityGroupRules": 20, + }, + }, + } + + def _get_project_quotas(context, project_id, usages=True): + return dict((k, dict(limit=v)) + for k, v in self.absolute_limits.items()) + + with mock.patch('nova.quota.QUOTAS.get_project_quotas') as \ + get_project_quotas: + get_project_quotas.side_effect = _get_project_quotas + + response = request.get_response(self.controller) + + body = jsonutils.loads(response.body) + self.assertEqual(expected, body) + get_project_quotas.assert_called_once_with(context, tenant_id, + usages=False) + + +class LimitsControllerTestV2(LimitsControllerTestV21): + limits_controller = limits.LimitsController + + def _populate_limits_diff_regex(self, request): + """Put limit info into a request.""" + _limits = [ + limits.Limit("GET", "*", ".*", 10, 60).display(), + limits.Limit("GET", "*", "*.*", 10, 60).display(), + ] + request.environ["nova.limits"] = _limits + return request + + def test_index_diff_regex(self): + # Test getting limit details in JSON. + request = self._get_index_request() + request = self._populate_limits_diff_regex(request) + response = request.get_response(self.controller) + expected = { + "limits": { + "rate": [ + { + "regex": ".*", + "uri": "*", + "limit": [ + { + "verb": "GET", + "next-available": "1970-01-01T00:00:00Z", + "unit": "MINUTE", + "value": 10, + "remaining": 10, + }, + ], + }, + { + "regex": "*.*", + "uri": "*", + "limit": [ + { + "verb": "GET", + "next-available": "1970-01-01T00:00:00Z", + "unit": "MINUTE", + "value": 10, + "remaining": 10, + }, + ], + }, + + ], + "absolute": {}, + }, + } + body = jsonutils.loads(response.body) + self.assertEqual(expected, body) + + def _test_index_absolute_limits_json(self, expected): + request = self._get_index_request() + response = request.get_response(self.controller) + body = jsonutils.loads(response.body) + self.assertEqual(expected, body['limits']['absolute']) + + def test_index_ignores_extra_absolute_limits_json(self): + self.absolute_limits = {'unknown_limit': 9001} + self._test_index_absolute_limits_json({}) + + def test_index_absolute_ram_json(self): + self.absolute_limits = {'ram': 1024} + self._test_index_absolute_limits_json({'maxTotalRAMSize': 1024}) + + def test_index_absolute_cores_json(self): + self.absolute_limits = {'cores': 17} + self._test_index_absolute_limits_json({'maxTotalCores': 17}) + + def test_index_absolute_instances_json(self): + self.absolute_limits = {'instances': 19} + self._test_index_absolute_limits_json({'maxTotalInstances': 19}) + + def test_index_absolute_metadata_json(self): + # NOTE: both server metadata and image metadata are overloaded + # into metadata_items + self.absolute_limits = {'metadata_items': 23} + expected = { + 'maxServerMeta': 23, + 'maxImageMeta': 23, + } + self._test_index_absolute_limits_json(expected) + + def test_index_absolute_injected_files(self): + self.absolute_limits = { + 'injected_files': 17, + 'injected_file_content_bytes': 86753, + } + expected = { + 'maxPersonality': 17, + 'maxPersonalitySize': 86753, + } + self._test_index_absolute_limits_json(expected) + + def test_index_absolute_security_groups(self): + self.absolute_limits = { + 'security_groups': 8, + 'security_group_rules': 16, + } + expected = { + 'maxSecurityGroups': 8, + 'maxSecurityGroupRules': 16, + } + self._test_index_absolute_limits_json(expected) + + def test_limit_create(self): + req = fakes.HTTPRequest.blank('/v2/fake/limits') + self.assertRaises(webob.exc.HTTPNotImplemented, self.ctrler.create, + req, {}) + + def test_limit_delete(self): + req = fakes.HTTPRequest.blank('/v2/fake/limits') + self.assertRaises(webob.exc.HTTPNotImplemented, self.ctrler.delete, + req, 1) + + def test_limit_detail(self): + req = fakes.HTTPRequest.blank('/v2/fake/limits') + self.assertRaises(webob.exc.HTTPNotImplemented, self.ctrler.detail, + req) + + def test_limit_show(self): + req = fakes.HTTPRequest.blank('/v2/fake/limits') + self.assertRaises(webob.exc.HTTPNotImplemented, self.ctrler.show, + req, 1) + + def test_limit_update(self): + req = fakes.HTTPRequest.blank('/v2/fake/limits') + self.assertRaises(webob.exc.HTTPNotImplemented, self.ctrler.update, + req, 1, {}) + + +class MockLimiter(limits.Limiter): + pass + + +class LimitMiddlewareTest(BaseLimitTestSuite): + """Tests for the `limits.RateLimitingMiddleware` class.""" + + @webob.dec.wsgify + def _empty_app(self, request): + """Do-nothing WSGI app.""" + pass + + def setUp(self): + """Prepare middleware for use through fake WSGI app.""" + super(LimitMiddlewareTest, self).setUp() + _limits = '(GET, *, .*, 1, MINUTE)' + self.app = limits.RateLimitingMiddleware(self._empty_app, _limits, + "%s.MockLimiter" % + self.__class__.__module__) + + def test_limit_class(self): + # Test that middleware selected correct limiter class. + self.assertIsInstance(self.app._limiter, MockLimiter) + + def test_good_request(self): + # Test successful GET request through middleware. + request = webob.Request.blank("/") + response = request.get_response(self.app) + self.assertEqual(200, response.status_int) + + def test_limited_request_json(self): + # Test a rate-limited (429) GET request through middleware. + request = webob.Request.blank("/") + response = request.get_response(self.app) + self.assertEqual(200, response.status_int) + + request = webob.Request.blank("/") + response = request.get_response(self.app) + self.assertEqual(response.status_int, 429) + + self.assertIn('Retry-After', response.headers) + retry_after = int(response.headers['Retry-After']) + self.assertAlmostEqual(retry_after, 60, 1) + + body = jsonutils.loads(response.body) + expected = "Only 1 GET request(s) can be made to * every minute." + value = body["overLimit"]["details"].strip() + self.assertEqual(value, expected) + + self.assertIn("retryAfter", body["overLimit"]) + retryAfter = body["overLimit"]["retryAfter"] + self.assertEqual(retryAfter, "60") + + def test_limited_request_xml(self): + # Test a rate-limited (429) response as XML. + request = webob.Request.blank("/") + response = request.get_response(self.app) + self.assertEqual(200, response.status_int) + + request = webob.Request.blank("/") + request.accept = "application/xml" + response = request.get_response(self.app) + self.assertEqual(response.status_int, 429) + + root = minidom.parseString(response.body).childNodes[0] + expected = "Only 1 GET request(s) can be made to * every minute." + + self.assertIsNotNone(root.attributes.getNamedItem("retryAfter")) + retryAfter = root.attributes.getNamedItem("retryAfter").value + self.assertEqual(retryAfter, "60") + + details = root.getElementsByTagName("details") + self.assertEqual(details.length, 1) + + value = details.item(0).firstChild.data.strip() + self.assertEqual(value, expected) + + +class LimitTest(BaseLimitTestSuite): + """Tests for the `limits.Limit` class.""" + + def test_GET_no_delay(self): + # Test a limit handles 1 GET per second. + limit = limits.Limit("GET", "*", ".*", 1, 1) + delay = limit("GET", "/anything") + self.assertIsNone(delay) + self.assertEqual(0, limit.next_request) + self.assertEqual(0, limit.last_request) + + def test_GET_delay(self): + # Test two calls to 1 GET per second limit. + limit = limits.Limit("GET", "*", ".*", 1, 1) + delay = limit("GET", "/anything") + self.assertIsNone(delay) + + delay = limit("GET", "/anything") + self.assertEqual(1, delay) + self.assertEqual(1, limit.next_request) + self.assertEqual(0, limit.last_request) + + self.time += 4 + + delay = limit("GET", "/anything") + self.assertIsNone(delay) + self.assertEqual(4, limit.next_request) + self.assertEqual(4, limit.last_request) + + +class ParseLimitsTest(BaseLimitTestSuite): + """Tests for the default limits parser in the in-memory + `limits.Limiter` class. + """ + + def test_invalid(self): + # Test that parse_limits() handles invalid input correctly. + self.assertRaises(ValueError, limits.Limiter.parse_limits, + ';;;;;') + + def test_bad_rule(self): + # Test that parse_limits() handles bad rules correctly. + self.assertRaises(ValueError, limits.Limiter.parse_limits, + 'GET, *, .*, 20, minute') + + def test_missing_arg(self): + # Test that parse_limits() handles missing args correctly. + self.assertRaises(ValueError, limits.Limiter.parse_limits, + '(GET, *, .*, 20)') + + def test_bad_value(self): + # Test that parse_limits() handles bad values correctly. + self.assertRaises(ValueError, limits.Limiter.parse_limits, + '(GET, *, .*, foo, minute)') + + def test_bad_unit(self): + # Test that parse_limits() handles bad units correctly. + self.assertRaises(ValueError, limits.Limiter.parse_limits, + '(GET, *, .*, 20, lightyears)') + + def test_multiple_rules(self): + # Test that parse_limits() handles multiple rules correctly. + try: + l = limits.Limiter.parse_limits('(get, *, .*, 20, minute);' + '(PUT, /foo*, /foo.*, 10, hour);' + '(POST, /bar*, /bar.*, 5, second);' + '(Say, /derp*, /derp.*, 1, day)') + except ValueError as e: + assert False, six.text_type(e) + + # Make sure the number of returned limits are correct + self.assertEqual(len(l), 4) + + # Check all the verbs... + expected = ['GET', 'PUT', 'POST', 'SAY'] + self.assertEqual([t.verb for t in l], expected) + + # ...the URIs... + expected = ['*', '/foo*', '/bar*', '/derp*'] + self.assertEqual([t.uri for t in l], expected) + + # ...the regexes... + expected = ['.*', '/foo.*', '/bar.*', '/derp.*'] + self.assertEqual([t.regex for t in l], expected) + + # ...the values... + expected = [20, 10, 5, 1] + self.assertEqual([t.value for t in l], expected) + + # ...and the units... + expected = [utils.TIME_UNITS['MINUTE'], utils.TIME_UNITS['HOUR'], + utils.TIME_UNITS['SECOND'], utils.TIME_UNITS['DAY']] + self.assertEqual([t.unit for t in l], expected) + + +class LimiterTest(BaseLimitTestSuite): + """Tests for the in-memory `limits.Limiter` class.""" + + def setUp(self): + """Run before each test.""" + super(LimiterTest, self).setUp() + userlimits = {'limits.user3': '', + 'limits.user0': '(get, *, .*, 4, minute);' + '(put, *, .*, 2, minute)'} + self.limiter = limits.Limiter(TEST_LIMITS, **userlimits) + + def _check(self, num, verb, url, username=None): + """Check and yield results from checks.""" + for x in xrange(num): + yield self.limiter.check_for_delay(verb, url, username)[0] + + def _check_sum(self, num, verb, url, username=None): + """Check and sum results from checks.""" + results = self._check(num, verb, url, username) + return sum(item for item in results if item) + + def test_no_delay_GET(self): + """Simple test to ensure no delay on a single call for a limit verb we + didn"t set. + """ + delay = self.limiter.check_for_delay("GET", "/anything") + self.assertEqual(delay, (None, None)) + + def test_no_delay_PUT(self): + # Simple test to ensure no delay on a single call for a known limit. + delay = self.limiter.check_for_delay("PUT", "/anything") + self.assertEqual(delay, (None, None)) + + def test_delay_PUT(self): + """Ensure the 11th PUT will result in a delay of 6.0 seconds until + the next request will be granced. + """ + expected = [None] * 10 + [6.0] + results = list(self._check(11, "PUT", "/anything")) + + self.assertEqual(expected, results) + + def test_delay_POST(self): + """Ensure the 8th POST will result in a delay of 6.0 seconds until + the next request will be granced. + """ + expected = [None] * 7 + results = list(self._check(7, "POST", "/anything")) + self.assertEqual(expected, results) + + expected = 60.0 / 7.0 + results = self._check_sum(1, "POST", "/anything") + self.assertAlmostEqual(expected, results, 8) + + def test_delay_GET(self): + # Ensure the 11th GET will result in NO delay. + expected = [None] * 11 + results = list(self._check(11, "GET", "/anything")) + self.assertEqual(expected, results) + + expected = [None] * 4 + [15.0] + results = list(self._check(5, "GET", "/foo", "user0")) + self.assertEqual(expected, results) + + def test_delay_PUT_servers(self): + """Ensure PUT on /servers limits at 5 requests, and PUT elsewhere is + still OK after 5 requests...but then after 11 total requests, PUT + limiting kicks in. + """ + # First 6 requests on PUT /servers + expected = [None] * 5 + [12.0] + results = list(self._check(6, "PUT", "/servers")) + self.assertEqual(expected, results) + + # Next 5 request on PUT /anything + expected = [None] * 4 + [6.0] + results = list(self._check(5, "PUT", "/anything")) + self.assertEqual(expected, results) + + def test_delay_PUT_wait(self): + """Ensure after hitting the limit and then waiting for the correct + amount of time, the limit will be lifted. + """ + expected = [None] * 10 + [6.0] + results = list(self._check(11, "PUT", "/anything")) + self.assertEqual(expected, results) + + # Advance time + self.time += 6.0 + + expected = [None, 6.0] + results = list(self._check(2, "PUT", "/anything")) + self.assertEqual(expected, results) + + def test_multiple_delays(self): + # Ensure multiple requests still get a delay. + expected = [None] * 10 + [6.0] * 10 + results = list(self._check(20, "PUT", "/anything")) + self.assertEqual(expected, results) + + self.time += 1.0 + + expected = [5.0] * 10 + results = list(self._check(10, "PUT", "/anything")) + self.assertEqual(expected, results) + + expected = [None] * 2 + [30.0] * 8 + results = list(self._check(10, "PUT", "/anything", "user0")) + self.assertEqual(expected, results) + + def test_user_limit(self): + # Test user-specific limits. + self.assertEqual(self.limiter.levels['user3'], []) + self.assertEqual(len(self.limiter.levels['user0']), 2) + + def test_multiple_users(self): + # Tests involving multiple users. + # User0 + expected = [None] * 2 + [30.0] * 8 + results = list(self._check(10, "PUT", "/anything", "user0")) + self.assertEqual(expected, results) + + # User1 + expected = [None] * 10 + [6.0] * 10 + results = list(self._check(20, "PUT", "/anything", "user1")) + self.assertEqual(expected, results) + + # User2 + expected = [None] * 10 + [6.0] * 5 + results = list(self._check(15, "PUT", "/anything", "user2")) + self.assertEqual(expected, results) + + # User3 + expected = [None] * 20 + results = list(self._check(20, "PUT", "/anything", "user3")) + self.assertEqual(expected, results) + + self.time += 1.0 + + # User1 again + expected = [5.0] * 10 + results = list(self._check(10, "PUT", "/anything", "user1")) + self.assertEqual(expected, results) + + self.time += 1.0 + + # User1 again + expected = [4.0] * 5 + results = list(self._check(5, "PUT", "/anything", "user2")) + self.assertEqual(expected, results) + + # User0 again + expected = [28.0] + results = list(self._check(1, "PUT", "/anything", "user0")) + self.assertEqual(expected, results) + + self.time += 28.0 + + expected = [None, 30.0] + results = list(self._check(2, "PUT", "/anything", "user0")) + self.assertEqual(expected, results) + + +class WsgiLimiterTest(BaseLimitTestSuite): + """Tests for `limits.WsgiLimiter` class.""" + + def setUp(self): + """Run before each test.""" + super(WsgiLimiterTest, self).setUp() + self.app = limits.WsgiLimiter(TEST_LIMITS) + + def _request_data(self, verb, path): + """Get data describing a limit request verb/path.""" + return jsonutils.dumps({"verb": verb, "path": path}) + + def _request(self, verb, url, username=None): + """Make sure that POSTing to the given url causes the given username + to perform the given action. Make the internal rate limiter return + delay and make sure that the WSGI app returns the correct response. + """ + if username: + request = webob.Request.blank("/%s" % username) + else: + request = webob.Request.blank("/") + + request.method = "POST" + request.body = self._request_data(verb, url) + response = request.get_response(self.app) + + if "X-Wait-Seconds" in response.headers: + self.assertEqual(response.status_int, 403) + return response.headers["X-Wait-Seconds"] + + self.assertEqual(response.status_int, 204) + + def test_invalid_methods(self): + # Only POSTs should work. + for method in ["GET", "PUT", "DELETE", "HEAD", "OPTIONS"]: + request = webob.Request.blank("/", method=method) + response = request.get_response(self.app) + self.assertEqual(response.status_int, 405) + + def test_good_url(self): + delay = self._request("GET", "/something") + self.assertIsNone(delay) + + def test_escaping(self): + delay = self._request("GET", "/something/jump%20up") + self.assertIsNone(delay) + + def test_response_to_delays(self): + delay = self._request("GET", "/delayed") + self.assertIsNone(delay) + + delay = self._request("GET", "/delayed") + self.assertEqual(delay, '60.00') + + def test_response_to_delays_usernames(self): + delay = self._request("GET", "/delayed", "user1") + self.assertIsNone(delay) + + delay = self._request("GET", "/delayed", "user2") + self.assertIsNone(delay) + + delay = self._request("GET", "/delayed", "user1") + self.assertEqual(delay, '60.00') + + delay = self._request("GET", "/delayed", "user2") + self.assertEqual(delay, '60.00') + + +class FakeHttplibSocket(object): + """Fake `httplib.HTTPResponse` replacement.""" + + def __init__(self, response_string): + """Initialize new `FakeHttplibSocket`.""" + self._buffer = StringIO.StringIO(response_string) + + def makefile(self, _mode, _other): + """Returns the socket's internal buffer.""" + return self._buffer + + +class FakeHttplibConnection(object): + """Fake `httplib.HTTPConnection`.""" + + def __init__(self, app, host): + """Initialize `FakeHttplibConnection`.""" + self.app = app + self.host = host + + def request(self, method, path, body="", headers=None): + """Requests made via this connection actually get translated and routed + into our WSGI app, we then wait for the response and turn it back into + an `httplib.HTTPResponse`. + """ + if not headers: + headers = {} + + req = webob.Request.blank(path) + req.method = method + req.headers = headers + req.host = self.host + req.body = body + + resp = str(req.get_response(self.app)) + resp = "HTTP/1.0 %s" % resp + sock = FakeHttplibSocket(resp) + self.http_response = httplib.HTTPResponse(sock) + self.http_response.begin() + + def getresponse(self): + """Return our generated response from the request.""" + return self.http_response + + +def wire_HTTPConnection_to_WSGI(host, app): + """Monkeypatches HTTPConnection so that if you try to connect to host, you + are instead routed straight to the given WSGI app. + + After calling this method, when any code calls + + httplib.HTTPConnection(host) + + the connection object will be a fake. Its requests will be sent directly + to the given WSGI app rather than through a socket. + + Code connecting to hosts other than host will not be affected. + + This method may be called multiple times to map different hosts to + different apps. + + This method returns the original HTTPConnection object, so that the caller + can restore the default HTTPConnection interface (for all hosts). + """ + class HTTPConnectionDecorator(object): + """Wraps the real HTTPConnection class so that when you instantiate + the class you might instead get a fake instance. + """ + + def __init__(self, wrapped): + self.wrapped = wrapped + + def __call__(self, connection_host, *args, **kwargs): + if connection_host == host: + return FakeHttplibConnection(app, host) + else: + return self.wrapped(connection_host, *args, **kwargs) + + oldHTTPConnection = httplib.HTTPConnection + httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) + return oldHTTPConnection + + +class WsgiLimiterProxyTest(BaseLimitTestSuite): + """Tests for the `limits.WsgiLimiterProxy` class.""" + + def setUp(self): + """Do some nifty HTTP/WSGI magic which allows for WSGI to be called + directly by something like the `httplib` library. + """ + super(WsgiLimiterProxyTest, self).setUp() + self.app = limits.WsgiLimiter(TEST_LIMITS) + self.oldHTTPConnection = ( + wire_HTTPConnection_to_WSGI("169.254.0.1:80", self.app)) + self.proxy = limits.WsgiLimiterProxy("169.254.0.1:80") + + def test_200(self): + # Successful request test. + delay = self.proxy.check_for_delay("GET", "/anything") + self.assertEqual(delay, (None, None)) + + def test_403(self): + # Forbidden request test. + delay = self.proxy.check_for_delay("GET", "/delayed") + self.assertEqual(delay, (None, None)) + + delay, error = self.proxy.check_for_delay("GET", "/delayed") + error = error.strip() + + expected = ("60.00", "403 Forbidden\n\nOnly 1 GET request(s) can be " + "made to /delayed every minute.") + + self.assertEqual((delay, error), expected) + + def tearDown(self): + # restore original HTTPConnection object + httplib.HTTPConnection = self.oldHTTPConnection + super(WsgiLimiterProxyTest, self).tearDown() + + +class LimitsViewBuilderTest(test.NoDBTestCase): + def setUp(self): + super(LimitsViewBuilderTest, self).setUp() + self.view_builder = views.limits.ViewBuilder() + self.rate_limits = [{"URI": "*", + "regex": ".*", + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "resetTime": 1311272226}, + {"URI": "*/servers", + "regex": "^/servers", + "value": 50, + "verb": "POST", + "remaining": 10, + "unit": "DAY", + "resetTime": 1311272226}] + self.absolute_limits = {"metadata_items": 1, + "injected_files": 5, + "injected_file_content_bytes": 5} + + def test_build_limits(self): + expected_limits = {"limits": { + "rate": [{ + "uri": "*", + "regex": ".*", + "limit": [{"value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-07-21T18:17:06Z"}]}, + {"uri": "*/servers", + "regex": "^/servers", + "limit": [{"value": 50, + "verb": "POST", + "remaining": 10, + "unit": "DAY", + "next-available": "2011-07-21T18:17:06Z"}]}], + "absolute": {"maxServerMeta": 1, + "maxImageMeta": 1, + "maxPersonality": 5, + "maxPersonalitySize": 5}}} + + output = self.view_builder.build(self.rate_limits, + self.absolute_limits) + self.assertThat(output, matchers.DictMatches(expected_limits)) + + def test_build_limits_empty_limits(self): + expected_limits = {"limits": {"rate": [], + "absolute": {}}} + + abs_limits = {} + rate_limits = [] + output = self.view_builder.build(rate_limits, abs_limits) + self.assertThat(output, matchers.DictMatches(expected_limits)) + + +class LimitsXMLSerializationTest(test.NoDBTestCase): + def test_xml_declaration(self): + serializer = limits.LimitsTemplate() + + fixture = {"limits": { + "rate": [], + "absolute": {}}} + + output = serializer.serialize(fixture) + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) + + def test_index(self): + serializer = limits.LimitsTemplate() + fixture = { + "limits": { + "rate": [{ + "uri": "*", + "regex": ".*", + "limit": [{ + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z"}]}, + {"uri": "*/servers", + "regex": "^/servers", + "limit": [{ + "value": 50, + "verb": "POST", + "remaining": 10, + "unit": "DAY", + "next-available": "2011-12-15T22:42:45Z"}]}], + "absolute": {"maxServerMeta": 1, + "maxImageMeta": 1, + "maxPersonality": 5, + "maxPersonalitySize": 10240}}} + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'limits') + + # verify absolute limits + absolutes = root.xpath('ns:absolute/ns:limit', namespaces=NS) + self.assertEqual(len(absolutes), 4) + for limit in absolutes: + name = limit.get('name') + value = limit.get('value') + self.assertEqual(value, str(fixture['limits']['absolute'][name])) + + # verify rate limits + rates = root.xpath('ns:rates/ns:rate', namespaces=NS) + self.assertEqual(len(rates), 2) + for i, rate in enumerate(rates): + for key in ['uri', 'regex']: + self.assertEqual(rate.get(key), + str(fixture['limits']['rate'][i][key])) + rate_limits = rate.xpath('ns:limit', namespaces=NS) + self.assertEqual(len(rate_limits), 1) + for j, limit in enumerate(rate_limits): + for key in ['verb', 'value', 'remaining', 'unit', + 'next-available']: + self.assertEqual(limit.get(key), + str(fixture['limits']['rate'][i]['limit'][j][key])) + + def test_index_no_limits(self): + serializer = limits.LimitsTemplate() + + fixture = {"limits": { + "rate": [], + "absolute": {}}} + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'limits') + + # verify absolute limits + absolutes = root.xpath('ns:absolute/ns:limit', namespaces=NS) + self.assertEqual(len(absolutes), 0) + + # verify rate limits + rates = root.xpath('ns:rates/ns:rate', namespaces=NS) + self.assertEqual(len(rates), 0) diff --git a/nova/tests/unit/api/openstack/compute/test_server_actions.py b/nova/tests/unit/api/openstack/compute/test_server_actions.py new file mode 100644 index 0000000000..16f8ce14bf --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_server_actions.py @@ -0,0 +1,1556 @@ +# Copyright 2011 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 base64 +import uuid + +import mock +import mox +from oslo.config import cfg +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute import servers +from nova.compute import api as compute_api +from nova.compute import task_states +from nova.compute import vm_states +from nova import context +from nova import db +from nova import exception +from nova.image import glance +from nova import objects +from nova.openstack.common import uuidutils +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_block_device +from nova.tests.unit import fake_instance +from nova.tests.unit.image import fake +from nova.tests.unit import matchers +from nova.tests.unit import utils + +CONF = cfg.CONF +CONF.import_opt('password_length', 'nova.utils') +FAKE_UUID = fakes.FAKE_UUID +INSTANCE_IDS = {FAKE_UUID: 1} + + +def return_server_not_found(*arg, **kwarg): + raise exception.NotFound() + + +def instance_update_and_get_original(context, instance_uuid, values, + update_cells=True, + columns_to_join=None, + ): + inst = fakes.stub_instance(INSTANCE_IDS[instance_uuid], host='fake_host') + inst = dict(inst, **values) + return (inst, inst) + + +def instance_update(context, instance_uuid, kwargs, update_cells=True): + inst = fakes.stub_instance(INSTANCE_IDS[instance_uuid], host='fake_host') + return inst + + +class MockSetAdminPassword(object): + def __init__(self): + self.instance_id = None + self.password = None + + def __call__(self, context, instance, password): + self.instance_id = instance['uuid'] + self.password = password + + +class ServerActionsControllerTest(test.TestCase): + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + image_href = 'http://localhost/v2/fake/images/%s' % image_uuid + + def setUp(self): + super(ServerActionsControllerTest, self).setUp() + + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE, + host='fake_host')) + self.stubs.Set(db, 'instance_update_and_get_original', + instance_update_and_get_original) + + fakes.stub_out_nw_api(self.stubs) + fakes.stub_out_compute_api_snapshot(self.stubs) + fake.stub_out_image_service(self.stubs) + self.flags(allow_instance_snapshots=True, + enable_instance_password=True) + self.uuid = FAKE_UUID + self.url = '/v2/fake/servers/%s/action' % self.uuid + self._image_href = '155d900f-4e14-4e4c-a73d-069cbf4541e6' + + class FakeExtManager(object): + def is_loaded(self, ext): + return False + + self.controller = servers.Controller(ext_mgr=FakeExtManager()) + self.compute_api = self.controller.compute_api + self.context = context.RequestContext('fake', 'fake') + self.app = fakes.wsgi_app(init_only=('servers',), + fake_auth_context=self.context) + + def _make_request(self, url, body): + req = webob.Request.blank('/v2/fake' + url) + req.method = 'POST' + req.body = jsonutils.dumps(body) + req.content_type = 'application/json' + return req.get_response(self.app) + + def _stub_instance_get(self, uuid=None): + self.mox.StubOutWithMock(compute_api.API, 'get') + if uuid is None: + uuid = uuidutils.generate_uuid() + instance = fake_instance.fake_db_instance( + id=1, uuid=uuid, vm_state=vm_states.ACTIVE, task_state=None) + instance = objects.Instance._from_db_object( + self.context, objects.Instance(), instance) + + self.compute_api.get(self.context, uuid, + want_objects=True).AndReturn(instance) + return instance + + def _test_locked_instance(self, action, method=None, body_map=None, + compute_api_args_map=None): + if method is None: + method = action + if body_map is None: + body_map = {} + if compute_api_args_map is None: + compute_api_args_map = {} + + instance = self._stub_instance_get() + args, kwargs = compute_api_args_map.get(action, ((), {})) + + getattr(compute_api.API, method)(self.context, instance, + *args, **kwargs).AndRaise( + exception.InstanceIsLocked(instance_uuid=instance['uuid'])) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance['uuid'], + {action: body_map.get(action)}) + self.assertEqual(409, res.status_int) + # Do these here instead of tearDown because this method is called + # more than once for the same test case + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def test_actions_with_locked_instance(self): + actions = ['resize', 'confirmResize', 'revertResize', 'reboot', + 'rebuild'] + + method_translations = {'confirmResize': 'confirm_resize', + 'revertResize': 'revert_resize'} + + body_map = {'resize': {'flavorRef': '2'}, + 'reboot': {'type': 'HARD'}, + 'rebuild': {'imageRef': self.image_uuid, + 'adminPass': 'TNc53Dr8s7vw'}} + + args_map = {'resize': (('2'), {}), + 'confirmResize': ((), {}), + 'reboot': (('HARD',), {}), + 'rebuild': ((self.image_uuid, 'TNc53Dr8s7vw'), + {'files_to_inject': None})} + + for action in actions: + method = method_translations.get(action) + self.mox.StubOutWithMock(compute_api.API, method or action) + self._test_locked_instance(action, method=method, + body_map=body_map, + compute_api_args_map=args_map) + + def test_server_change_password(self): + mock_method = MockSetAdminPassword() + self.stubs.Set(compute_api.API, 'set_admin_password', mock_method) + body = {'changePassword': {'adminPass': '1234pass'}} + + req = fakes.HTTPRequest.blank(self.url) + self.controller._action_change_password(req, FAKE_UUID, body) + + self.assertEqual(mock_method.instance_id, self.uuid) + self.assertEqual(mock_method.password, '1234pass') + + def test_server_change_password_pass_disabled(self): + # run with enable_instance_password disabled to verify adminPass + # is missing from response. See lp bug 921814 + self.flags(enable_instance_password=False) + + mock_method = MockSetAdminPassword() + self.stubs.Set(compute_api.API, 'set_admin_password', mock_method) + body = {'changePassword': {'adminPass': '1234pass'}} + + req = fakes.HTTPRequest.blank(self.url) + self.controller._action_change_password(req, FAKE_UUID, body) + + self.assertEqual(mock_method.instance_id, self.uuid) + # note,the mock still contains the password. + self.assertEqual(mock_method.password, '1234pass') + + def test_server_change_password_not_a_string(self): + body = {'changePassword': {'adminPass': 1234}} + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_change_password, + req, FAKE_UUID, body) + + def test_server_change_password_bad_request(self): + body = {'changePassword': {'pass': '12345'}} + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_change_password, + req, FAKE_UUID, body) + + def test_server_change_password_empty_string(self): + mock_method = MockSetAdminPassword() + self.stubs.Set(compute_api.API, 'set_admin_password', mock_method) + body = {'changePassword': {'adminPass': ''}} + + req = fakes.HTTPRequest.blank(self.url) + self.controller._action_change_password(req, FAKE_UUID, body) + + self.assertEqual(mock_method.instance_id, self.uuid) + self.assertEqual(mock_method.password, '') + + def test_server_change_password_none(self): + body = {'changePassword': {'adminPass': None}} + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_change_password, + req, FAKE_UUID, body) + + def test_reboot_hard(self): + body = dict(reboot=dict(type="HARD")) + req = fakes.HTTPRequest.blank(self.url) + self.controller._action_reboot(req, FAKE_UUID, body) + + def test_reboot_soft(self): + body = dict(reboot=dict(type="SOFT")) + req = fakes.HTTPRequest.blank(self.url) + self.controller._action_reboot(req, FAKE_UUID, body) + + def test_reboot_incorrect_type(self): + body = dict(reboot=dict(type="NOT_A_TYPE")) + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_reboot, + req, FAKE_UUID, body) + + def test_reboot_missing_type(self): + body = dict(reboot=dict()) + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_reboot, + req, FAKE_UUID, body) + + def test_reboot_none(self): + body = dict(reboot=dict(type=None)) + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_reboot, + req, FAKE_UUID, body) + + def test_reboot_not_found(self): + self.stubs.Set(db, 'instance_get_by_uuid', + return_server_not_found) + + body = dict(reboot=dict(type="HARD")) + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._action_reboot, + req, str(uuid.uuid4()), body) + + def test_reboot_raises_conflict_on_invalid_state(self): + body = dict(reboot=dict(type="HARD")) + + def fake_reboot(*args, **kwargs): + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + + self.stubs.Set(compute_api.API, 'reboot', fake_reboot) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_reboot, + req, FAKE_UUID, body) + + def test_reboot_soft_with_soft_in_progress_raises_conflict(self): + body = dict(reboot=dict(type="SOFT")) + req = fakes.HTTPRequest.blank(self.url) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE, + task_state=task_states.REBOOTING)) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_reboot, + req, FAKE_UUID, body) + + def test_reboot_hard_with_soft_in_progress_does_not_raise(self): + body = dict(reboot=dict(type="HARD")) + req = fakes.HTTPRequest.blank(self.url) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE, + task_state=task_states.REBOOTING)) + self.controller._action_reboot(req, FAKE_UUID, body) + + def test_reboot_hard_with_hard_in_progress_raises_conflict(self): + body = dict(reboot=dict(type="HARD")) + req = fakes.HTTPRequest.blank(self.url) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE, + task_state=task_states.REBOOTING_HARD)) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_reboot, + req, FAKE_UUID, body) + + def test_rebuild_preserve_ephemeral_is_ignored_when_ext_not_loaded(self): + return_server = fakes.fake_instance_get(image_ref='2', + vm_state=vm_states.ACTIVE, + host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + body = { + "rebuild": { + "imageRef": self._image_href, + "preserve_ephemeral": False, + }, + } + req = fakes.HTTPRequest.blank(self.url) + context = req.environ['nova.context'] + + self.mox.StubOutWithMock(compute_api.API, 'rebuild') + compute_api.API.rebuild(context, mox.IgnoreArg(), self._image_href, + mox.IgnoreArg(), files_to_inject=None) + self.mox.ReplayAll() + + self.controller._action_rebuild(req, FAKE_UUID, body) + + def _test_rebuild_preserve_ephemeral(self, value=None): + def fake_is_loaded(ext): + return ext == 'os-preserve-ephemeral-rebuild' + self.stubs.Set(self.controller.ext_mgr, 'is_loaded', fake_is_loaded) + + return_server = fakes.fake_instance_get(image_ref='2', + vm_state=vm_states.ACTIVE, + host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + body = { + "rebuild": { + "imageRef": self._image_href, + }, + } + if value is not None: + body['rebuild']['preserve_ephemeral'] = value + + req = fakes.HTTPRequest.blank(self.url) + context = req.environ['nova.context'] + + self.mox.StubOutWithMock(compute_api.API, 'rebuild') + + if value is not None: + compute_api.API.rebuild(context, mox.IgnoreArg(), self._image_href, + mox.IgnoreArg(), preserve_ephemeral=value, + files_to_inject=None) + else: + compute_api.API.rebuild(context, mox.IgnoreArg(), self._image_href, + mox.IgnoreArg(), files_to_inject=None) + self.mox.ReplayAll() + + self.controller._action_rebuild(req, FAKE_UUID, body) + + def test_rebuild_preserve_ephemeral_true(self): + self._test_rebuild_preserve_ephemeral(True) + + def test_rebuild_preserve_ephemeral_false(self): + self._test_rebuild_preserve_ephemeral(False) + + def test_rebuild_preserve_ephemeral_default(self): + self._test_rebuild_preserve_ephemeral() + + def test_rebuild_accepted_minimum(self): + return_server = fakes.fake_instance_get(image_ref='2', + vm_state=vm_states.ACTIVE, host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + self_href = 'http://localhost/v2/fake/servers/%s' % FAKE_UUID + + body = { + "rebuild": { + "imageRef": self._image_href, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + robj = self.controller._action_rebuild(req, FAKE_UUID, body) + body = robj.obj + + self.assertEqual(body['server']['image']['id'], '2') + self.assertEqual(len(body['server']['adminPass']), + CONF.password_length) + + self.assertEqual(robj['location'], self_href) + + def test_rebuild_instance_with_image_uuid(self): + info = dict(image_href_in_call=None) + + def rebuild(self2, context, instance, image_href, *args, **kwargs): + info['image_href_in_call'] = image_href + + self.stubs.Set(db, 'instance_get', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE)) + self.stubs.Set(compute_api.API, 'rebuild', rebuild) + + # proper local hrefs must start with 'http://localhost/v2/' + body = { + 'rebuild': { + 'imageRef': self.image_uuid, + }, + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers/a/action') + self.controller._action_rebuild(req, FAKE_UUID, body) + self.assertEqual(info['image_href_in_call'], self.image_uuid) + + def test_rebuild_instance_with_image_href_uses_uuid(self): + info = dict(image_href_in_call=None) + + def rebuild(self2, context, instance, image_href, *args, **kwargs): + info['image_href_in_call'] = image_href + + self.stubs.Set(db, 'instance_get', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE)) + self.stubs.Set(compute_api.API, 'rebuild', rebuild) + + # proper local hrefs must start with 'http://localhost/v2/' + body = { + 'rebuild': { + 'imageRef': self.image_href, + }, + } + + req = fakes.HTTPRequest.blank('/v2/fake/servers/a/action') + self.controller._action_rebuild(req, FAKE_UUID, body) + self.assertEqual(info['image_href_in_call'], self.image_uuid) + + def test_rebuild_accepted_minimum_pass_disabled(self): + # run with enable_instance_password disabled to verify adminPass + # is missing from response. See lp bug 921814 + self.flags(enable_instance_password=False) + + return_server = fakes.fake_instance_get(image_ref='2', + vm_state=vm_states.ACTIVE, host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + self_href = 'http://localhost/v2/fake/servers/%s' % FAKE_UUID + + body = { + "rebuild": { + "imageRef": self._image_href, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + robj = self.controller._action_rebuild(req, FAKE_UUID, body) + body = robj.obj + + self.assertEqual(body['server']['image']['id'], '2') + self.assertNotIn("adminPass", body['server']) + + self.assertEqual(robj['location'], self_href) + + def test_rebuild_raises_conflict_on_invalid_state(self): + body = { + "rebuild": { + "imageRef": self._image_href, + }, + } + + def fake_rebuild(*args, **kwargs): + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + + self.stubs.Set(compute_api.API, 'rebuild', fake_rebuild) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_rebuild, + req, FAKE_UUID, body) + + def test_rebuild_accepted_with_metadata(self): + metadata = {'new': 'metadata'} + + return_server = fakes.fake_instance_get(metadata=metadata, + vm_state=vm_states.ACTIVE, host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + body = { + "rebuild": { + "imageRef": self._image_href, + "metadata": metadata, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller._action_rebuild(req, FAKE_UUID, body).obj + + self.assertEqual(body['server']['metadata'], metadata) + + def test_rebuild_accepted_with_bad_metadata(self): + body = { + "rebuild": { + "imageRef": self._image_href, + "metadata": "stack", + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + req, FAKE_UUID, body) + + def test_rebuild_with_too_large_metadata(self): + body = { + "rebuild": { + "imageRef": self._image_href, + "metadata": { + 256 * "k": "value" + } + } + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller._action_rebuild, req, + FAKE_UUID, body) + + def test_rebuild_bad_entity(self): + body = { + "rebuild": { + "imageId": self._image_href, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + req, FAKE_UUID, body) + + def test_rebuild_bad_personality(self): + body = { + "rebuild": { + "imageRef": self._image_href, + "personality": [{ + "path": "/path/to/file", + "contents": "INVALID b64", + }] + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + req, FAKE_UUID, body) + + def test_rebuild_personality(self): + body = { + "rebuild": { + "imageRef": self._image_href, + "personality": [{ + "path": "/path/to/file", + "contents": base64.b64encode("Test String"), + }] + }, + } + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller._action_rebuild(req, FAKE_UUID, body).obj + + self.assertNotIn('personality', body['server']) + + def test_rebuild_admin_pass(self): + return_server = fakes.fake_instance_get(image_ref='2', + vm_state=vm_states.ACTIVE, host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + body = { + "rebuild": { + "imageRef": self._image_href, + "adminPass": "asdf", + }, + } + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller._action_rebuild(req, FAKE_UUID, body).obj + + self.assertEqual(body['server']['image']['id'], '2') + self.assertEqual(body['server']['adminPass'], 'asdf') + + def test_rebuild_admin_pass_pass_disabled(self): + # run with enable_instance_password disabled to verify adminPass + # is missing from response. See lp bug 921814 + self.flags(enable_instance_password=False) + + return_server = fakes.fake_instance_get(image_ref='2', + vm_state=vm_states.ACTIVE, host='fake_host') + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + body = { + "rebuild": { + "imageRef": self._image_href, + "adminPass": "asdf", + }, + } + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller._action_rebuild(req, FAKE_UUID, body).obj + + self.assertEqual(body['server']['image']['id'], '2') + self.assertNotIn('adminPass', body['server']) + + def test_rebuild_server_not_found(self): + def server_not_found(self, instance_id, + columns_to_join=None, use_slave=False): + raise exception.InstanceNotFound(instance_id=instance_id) + self.stubs.Set(db, 'instance_get_by_uuid', server_not_found) + + body = { + "rebuild": { + "imageRef": self._image_href, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._action_rebuild, + req, FAKE_UUID, body) + + def test_rebuild_with_bad_image(self): + body = { + "rebuild": { + "imageRef": "foo", + }, + } + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + req, FAKE_UUID, body) + + def test_rebuild_accessIP(self): + attributes = { + 'access_ip_v4': '172.19.0.1', + 'access_ip_v6': 'fe80::1', + } + + body = { + "rebuild": { + "imageRef": self._image_href, + "accessIPv4": "172.19.0.1", + "accessIPv6": "fe80::1", + }, + } + + data = {'changes': {}} + orig_get = compute_api.API.get + + def wrap_get(*args, **kwargs): + data['instance'] = orig_get(*args, **kwargs) + return data['instance'] + + def fake_save(context, **kwargs): + data['changes'].update(data['instance'].obj_get_changes()) + + self.stubs.Set(compute_api.API, 'get', wrap_get) + self.stubs.Set(objects.Instance, 'save', fake_save) + req = fakes.HTTPRequest.blank(self.url) + + self.controller._action_rebuild(req, FAKE_UUID, body) + + self.assertEqual(self._image_href, data['changes']['image_ref']) + self.assertEqual("", data['changes']['kernel_id']) + self.assertEqual("", data['changes']['ramdisk_id']) + self.assertEqual(task_states.REBUILDING, data['changes']['task_state']) + self.assertEqual(0, data['changes']['progress']) + for attr, value in attributes.items(): + self.assertEqual(value, str(data['changes'][attr])) + + def test_rebuild_when_kernel_not_exists(self): + + def return_image_meta(*args, **kwargs): + image_meta_table = { + '2': {'id': 2, 'status': 'active', 'container_format': 'ari'}, + '155d900f-4e14-4e4c-a73d-069cbf4541e6': + {'id': 3, 'status': 'active', 'container_format': 'raw', + 'properties': {'kernel_id': 1, 'ramdisk_id': 2}}, + } + image_id = args[2] + try: + image_meta = image_meta_table[str(image_id)] + except KeyError: + raise exception.ImageNotFound(image_id=image_id) + + return image_meta + + self.stubs.Set(fake._FakeImageService, 'show', return_image_meta) + body = { + "rebuild": { + "imageRef": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + }, + } + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + req, FAKE_UUID, body) + + def test_rebuild_proper_kernel_ram(self): + instance_meta = {'kernel_id': None, 'ramdisk_id': None} + + orig_get = compute_api.API.get + + def wrap_get(*args, **kwargs): + inst = orig_get(*args, **kwargs) + instance_meta['instance'] = inst + return inst + + def fake_save(context, **kwargs): + instance = instance_meta['instance'] + for key in instance_meta.keys(): + if key in instance.obj_what_changed(): + instance_meta[key] = instance[key] + + def return_image_meta(*args, **kwargs): + image_meta_table = { + '1': {'id': 1, 'status': 'active', 'container_format': 'aki'}, + '2': {'id': 2, 'status': 'active', 'container_format': 'ari'}, + '155d900f-4e14-4e4c-a73d-069cbf4541e6': + {'id': 3, 'status': 'active', 'container_format': 'raw', + 'properties': {'kernel_id': 1, 'ramdisk_id': 2}}, + } + image_id = args[2] + try: + image_meta = image_meta_table[str(image_id)] + except KeyError: + raise exception.ImageNotFound(image_id=image_id) + + return image_meta + + self.stubs.Set(fake._FakeImageService, 'show', return_image_meta) + self.stubs.Set(compute_api.API, 'get', wrap_get) + self.stubs.Set(objects.Instance, 'save', fake_save) + body = { + "rebuild": { + "imageRef": "155d900f-4e14-4e4c-a73d-069cbf4541e6", + }, + } + req = fakes.HTTPRequest.blank(self.url) + self.controller._action_rebuild(req, FAKE_UUID, body).obj + self.assertEqual(instance_meta['kernel_id'], '1') + self.assertEqual(instance_meta['ramdisk_id'], '2') + + @mock.patch.object(compute_api.API, 'rebuild') + def test_rebuild_instance_raise_auto_disk_config_exc(self, mock_rebuild): + body = { + "rebuild": { + "imageRef": self._image_href, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + mock_rebuild.side_effect = exception.AutoDiskConfigDisabledByImage( + image='dummy') + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + req, FAKE_UUID, body) + + def test_resize_server(self): + + body = dict(resize=dict(flavorRef="http://localhost/3")) + + self.resize_called = False + + def resize_mock(*args): + self.resize_called = True + + self.stubs.Set(compute_api.API, 'resize', resize_mock) + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller._action_resize(req, FAKE_UUID, body) + + self.assertEqual(self.resize_called, True) + + def test_resize_server_no_flavor(self): + body = dict(resize=dict()) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_resize, + req, FAKE_UUID, body) + + def test_resize_server_no_flavor_ref(self): + body = dict(resize=dict(flavorRef=None)) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_resize, + req, FAKE_UUID, body) + + def test_resize_with_server_not_found(self): + body = dict(resize=dict(flavorRef="http://localhost/3")) + + self.stubs.Set(compute_api.API, 'get', return_server_not_found) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._action_resize, + req, FAKE_UUID, body) + + def test_resize_with_image_exceptions(self): + body = dict(resize=dict(flavorRef="http://localhost/3")) + self.resize_called = 0 + image_id = 'fake_image_id' + + exceptions = [ + (exception.ImageNotAuthorized(image_id=image_id), + webob.exc.HTTPUnauthorized), + (exception.ImageNotFound(image_id=image_id), + webob.exc.HTTPBadRequest), + (exception.Invalid, webob.exc.HTTPBadRequest), + (exception.NoValidHost(reason='Bad host'), + webob.exc.HTTPBadRequest), + (exception.AutoDiskConfigDisabledByImage(image=image_id), + webob.exc.HTTPBadRequest), + ] + + raised, expected = map(iter, zip(*exceptions)) + + def _fake_resize(obj, context, instance, flavor_id): + self.resize_called += 1 + raise raised.next() + + self.stubs.Set(compute_api.API, 'resize', _fake_resize) + + for call_no in range(len(exceptions)): + req = fakes.HTTPRequest.blank(self.url) + next_exception = expected.next() + actual = self.assertRaises(next_exception, + self.controller._action_resize, + req, FAKE_UUID, body) + if (isinstance(exceptions[call_no][0], + exception.NoValidHost)): + self.assertEqual(actual.explanation, + 'No valid host was found. Bad host') + elif (isinstance(exceptions[call_no][0], + exception.AutoDiskConfigDisabledByImage)): + self.assertEqual(actual.explanation, + 'Requested image fake_image_id has automatic' + ' disk resize disabled.') + self.assertEqual(self.resize_called, call_no + 1) + + @mock.patch('nova.compute.api.API.resize', + side_effect=exception.CannotResizeDisk(reason='')) + def test_resize_raises_cannot_resize_disk(self, mock_resize): + body = dict(resize=dict(flavorRef="http://localhost/3")) + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_resize, + req, FAKE_UUID, body) + + @mock.patch('nova.compute.api.API.resize', + side_effect=exception.FlavorNotFound(reason='', + flavor_id='fake_id')) + def test_resize_raises_flavor_not_found(self, mock_resize): + body = dict(resize=dict(flavorRef="http://localhost/3")) + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_resize, + req, FAKE_UUID, body) + + def test_resize_with_too_many_instances(self): + body = dict(resize=dict(flavorRef="http://localhost/3")) + + def fake_resize(*args, **kwargs): + raise exception.TooManyInstances(message="TooManyInstance") + + self.stubs.Set(compute_api.API, 'resize', fake_resize) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPForbidden, + self.controller._action_resize, + req, FAKE_UUID, body) + + def test_resize_raises_conflict_on_invalid_state(self): + body = dict(resize=dict(flavorRef="http://localhost/3")) + + def fake_resize(*args, **kwargs): + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + + self.stubs.Set(compute_api.API, 'resize', fake_resize) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_resize, + req, FAKE_UUID, body) + + @mock.patch('nova.compute.api.API.resize', + side_effect=exception.NoValidHost(reason='')) + def test_resize_raises_no_valid_host(self, mock_resize): + body = dict(resize=dict(flavorRef="http://localhost/3")) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_resize, + req, FAKE_UUID, body) + + @mock.patch.object(compute_api.API, 'resize') + def test_resize_instance_raise_auto_disk_config_exc(self, mock_resize): + mock_resize.side_effect = exception.AutoDiskConfigDisabledByImage( + image='dummy') + + body = dict(resize=dict(flavorRef="http://localhost/3")) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_resize, + req, FAKE_UUID, body) + + def test_confirm_resize_server(self): + body = dict(confirmResize=None) + + self.confirm_resize_called = False + + def cr_mock(*args): + self.confirm_resize_called = True + + self.stubs.Set(compute_api.API, 'confirm_resize', cr_mock) + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller._action_confirm_resize(req, FAKE_UUID, body) + + self.assertEqual(self.confirm_resize_called, True) + + def test_confirm_resize_migration_not_found(self): + body = dict(confirmResize=None) + + def confirm_resize_mock(*args): + raise exception.MigrationNotFoundByStatus(instance_id=1, + status='finished') + + self.stubs.Set(compute_api.API, + 'confirm_resize', + confirm_resize_mock) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_confirm_resize, + req, FAKE_UUID, body) + + def test_confirm_resize_raises_conflict_on_invalid_state(self): + body = dict(confirmResize=None) + + def fake_confirm_resize(*args, **kwargs): + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + + self.stubs.Set(compute_api.API, 'confirm_resize', + fake_confirm_resize) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_confirm_resize, + req, FAKE_UUID, body) + + def test_revert_resize_migration_not_found(self): + body = dict(revertResize=None) + + def revert_resize_mock(*args): + raise exception.MigrationNotFoundByStatus(instance_id=1, + status='finished') + + self.stubs.Set(compute_api.API, + 'revert_resize', + revert_resize_mock) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_revert_resize, + req, FAKE_UUID, body) + + def test_revert_resize_server_not_found(self): + body = dict(revertResize=None) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob. exc.HTTPNotFound, + self.controller._action_revert_resize, + req, "bad_server_id", body) + + def test_revert_resize_server(self): + body = dict(revertResize=None) + + self.revert_resize_called = False + + def revert_mock(*args): + self.revert_resize_called = True + + self.stubs.Set(compute_api.API, 'revert_resize', revert_mock) + + req = fakes.HTTPRequest.blank(self.url) + body = self.controller._action_revert_resize(req, FAKE_UUID, body) + + self.assertEqual(self.revert_resize_called, True) + + def test_revert_resize_raises_conflict_on_invalid_state(self): + body = dict(revertResize=None) + + def fake_revert_resize(*args, **kwargs): + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + + self.stubs.Set(compute_api.API, 'revert_resize', + fake_revert_resize) + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_revert_resize, + req, FAKE_UUID, body) + + def test_create_image(self): + body = { + 'createImage': { + 'name': 'Snapshot 1', + }, + } + + req = fakes.HTTPRequest.blank(self.url) + response = self.controller._action_create_image(req, FAKE_UUID, body) + + location = response.headers['Location'] + self.assertEqual('http://localhost/v2/fake/images/123', location) + + def test_create_image_glance_link_prefix(self): + self.flags(osapi_glance_link_prefix='https://glancehost') + body = { + 'createImage': { + 'name': 'Snapshot 1', + }, + } + + req = fakes.HTTPRequest.blank(self.url) + response = self.controller._action_create_image(req, FAKE_UUID, body) + + location = response.headers['Location'] + self.assertEqual('https://glancehost/v2/fake/images/123', location) + + def test_create_image_name_too_long(self): + long_name = 'a' * 260 + body = { + 'createImage': { + 'name': long_name, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_create_image, req, + FAKE_UUID, body) + + def _do_test_create_volume_backed_image(self, extra_properties): + + def _fake_id(x): + return '%s-%s-%s-%s' % (x * 8, x * 4, x * 4, x * 12) + + body = dict(createImage=dict(name='snapshot_of_volume_backed')) + + if extra_properties: + body['createImage']['metadata'] = extra_properties + + image_service = glance.get_default_image_service() + + bdm = [dict(volume_id=_fake_id('a'), + volume_size=1, + device_name='vda', + delete_on_termination=False)] + props = dict(kernel_id=_fake_id('b'), + ramdisk_id=_fake_id('c'), + root_device_name='/dev/vda', + block_device_mapping=bdm) + original_image = dict(properties=props, + container_format='ami', + status='active', + is_public=True) + + image_service.create(None, original_image) + + def fake_block_device_mapping_get_all_by_instance(context, inst_id, + use_slave=False): + return [fake_block_device.FakeDbBlockDeviceDict( + {'volume_id': _fake_id('a'), + 'source_type': 'snapshot', + 'destination_type': 'volume', + 'volume_size': 1, + 'device_name': 'vda', + 'snapshot_id': 1, + 'boot_index': 0, + 'delete_on_termination': False, + 'no_device': None})] + + self.stubs.Set(db, 'block_device_mapping_get_all_by_instance', + fake_block_device_mapping_get_all_by_instance) + + instance = fakes.fake_instance_get(image_ref=original_image['id'], + vm_state=vm_states.ACTIVE, + root_device_name='/dev/vda') + self.stubs.Set(db, 'instance_get_by_uuid', instance) + + volume = dict(id=_fake_id('a'), + size=1, + host='fake', + display_description='fake') + snapshot = dict(id=_fake_id('d')) + self.mox.StubOutWithMock(self.controller.compute_api, 'volume_api') + volume_api = self.controller.compute_api.volume_api + volume_api.get(mox.IgnoreArg(), volume['id']).AndReturn(volume) + volume_api.create_snapshot_force(mox.IgnoreArg(), volume['id'], + mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(snapshot) + + self.mox.ReplayAll() + + req = fakes.HTTPRequest.blank(self.url) + response = self.controller._action_create_image(req, FAKE_UUID, body) + + location = response.headers['Location'] + image_id = location.replace('http://localhost/v2/fake/images/', '') + image = image_service.show(None, image_id) + + self.assertEqual(image['name'], 'snapshot_of_volume_backed') + properties = image['properties'] + self.assertEqual(properties['kernel_id'], _fake_id('b')) + self.assertEqual(properties['ramdisk_id'], _fake_id('c')) + self.assertEqual(properties['root_device_name'], '/dev/vda') + self.assertEqual(properties['bdm_v2'], True) + bdms = properties['block_device_mapping'] + self.assertEqual(len(bdms), 1) + self.assertEqual(bdms[0]['boot_index'], 0) + self.assertEqual(bdms[0]['source_type'], 'snapshot') + self.assertEqual(bdms[0]['destination_type'], 'volume') + self.assertEqual(bdms[0]['snapshot_id'], snapshot['id']) + for fld in ('connection_info', 'id', + 'instance_uuid', 'device_name'): + self.assertNotIn(fld, bdms[0]) + for k in extra_properties.keys(): + self.assertEqual(properties[k], extra_properties[k]) + + def test_create_volume_backed_image_no_metadata(self): + self._do_test_create_volume_backed_image({}) + + def test_create_volume_backed_image_with_metadata(self): + self._do_test_create_volume_backed_image(dict(ImageType='Gold', + ImageVersion='2.0')) + + def _test_create_volume_backed_image_with_metadata_from_volume( + self, extra_metadata=None): + + def _fake_id(x): + return '%s-%s-%s-%s' % (x * 8, x * 4, x * 4, x * 12) + + body = dict(createImage=dict(name='snapshot_of_volume_backed')) + if extra_metadata: + body['createImage']['metadata'] = extra_metadata + + image_service = glance.get_default_image_service() + + def fake_block_device_mapping_get_all_by_instance(context, inst_id, + use_slave=False): + return [fake_block_device.FakeDbBlockDeviceDict( + {'volume_id': _fake_id('a'), + 'source_type': 'snapshot', + 'destination_type': 'volume', + 'volume_size': 1, + 'device_name': 'vda', + 'snapshot_id': 1, + 'boot_index': 0, + 'delete_on_termination': False, + 'no_device': None})] + + self.stubs.Set(db, 'block_device_mapping_get_all_by_instance', + fake_block_device_mapping_get_all_by_instance) + + instance = fakes.fake_instance_get(image_ref='', + vm_state=vm_states.ACTIVE, + root_device_name='/dev/vda') + self.stubs.Set(db, 'instance_get_by_uuid', instance) + + fake_metadata = {'test_key1': 'test_value1', + 'test_key2': 'test_value2'} + volume = dict(id=_fake_id('a'), + size=1, + host='fake', + display_description='fake', + volume_image_metadata=fake_metadata) + snapshot = dict(id=_fake_id('d')) + self.mox.StubOutWithMock(self.controller.compute_api, 'volume_api') + volume_api = self.controller.compute_api.volume_api + volume_api.get(mox.IgnoreArg(), volume['id']).AndReturn(volume) + volume_api.get(mox.IgnoreArg(), volume['id']).AndReturn(volume) + volume_api.create_snapshot_force(mox.IgnoreArg(), volume['id'], + mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(snapshot) + + req = fakes.HTTPRequest.blank(self.url) + + self.mox.ReplayAll() + response = self.controller._action_create_image(req, FAKE_UUID, body) + location = response.headers['Location'] + image_id = location.replace('http://localhost/v2/fake/images/', '') + image = image_service.show(None, image_id) + + properties = image['properties'] + self.assertEqual(properties['test_key1'], 'test_value1') + self.assertEqual(properties['test_key2'], 'test_value2') + if extra_metadata: + for key, val in extra_metadata.items(): + self.assertEqual(properties[key], val) + + def test_create_vol_backed_img_with_meta_from_vol_without_extra_meta(self): + self._test_create_volume_backed_image_with_metadata_from_volume() + + def test_create_vol_backed_img_with_meta_from_vol_with_extra_meta(self): + self._test_create_volume_backed_image_with_metadata_from_volume( + extra_metadata={'a': 'b'}) + + def test_create_image_snapshots_disabled(self): + """Don't permit a snapshot if the allow_instance_snapshots flag is + False + """ + self.flags(allow_instance_snapshots=False) + body = { + 'createImage': { + 'name': 'Snapshot 1', + }, + } + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_create_image, + req, FAKE_UUID, body) + + def test_create_image_with_metadata(self): + body = { + 'createImage': { + 'name': 'Snapshot 1', + 'metadata': {'key': 'asdf'}, + }, + } + + req = fakes.HTTPRequest.blank(self.url) + response = self.controller._action_create_image(req, FAKE_UUID, body) + + location = response.headers['Location'] + self.assertEqual('http://localhost/v2/fake/images/123', location) + + def test_create_image_with_too_much_metadata(self): + body = { + 'createImage': { + 'name': 'Snapshot 1', + 'metadata': {}, + }, + } + for num in range(CONF.quota_metadata_items + 1): + body['createImage']['metadata']['foo%i' % num] = "bar" + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPForbidden, + self.controller._action_create_image, + req, FAKE_UUID, body) + + def test_create_image_no_name(self): + body = { + 'createImage': {}, + } + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_create_image, + req, FAKE_UUID, body) + + def test_create_image_blank_name(self): + body = { + 'createImage': { + 'name': '', + } + } + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_create_image, + req, FAKE_UUID, body) + + def test_create_image_bad_metadata(self): + body = { + 'createImage': { + 'name': 'geoff', + 'metadata': 'henry', + }, + } + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_create_image, + req, FAKE_UUID, body) + + def test_create_image_raises_conflict_on_invalid_state(self): + def snapshot(*args, **kwargs): + raise exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + self.stubs.Set(compute_api.API, 'snapshot', snapshot) + + body = { + "createImage": { + "name": "test_snapshot", + }, + } + + req = fakes.HTTPRequest.blank(self.url) + self.assertRaises(webob.exc.HTTPConflict, + self.controller._action_create_image, + req, FAKE_UUID, body) + + +class TestServerActionXMLDeserializer(test.TestCase): + + def setUp(self): + super(TestServerActionXMLDeserializer, self).setUp() + self.deserializer = servers.ActionDeserializer() + + def test_create_image(self): + serial_request = """ +<createImage xmlns="http://docs.openstack.org/compute/api/v1.1" + name="new-server-test"/>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "createImage": { + "name": "new-server-test", + }, + } + self.assertEqual(request['body'], expected) + + def test_create_image_with_metadata(self): + serial_request = """ +<createImage xmlns="http://docs.openstack.org/compute/api/v1.1" + name="new-server-test"> + <metadata> + <meta key="key1">value1</meta> + </metadata> +</createImage>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "createImage": { + "name": "new-server-test", + "metadata": {"key1": "value1"}, + }, + } + self.assertEqual(request['body'], expected) + + def test_change_pass(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <changePassword + xmlns="http://docs.openstack.org/compute/api/v1.1" + adminPass="1234pass"/> """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "changePassword": { + "adminPass": "1234pass", + }, + } + self.assertEqual(request['body'], expected) + + def test_change_pass_no_pass(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <changePassword + xmlns="http://docs.openstack.org/compute/api/v1.1"/> """ + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_change_pass_empty_pass(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <changePassword + xmlns="http://docs.openstack.org/compute/api/v1.1" + adminPass=""/> """ + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "changePassword": { + "adminPass": "", + }, + } + self.assertEqual(request['body'], expected) + + def test_reboot(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <reboot + xmlns="http://docs.openstack.org/compute/api/v1.1" + type="HARD"/>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "reboot": { + "type": "HARD", + }, + } + self.assertEqual(request['body'], expected) + + def test_reboot_no_type(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <reboot + xmlns="http://docs.openstack.org/compute/api/v1.1"/>""" + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_resize(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <resize + xmlns="http://docs.openstack.org/compute/api/v1.1" + flavorRef="http://localhost/flavors/3"/>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "resize": {"flavorRef": "http://localhost/flavors/3"}, + } + self.assertEqual(request['body'], expected) + + def test_resize_no_flavor_ref(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <resize + xmlns="http://docs.openstack.org/compute/api/v1.1"/>""" + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_confirm_resize(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <confirmResize + xmlns="http://docs.openstack.org/compute/api/v1.1"/>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "confirmResize": None, + } + self.assertEqual(request['body'], expected) + + def test_revert_resize(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <revertResize + xmlns="http://docs.openstack.org/compute/api/v1.1"/>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "revertResize": None, + } + self.assertEqual(request['body'], expected) + + def test_rebuild(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <rebuild + xmlns="http://docs.openstack.org/compute/api/v1.1" + name="new-server-test" + imageRef="http://localhost/images/1"> + <metadata> + <meta key="My Server Name">Apache1</meta> + </metadata> + <personality> + <file path="/etc/banner.txt">Mg==</file> + </personality> + </rebuild>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "rebuild": { + "name": "new-server-test", + "imageRef": "http://localhost/images/1", + "metadata": { + "My Server Name": "Apache1", + }, + "personality": [ + {"path": "/etc/banner.txt", "contents": "Mg=="}, + ], + }, + } + self.assertThat(request['body'], matchers.DictMatches(expected)) + + def test_rebuild_minimum(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <rebuild + xmlns="http://docs.openstack.org/compute/api/v1.1" + imageRef="http://localhost/images/1"/>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "rebuild": { + "imageRef": "http://localhost/images/1", + }, + } + self.assertThat(request['body'], matchers.DictMatches(expected)) + + def test_rebuild_no_imageRef(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <rebuild + xmlns="http://docs.openstack.org/compute/api/v1.1" + name="new-server-test"> + <metadata> + <meta key="My Server Name">Apache1</meta> + </metadata> + <personality> + <file path="/etc/banner.txt">Mg==</file> + </personality> + </rebuild>""" + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_rebuild_blank_name(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <rebuild + xmlns="http://docs.openstack.org/compute/api/v1.1" + imageRef="http://localhost/images/1" + name=""/>""" + self.assertRaises(AttributeError, + self.deserializer.deserialize, + serial_request, + 'action') + + def test_rebuild_preserve_ephemeral_passed(self): + serial_request = """<?xml version="1.0" encoding="UTF-8"?> + <rebuild + xmlns="http://docs.openstack.org/compute/api/v1.1" + imageRef="http://localhost/images/1" + preserve_ephemeral="true"/>""" + request = self.deserializer.deserialize(serial_request, 'action') + expected = { + "rebuild": { + "imageRef": "http://localhost/images/1", + "preserve_ephemeral": True, + }, + } + self.assertThat(request['body'], matchers.DictMatches(expected)) + + def test_corrupt_xml(self): + """Should throw a 400 error on corrupt xml.""" + self.assertRaises( + exception.MalformedRequestBody, + self.deserializer.deserialize, + utils.killer_xml_body()) diff --git a/nova/tests/unit/api/openstack/compute/test_server_metadata.py b/nova/tests/unit/api/openstack/compute/test_server_metadata.py new file mode 100644 index 0000000000..ba9126f0f1 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_server_metadata.py @@ -0,0 +1,771 @@ +# Copyright 2011 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 uuid + +import mock +from oslo.config import cfg +from oslo.serialization import jsonutils +from oslo.utils import timeutils +import six +import webob + +from nova.api.openstack.compute.plugins.v3 import server_metadata \ + as server_metadata_v21 +from nova.api.openstack.compute import server_metadata as server_metadata_v2 +from nova.compute import rpcapi as compute_rpcapi +from nova.compute import vm_states +import nova.db +from nova import exception +from nova import objects +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance + + +CONF = cfg.CONF + + +def return_create_instance_metadata_max(context, server_id, metadata, delete): + return stub_max_server_metadata() + + +def return_create_instance_metadata(context, server_id, metadata, delete): + return stub_server_metadata() + + +def fake_instance_save(inst, **kwargs): + inst.metadata = stub_server_metadata() + inst.obj_reset_changes() + + +def return_server_metadata(context, server_id): + if not isinstance(server_id, six.string_types) or not len(server_id) == 36: + msg = 'id %s must be a uuid in return server metadata' % server_id + raise Exception(msg) + return stub_server_metadata() + + +def return_empty_server_metadata(context, server_id): + return {} + + +def delete_server_metadata(context, server_id, key): + pass + + +def stub_server_metadata(): + metadata = { + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + return metadata + + +def stub_max_server_metadata(): + metadata = {"metadata": {}} + for num in range(CONF.quota_metadata_items): + metadata['metadata']['key%i' % num] = "blah" + return metadata + + +def return_server(context, server_id, columns_to_join=None): + return fake_instance.fake_db_instance( + **{'id': server_id, + 'uuid': '0cc3346e-9fef-4445-abe6-5d2b2690ec64', + 'name': 'fake', + 'locked': False, + 'launched_at': timeutils.utcnow(), + 'vm_state': vm_states.ACTIVE}) + + +def return_server_by_uuid(context, server_uuid, + columns_to_join=None, use_slave=False): + return fake_instance.fake_db_instance( + **{'id': 1, + 'uuid': '0cc3346e-9fef-4445-abe6-5d2b2690ec64', + 'name': 'fake', + 'locked': False, + 'launched_at': timeutils.utcnow(), + 'metadata': stub_server_metadata(), + 'vm_state': vm_states.ACTIVE}) + + +def return_server_nonexistent(context, server_id, + columns_to_join=None, use_slave=False): + raise exception.InstanceNotFound(instance_id=server_id) + + +def fake_change_instance_metadata(self, context, instance, diff): + pass + + +class ServerMetaDataTestV21(test.TestCase): + validation_ex = exception.ValidationError + validation_ex_large = validation_ex + + def setUp(self): + super(ServerMetaDataTestV21, self).setUp() + fakes.stub_out_key_pair_funcs(self.stubs) + self.stubs.Set(nova.db, 'instance_get', return_server) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_by_uuid) + + self.stubs.Set(nova.db, 'instance_metadata_get', + return_server_metadata) + + self.stubs.Set(compute_rpcapi.ComputeAPI, 'change_instance_metadata', + fake_change_instance_metadata) + self._set_up_resources() + + def _set_up_resources(self): + self.controller = server_metadata_v21.ServerMetadataController() + self.uuid = str(uuid.uuid4()) + self.url = '/fake/servers/%s/metadata' % self.uuid + + def _get_request(self, param_url=''): + return fakes.HTTPRequestV3.blank(self.url + param_url) + + def test_index(self): + req = self._get_request() + res_dict = self.controller.index(req, self.uuid) + + expected = { + 'metadata': { + 'key1': 'value1', + 'key2': 'value2', + 'key3': 'value3', + }, + } + self.assertEqual(expected, res_dict) + + def test_index_nonexistent_server(self): + self.stubs.Set(nova.db, 'instance_metadata_get', + return_server_nonexistent) + req = self._get_request() + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.index, req, self.url) + + def test_index_no_data(self): + self.stubs.Set(nova.db, 'instance_metadata_get', + return_empty_server_metadata) + req = self._get_request() + res_dict = self.controller.index(req, self.uuid) + expected = {'metadata': {}} + self.assertEqual(expected, res_dict) + + def test_show(self): + req = self._get_request('/key2') + res_dict = self.controller.show(req, self.uuid, 'key2') + expected = {"meta": {'key2': 'value2'}} + self.assertEqual(expected, res_dict) + + def test_show_nonexistent_server(self): + self.stubs.Set(nova.db, 'instance_metadata_get', + return_server_nonexistent) + req = self._get_request('/key2') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, self.uuid, 'key2') + + def test_show_meta_not_found(self): + self.stubs.Set(nova.db, 'instance_metadata_get', + return_empty_server_metadata) + req = self._get_request('/key6') + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.show, req, self.uuid, 'key6') + + def test_delete(self): + self.stubs.Set(nova.db, 'instance_metadata_get', + return_server_metadata) + self.stubs.Set(nova.db, 'instance_metadata_delete', + delete_server_metadata) + req = self._get_request('/key2') + req.method = 'DELETE' + res = self.controller.delete(req, self.uuid, 'key2') + + self.assertIsNone(res) + + def test_delete_nonexistent_server(self): + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_nonexistent) + req = self._get_request('/key1') + req.method = 'DELETE' + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, req, self.uuid, 'key1') + + def test_delete_meta_not_found(self): + self.stubs.Set(nova.db, 'instance_metadata_get', + return_empty_server_metadata) + req = self._get_request('/key6') + req.method = 'DELETE' + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, req, self.uuid, 'key6') + + def test_create(self): + self.stubs.Set(objects.Instance, 'save', fake_instance_save) + req = self._get_request() + req.method = 'POST' + req.content_type = "application/json" + body = {"metadata": {"key9": "value9"}} + req.body = jsonutils.dumps(body) + res_dict = self.controller.create(req, self.uuid, body=body) + + body['metadata'].update({ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }) + self.assertEqual(body, res_dict) + + def test_create_empty_body(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request() + req.method = 'POST' + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.create, req, self.uuid, body=None) + + def test_create_item_empty_key(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request('/key1') + req.method = 'PUT' + body = {"metadata": {"": "value1"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.create, req, self.uuid, body=body) + + def test_create_item_non_dict(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request('/key1') + req.method = 'PUT' + body = {"metadata": None} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.create, req, self.uuid, body=body) + + def test_create_item_key_too_long(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request('/key1') + req.method = 'PUT' + body = {"metadata": {("a" * 260): "value1"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex_large, + self.controller.create, + req, self.uuid, body=body) + + def test_create_malformed_container(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url + '/key1') + req.method = 'PUT' + body = {"meta": {}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.create, req, self.uuid, body=body) + + def test_create_malformed_data(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url + '/key1') + req.method = 'PUT' + body = {"metadata": ['asdf']} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.create, req, self.uuid, body=body) + + def test_create_nonexistent_server(self): + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_nonexistent) + req = self._get_request() + req.method = 'POST' + body = {"metadata": {"key1": "value1"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.create, req, self.uuid, body=body) + + def test_update_metadata(self): + self.stubs.Set(objects.Instance, 'save', fake_instance_save) + req = self._get_request() + req.method = 'POST' + req.content_type = 'application/json' + expected = { + 'metadata': { + 'key1': 'updatedvalue', + 'key29': 'newkey', + } + } + req.body = jsonutils.dumps(expected) + response = self.controller.update_all(req, self.uuid, body=expected) + self.assertEqual(expected, response) + + def test_update_all(self): + self.stubs.Set(objects.Instance, 'save', fake_instance_save) + req = self._get_request() + req.method = 'PUT' + req.content_type = "application/json" + expected = { + 'metadata': { + 'key10': 'value10', + 'key99': 'value99', + }, + } + req.body = jsonutils.dumps(expected) + res_dict = self.controller.update_all(req, self.uuid, body=expected) + + self.assertEqual(expected, res_dict) + + def test_update_all_empty_container(self): + self.stubs.Set(objects.Instance, 'save', fake_instance_save) + req = self._get_request() + req.method = 'PUT' + req.content_type = "application/json" + expected = {'metadata': {}} + req.body = jsonutils.dumps(expected) + res_dict = self.controller.update_all(req, self.uuid, body=expected) + + self.assertEqual(expected, res_dict) + + def test_update_all_empty_body_item(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url + '/key1') + req.method = 'PUT' + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.update_all, req, self.uuid, + body=None) + + def test_update_all_with_non_dict_item(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url + '/bad') + req.method = 'PUT' + body = {"metadata": None} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.update_all, req, self.uuid, + body=body) + + def test_update_all_malformed_container(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request() + req.method = 'PUT' + req.content_type = "application/json" + expected = {'meta': {}} + req.body = jsonutils.dumps(expected) + + self.assertRaises(self.validation_ex, + self.controller.update_all, req, self.uuid, + body=expected) + + def test_update_all_malformed_data(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request() + req.method = 'PUT' + req.content_type = "application/json" + expected = {'metadata': ['asdf']} + req.body = jsonutils.dumps(expected) + + self.assertRaises(self.validation_ex, + self.controller.update_all, req, self.uuid, + body=expected) + + def test_update_all_nonexistent_server(self): + self.stubs.Set(nova.db, 'instance_get', return_server_nonexistent) + req = self._get_request() + req.method = 'PUT' + req.content_type = "application/json" + body = {'metadata': {'key10': 'value10'}} + req.body = jsonutils.dumps(body) + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update_all, req, '100', body=body) + + def test_update_all_non_dict(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request() + req.method = 'PUT' + body = {"metadata": None} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, self.controller.update_all, + req, self.uuid, body=body) + + def test_update_item(self): + self.stubs.Set(objects.Instance, 'save', fake_instance_save) + req = self._get_request('/key1') + req.method = 'PUT' + body = {"meta": {"key1": "value1"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + res_dict = self.controller.update(req, self.uuid, 'key1', body=body) + expected = {"meta": {'key1': 'value1'}} + self.assertEqual(expected, res_dict) + + def test_update_item_nonexistent_server(self): + self.stubs.Set(nova.db, 'instance_get_by_uuid', + return_server_nonexistent) + req = self._get_request('/key1') + req.method = 'PUT' + body = {"meta": {"key1": "value1"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.update, req, self.uuid, 'key1', + body=body) + + def test_update_item_empty_body(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request('/key1') + req.method = 'PUT' + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.update, req, self.uuid, 'key1', + body=None) + + def test_update_malformed_container(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url) + req.method = 'PUT' + expected = {'meta': {}} + req.body = jsonutils.dumps(expected) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.update, req, self.uuid, 'key1', + body=expected) + + def test_update_malformed_data(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url) + req.method = 'PUT' + expected = {'metadata': ['asdf']} + req.body = jsonutils.dumps(expected) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.update, req, self.uuid, 'key1', + body=expected) + + def test_update_item_empty_key(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request('/key1') + req.method = 'PUT' + body = {"meta": {"": "value1"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.update, req, self.uuid, '', + body=body) + + def test_update_item_key_too_long(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request('/key1') + req.method = 'PUT' + body = {"meta": {("a" * 260): "value1"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex_large, + self.controller.update, + req, self.uuid, ("a" * 260), body=body) + + def test_update_item_value_too_long(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request('/key1') + req.method = 'PUT' + body = {"meta": {"key1": ("a" * 260)}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex_large, + self.controller.update, + req, self.uuid, "key1", body=body) + + def test_update_item_too_many_keys(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request('/key1') + req.method = 'PUT' + body = {"meta": {"key1": "value1", "key2": "value2"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.update, req, self.uuid, 'key1', + body=body) + + def test_update_item_body_uri_mismatch(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request('/bad') + req.method = 'PUT' + body = {"meta": {"key1": "value1"}} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, self.uuid, 'bad', + body=body) + + def test_update_item_non_dict(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request('/bad') + req.method = 'PUT' + body = {"meta": None} + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.update, req, self.uuid, 'bad', + body=body) + + def test_update_empty_container(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = fakes.HTTPRequest.blank(self.url) + req.method = 'PUT' + expected = {'metadata': {}} + req.body = jsonutils.dumps(expected) + req.headers["content-type"] = "application/json" + + self.assertRaises(self.validation_ex, + self.controller.update, req, self.uuid, 'bad', + body=expected) + + def test_too_many_metadata_items_on_create(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + data = {"metadata": {}} + for num in range(CONF.quota_metadata_items + 1): + data['metadata']['key%i' % num] = "blah" + req = self._get_request() + req.method = 'POST' + req.body = jsonutils.dumps(data) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.create, req, self.uuid, body=data) + + def test_invalid_metadata_items_on_create(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request() + req.method = 'POST' + req.headers["content-type"] = "application/json" + + # test for long key + data = {"metadata": {"a" * 260: "value1"}} + req.body = jsonutils.dumps(data) + self.assertRaises(self.validation_ex_large, + self.controller.create, req, self.uuid, body=data) + + # test for long value + data = {"metadata": {"key": "v" * 260}} + req.body = jsonutils.dumps(data) + self.assertRaises(self.validation_ex_large, + self.controller.create, req, self.uuid, body=data) + + # test for empty key. + data = {"metadata": {"": "value1"}} + req.body = jsonutils.dumps(data) + self.assertRaises(self.validation_ex, + self.controller.create, req, self.uuid, body=data) + + def test_too_many_metadata_items_on_update_item(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + data = {"metadata": {}} + for num in range(CONF.quota_metadata_items + 1): + data['metadata']['key%i' % num] = "blah" + req = self._get_request() + req.method = 'PUT' + req.body = jsonutils.dumps(data) + req.headers["content-type"] = "application/json" + + self.assertRaises(webob.exc.HTTPForbidden, self.controller.update_all, + req, self.uuid, body=data) + + def test_invalid_metadata_items_on_update_item(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + data = {"metadata": {}} + for num in range(CONF.quota_metadata_items + 1): + data['metadata']['key%i' % num] = "blah" + req = self._get_request() + req.method = 'PUT' + req.body = jsonutils.dumps(data) + req.headers["content-type"] = "application/json" + + # test for long key + data = {"metadata": {"a" * 260: "value1"}} + req.body = jsonutils.dumps(data) + self.assertRaises(self.validation_ex_large, + self.controller.update_all, req, self.uuid, + body=data) + + # test for long value + data = {"metadata": {"key": "v" * 260}} + req.body = jsonutils.dumps(data) + self.assertRaises(self.validation_ex_large, + self.controller.update_all, req, self.uuid, + body=data) + + # test for empty key. + data = {"metadata": {"": "value1"}} + req.body = jsonutils.dumps(data) + self.assertRaises(self.validation_ex, + self.controller.update_all, req, self.uuid, + body=data) + + +class ServerMetaDataTestV2(ServerMetaDataTestV21): + validation_ex = webob.exc.HTTPBadRequest + validation_ex_large = webob.exc.HTTPRequestEntityTooLarge + + def _set_up_resources(self): + self.controller = server_metadata_v2.Controller() + self.uuid = str(uuid.uuid4()) + self.url = '/v1.1/fake/servers/%s/metadata' % self.uuid + + def _get_request(self, param_url=''): + return fakes.HTTPRequest.blank(self.url + param_url) + + +class BadStateServerMetaDataTestV21(test.TestCase): + + def setUp(self): + super(BadStateServerMetaDataTestV21, self).setUp() + fakes.stub_out_key_pair_funcs(self.stubs) + self.stubs.Set(nova.db, 'instance_metadata_get', + return_server_metadata) + self.stubs.Set(compute_rpcapi.ComputeAPI, 'change_instance_metadata', + fake_change_instance_metadata) + self.stubs.Set(nova.db, 'instance_get', self._return_server_in_build) + self.stubs.Set(nova.db, 'instance_get_by_uuid', + self._return_server_in_build_by_uuid) + self.stubs.Set(nova.db, 'instance_metadata_delete', + delete_server_metadata) + self._set_up_resources() + + def _set_up_resources(self): + self.controller = server_metadata_v21.ServerMetadataController() + self.uuid = str(uuid.uuid4()) + self.url = '/fake/servers/%s/metadata' % self.uuid + + def _get_request(self, param_url=''): + return fakes.HTTPRequestV3.blank(self.url + param_url) + + def test_invalid_state_on_delete(self): + req = self._get_request('/key2') + req.method = 'DELETE' + self.assertRaises(webob.exc.HTTPConflict, self.controller.delete, + req, self.uuid, 'key2') + + def test_invalid_state_on_update_metadata(self): + self.stubs.Set(nova.db, 'instance_metadata_update', + return_create_instance_metadata) + req = self._get_request() + req.method = 'POST' + req.content_type = 'application/json' + expected = { + 'metadata': { + 'key1': 'updatedvalue', + 'key29': 'newkey', + } + } + req.body = jsonutils.dumps(expected) + self.assertRaises(webob.exc.HTTPConflict, self.controller.update_all, + req, self.uuid, body=expected) + + def _return_server_in_build(self, context, server_id, + columns_to_join=None): + return fake_instance.fake_db_instance( + **{'id': server_id, + 'uuid': '0cc3346e-9fef-4445-abe6-5d2b2690ec64', + 'name': 'fake', + 'locked': False, + 'vm_state': vm_states.BUILDING}) + + def _return_server_in_build_by_uuid(self, context, server_uuid, + columns_to_join=None, use_slave=False): + return fake_instance.fake_db_instance( + **{'id': 1, + 'uuid': '0cc3346e-9fef-4445-abe6-5d2b2690ec64', + 'name': 'fake', + 'locked': False, + 'vm_state': vm_states.BUILDING}) + + @mock.patch.object(nova.compute.api.API, 'update_instance_metadata', + side_effect=exception.InstanceIsLocked(instance_uuid=0)) + def test_instance_lock_update_metadata(self, mock_update): + req = self._get_request() + req.method = 'POST' + req.content_type = 'application/json' + expected = { + 'metadata': { + 'keydummy': 'newkey', + } + } + req.body = jsonutils.dumps(expected) + self.assertRaises(webob.exc.HTTPConflict, self.controller.update_all, + req, self.uuid, body=expected) + + +class BadStateServerMetaDataTestV2(BadStateServerMetaDataTestV21): + def _set_up_resources(self): + self.controller = server_metadata_v2.Controller() + self.uuid = str(uuid.uuid4()) + self.url = '/v1.1/fake/servers/%s/metadata' % self.uuid + + def _get_request(self, param_url=''): + return fakes.HTTPRequest.blank(self.url + param_url) diff --git a/nova/tests/unit/api/openstack/compute/test_servers.py b/nova/tests/unit/api/openstack/compute/test_servers.py new file mode 100644 index 0000000000..c37df741ec --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_servers.py @@ -0,0 +1,4625 @@ +# Copyright 2010-2011 OpenStack Foundation +# Copyright 2011 Piston Cloud Computing, Inc. +# All Rights Reserved. +# Copyright 2013 Red Hat, 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 base64 +import contextlib +import datetime +import urllib +import uuid + +import iso8601 +from lxml import etree +import mock +from oslo.config import cfg +from oslo.serialization import jsonutils +from oslo.utils import timeutils +import six +import six.moves.urllib.parse as urlparse +import testtools +import webob + +from nova.api.openstack import compute +from nova.api.openstack.compute import ips +from nova.api.openstack.compute import servers +from nova.api.openstack.compute import views +from nova.api.openstack import extensions +from nova.api.openstack import xmlutil +from nova.compute import api as compute_api +from nova.compute import delete_types +from nova.compute import flavors +from nova.compute import task_states +from nova.compute import vm_states +from nova import context +from nova import db +from nova.db.sqlalchemy import models +from nova import exception +from nova.i18n import _ +from nova.image import glance +from nova.network import manager +from nova.network.neutronv2 import api as neutron_api +from nova import objects +from nova.objects import instance as instance_obj +from nova.openstack.common import policy as common_policy +from nova import policy +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import fake_instance +from nova.tests.unit import fake_network +from nova.tests.unit.image import fake +from nova.tests.unit import matchers +from nova.tests.unit.objects import test_keypair +from nova.tests.unit import utils +from nova import utils as nova_utils + +CONF = cfg.CONF +CONF.import_opt('password_length', 'nova.utils') + +FAKE_UUID = fakes.FAKE_UUID +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" +XPATH_NS = { + 'atom': 'http://www.w3.org/2005/Atom', + 'ns': 'http://docs.openstack.org/compute/api/v1.1' +} + +INSTANCE_IDS = {FAKE_UUID: 1} + +FIELDS = instance_obj.INSTANCE_DEFAULT_FIELDS + + +def fake_gen_uuid(): + return FAKE_UUID + + +def return_servers_empty(context, *args, **kwargs): + return [] + + +def return_security_group(context, instance_id, security_group_id): + pass + + +def instance_update_and_get_original(context, instance_uuid, values, + update_cells=True, + columns_to_join=None, + ): + inst = fakes.stub_instance(INSTANCE_IDS.get(instance_uuid), + name=values.get('display_name')) + inst = dict(inst, **values) + return (inst, inst) + + +def instance_update(context, instance_uuid, values, update_cells=True): + inst = fakes.stub_instance(INSTANCE_IDS.get(instance_uuid), + name=values.get('display_name')) + inst = dict(inst, **values) + return inst + + +def fake_compute_api(cls, req, id): + return True + + +class MockSetAdminPassword(object): + def __init__(self): + self.instance_id = None + self.password = None + + def __call__(self, context, instance_id, password): + self.instance_id = instance_id + self.password = password + + +class Base64ValidationTest(test.TestCase): + def setUp(self): + super(Base64ValidationTest, self).setUp() + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = servers.Controller(self.ext_mgr) + + def test_decode_base64(self): + value = "A random string" + result = self.controller._decode_base64(base64.b64encode(value)) + self.assertEqual(result, value) + + def test_decode_base64_binary(self): + value = "\x00\x12\x75\x99" + result = self.controller._decode_base64(base64.b64encode(value)) + self.assertEqual(result, value) + + def test_decode_base64_whitespace(self): + value = "A random string" + encoded = base64.b64encode(value) + white = "\n \n%s\t%s\n" % (encoded[:2], encoded[2:]) + result = self.controller._decode_base64(white) + self.assertEqual(result, value) + + def test_decode_base64_invalid(self): + invalid = "A random string" + result = self.controller._decode_base64(invalid) + self.assertIsNone(result) + + def test_decode_base64_illegal_bytes(self): + value = "A random string" + encoded = base64.b64encode(value) + white = ">\x01%s*%s()" % (encoded[:2], encoded[2:]) + result = self.controller._decode_base64(white) + self.assertIsNone(result) + + +class NeutronV2Subclass(neutron_api.API): + """Used to ensure that API handles subclasses properly.""" + pass + + +class ControllerTest(test.TestCase): + + def setUp(self): + super(ControllerTest, self).setUp() + self.flags(verbose=True, use_ipv6=False) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + fake.stub_out_image_service(self.stubs) + return_server = fakes.fake_instance_get() + return_servers = fakes.fake_instance_get_all_by_filters() + self.stubs.Set(db, 'instance_get_all_by_filters', + return_servers) + self.stubs.Set(db, 'instance_get_by_uuid', + return_server) + self.stubs.Set(db, 'instance_add_security_group', + return_security_group) + self.stubs.Set(db, 'instance_update_and_get_original', + instance_update_and_get_original) + + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = servers.Controller(self.ext_mgr) + self.ips_controller = ips.Controller() + policy.reset() + policy.init() + fake_network.stub_out_nw_api_get_instance_nw_info(self.stubs) + + +class ServersControllerTest(ControllerTest): + def test_can_check_loaded_extensions(self): + self.ext_mgr.extensions = {'os-fake': None} + self.assertTrue(self.controller.ext_mgr.is_loaded('os-fake')) + self.assertFalse(self.controller.ext_mgr.is_loaded('os-not-loaded')) + + def test_requested_networks_prefix(self): + uuid = 'br-00000000-0000-0000-0000-000000000000' + requested_networks = [{'uuid': uuid}] + res = self.controller._get_requested_networks(requested_networks) + self.assertIn((uuid, None), res.as_tuples()) + + def test_requested_networks_neutronv2_enabled_with_port(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'port': port}] + res = self.controller._get_requested_networks(requested_networks) + self.assertEqual([(None, None, port, None)], res.as_tuples()) + + def test_requested_networks_neutronv2_enabled_with_network(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + requested_networks = [{'uuid': network}] + res = self.controller._get_requested_networks(requested_networks) + self.assertEqual([(network, None, None, None)], res.as_tuples()) + + def test_requested_networks_neutronv2_enabled_with_network_and_port(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network, 'port': port}] + res = self.controller._get_requested_networks(requested_networks) + self.assertEqual([(None, None, port, None)], res.as_tuples()) + + def test_requested_networks_neutronv2_disabled_with_port(self): + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'port': port}] + self.assertRaises( + webob.exc.HTTPBadRequest, + self.controller._get_requested_networks, + requested_networks) + + def test_requested_networks_api_enabled_with_v2_subclass(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network, 'port': port}] + res = self.controller._get_requested_networks(requested_networks) + self.assertEqual([(None, None, port, None)], res.as_tuples()) + + def test_requested_networks_neutronv2_subclass_with_port(self): + cls = ('nova.tests.unit.api.openstack.compute' + + '.test_servers.NeutronV2Subclass') + self.flags(network_api_class=cls) + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'port': port}] + res = self.controller._get_requested_networks(requested_networks) + self.assertEqual([(None, None, port, None)], res.as_tuples()) + + def test_get_server_by_uuid(self): + req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID) + res_dict = self.controller.show(req, FAKE_UUID) + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + + def test_unique_host_id(self): + """Create two servers with the same host and different + project_ids and check that the hostId's are unique. + """ + def return_instance_with_host(self, *args, **kwargs): + project_id = str(uuid.uuid4()) + return fakes.stub_instance(id=1, uuid=FAKE_UUID, + project_id=project_id, + host='fake_host') + + self.stubs.Set(db, 'instance_get_by_uuid', + return_instance_with_host) + self.stubs.Set(db, 'instance_get', + return_instance_with_host) + + req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID) + server1 = self.controller.show(req, FAKE_UUID) + server2 = self.controller.show(req, FAKE_UUID) + + self.assertNotEqual(server1['server']['hostId'], + server2['server']['hostId']) + + def _get_server_data_dict(self, uuid, image_bookmark, flavor_bookmark, + status="ACTIVE", progress=100): + return { + "server": { + "id": uuid, + "user_id": "fake_user", + "tenant_id": "fake_project", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": progress, + "name": "server1", + "status": status, + "accessIPv4": "", + "accessIPv6": "", + "hostId": '', + "image": { + "id": "10", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": { + 'test1': [ + {'version': 4, 'addr': '192.168.1.100'}, + {'version': 6, 'addr': '2001:db8:0:1::1'} + ] + }, + "metadata": { + "seq": "1", + }, + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/fake/servers/%s" % uuid, + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/servers/%s" % uuid, + }, + ], + } + } + + def test_get_server_by_id(self): + self.flags(use_ipv6=True) + image_bookmark = "http://localhost/fake/images/10" + flavor_bookmark = "http://localhost/fake/flavors/1" + + uuid = FAKE_UUID + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % uuid) + res_dict = self.controller.show(req, uuid) + + expected_server = self._get_server_data_dict(uuid, + image_bookmark, + flavor_bookmark, + status="BUILD", + progress=0) + self.assertThat(res_dict, matchers.DictMatches(expected_server)) + + def test_get_server_with_active_status_by_id(self): + image_bookmark = "http://localhost/fake/images/10" + flavor_bookmark = "http://localhost/fake/flavors/1" + + new_return_server = fakes.fake_instance_get( + vm_state=vm_states.ACTIVE, progress=100) + self.stubs.Set(db, 'instance_get_by_uuid', new_return_server) + + uuid = FAKE_UUID + req = fakes.HTTPRequest.blank('/fake/servers/%s' % uuid) + res_dict = self.controller.show(req, uuid) + expected_server = self._get_server_data_dict(uuid, + image_bookmark, + flavor_bookmark) + self.assertThat(res_dict, matchers.DictMatches(expected_server)) + + def test_get_server_with_id_image_ref_by_id(self): + image_ref = "10" + image_bookmark = "http://localhost/fake/images/10" + flavor_id = "1" + flavor_bookmark = "http://localhost/fake/flavors/1" + + new_return_server = fakes.fake_instance_get( + vm_state=vm_states.ACTIVE, image_ref=image_ref, + flavor_id=flavor_id, progress=100) + self.stubs.Set(db, 'instance_get_by_uuid', new_return_server) + + uuid = FAKE_UUID + req = fakes.HTTPRequest.blank('/fake/servers/%s' % uuid) + res_dict = self.controller.show(req, uuid) + expected_server = self._get_server_data_dict(uuid, + image_bookmark, + flavor_bookmark) + self.assertThat(res_dict, matchers.DictMatches(expected_server)) + + def test_get_server_addresses_from_cache(self): + pub0 = ('172.19.0.1', '172.19.0.2',) + pub1 = ('1.2.3.4',) + pub2 = ('b33f::fdee:ddff:fecc:bbaa',) + priv0 = ('192.168.0.3', '192.168.0.4',) + + def _ip(ip): + return {'address': ip, 'type': 'fixed'} + + nw_cache = [ + {'address': 'aa:aa:aa:aa:aa:aa', + 'id': 1, + 'network': {'bridge': 'br0', + 'id': 1, + 'label': 'public', + 'subnets': [{'cidr': '172.19.0.0/24', + 'ips': [_ip(ip) for ip in pub0]}, + {'cidr': '1.2.3.0/16', + 'ips': [_ip(ip) for ip in pub1]}, + {'cidr': 'b33f::/64', + 'ips': [_ip(ip) for ip in pub2]}]}}, + {'address': 'bb:bb:bb:bb:bb:bb', + 'id': 2, + 'network': {'bridge': 'br1', + 'id': 2, + 'label': 'private', + 'subnets': [{'cidr': '192.168.0.0/24', + 'ips': [_ip(ip) for ip in priv0]}]}}] + + return_server = fakes.fake_instance_get(nw_cache=nw_cache) + self.stubs.Set(db, 'instance_get_by_uuid', return_server) + + req = fakes.HTTPRequest.blank('/fake/servers/%s/ips' % FAKE_UUID) + res_dict = self.ips_controller.index(req, FAKE_UUID) + + expected = { + 'addresses': { + 'private': [ + {'version': 4, 'addr': '192.168.0.3'}, + {'version': 4, 'addr': '192.168.0.4'}, + ], + 'public': [ + {'version': 4, 'addr': '172.19.0.1'}, + {'version': 4, 'addr': '172.19.0.2'}, + {'version': 4, 'addr': '1.2.3.4'}, + {'version': 6, 'addr': 'b33f::fdee:ddff:fecc:bbaa'}, + ], + }, + } + self.assertThat(res_dict, matchers.DictMatches(expected)) + + def test_get_server_addresses_nonexistent_network(self): + url = '/fake/servers/%s/ips/network_0' % FAKE_UUID + req = fakes.HTTPRequest.blank(url) + self.assertRaises(webob.exc.HTTPNotFound, self.ips_controller.show, + req, FAKE_UUID, 'network_0') + + def test_get_server_addresses_nonexistent_server(self): + def fake_instance_get(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get) + + server_id = str(uuid.uuid4()) + req = fakes.HTTPRequest.blank('/fake/servers/%s/ips' % server_id) + self.assertRaises(webob.exc.HTTPNotFound, + self.ips_controller.index, req, server_id) + + def test_get_server_list_empty(self): + self.stubs.Set(db, 'instance_get_all_by_filters', + return_servers_empty) + + req = fakes.HTTPRequest.blank('/fake/servers') + res_dict = self.controller.index(req) + + num_servers = len(res_dict['servers']) + self.assertEqual(0, num_servers) + + def test_get_server_list_with_reservation_id(self): + req = fakes.HTTPRequest.blank('/fake/servers?reservation_id=foo') + res_dict = self.controller.index(req) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s.get('name'), 'server%d' % (i + 1)) + i += 1 + + def test_get_server_list_with_reservation_id_empty(self): + req = fakes.HTTPRequest.blank('/fake/servers/detail?' + 'reservation_id=foo') + res_dict = self.controller.detail(req) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s.get('name'), 'server%d' % (i + 1)) + i += 1 + + def test_get_server_list_with_reservation_id_details(self): + req = fakes.HTTPRequest.blank('/fake/servers/detail?' + 'reservation_id=foo') + res_dict = self.controller.detail(req) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s.get('name'), 'server%d' % (i + 1)) + i += 1 + + def test_get_server_list(self): + req = fakes.HTTPRequest.blank('/fake/servers') + res_dict = self.controller.index(req) + + self.assertEqual(len(res_dict['servers']), 5) + for i, s in enumerate(res_dict['servers']): + self.assertEqual(s['id'], fakes.get_fake_uuid(i)) + self.assertEqual(s['name'], 'server%d' % (i + 1)) + self.assertIsNone(s.get('image', None)) + + expected_links = [ + { + "rel": "self", + "href": "http://localhost/v2/fake/servers/%s" % s['id'], + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/servers/%s" % s['id'], + }, + ] + + self.assertEqual(s['links'], expected_links) + + def test_get_servers_with_limit(self): + req = fakes.HTTPRequest.blank('/fake/servers?limit=3') + res_dict = self.controller.index(req) + + servers = res_dict['servers'] + self.assertEqual([s['id'] for s in servers], + [fakes.get_fake_uuid(i) for i in xrange(len(servers))]) + + servers_links = res_dict['servers_links'] + self.assertEqual(servers_links[0]['rel'], 'next') + href_parts = urlparse.urlparse(servers_links[0]['href']) + self.assertEqual('/v2/fake/servers', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + expected_params = {'limit': ['3'], + 'marker': [fakes.get_fake_uuid(2)]} + self.assertThat(params, matchers.DictMatches(expected_params)) + + def test_get_servers_with_limit_bad_value(self): + req = fakes.HTTPRequest.blank('/fake/servers?limit=aaa') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_get_server_details_empty(self): + self.stubs.Set(db, 'instance_get_all_by_filters', + return_servers_empty) + + req = fakes.HTTPRequest.blank('/fake/servers/detail') + res_dict = self.controller.detail(req) + + num_servers = len(res_dict['servers']) + self.assertEqual(0, num_servers) + + def test_get_server_details_with_limit(self): + req = fakes.HTTPRequest.blank('/fake/servers/detail?limit=3') + res = self.controller.detail(req) + + servers = res['servers'] + self.assertEqual([s['id'] for s in servers], + [fakes.get_fake_uuid(i) for i in xrange(len(servers))]) + + servers_links = res['servers_links'] + self.assertEqual(servers_links[0]['rel'], 'next') + + href_parts = urlparse.urlparse(servers_links[0]['href']) + self.assertEqual('/v2/fake/servers/detail', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + expected = {'limit': ['3'], 'marker': [fakes.get_fake_uuid(2)]} + self.assertThat(params, matchers.DictMatches(expected)) + + def test_get_server_details_with_limit_bad_value(self): + req = fakes.HTTPRequest.blank('/fake/servers/detail?limit=aaa') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.detail, req) + + def test_get_server_details_with_limit_and_other_params(self): + req = fakes.HTTPRequest.blank('/fake/servers/detail' + '?limit=3&blah=2:t') + res = self.controller.detail(req) + + servers = res['servers'] + self.assertEqual([s['id'] for s in servers], + [fakes.get_fake_uuid(i) for i in xrange(len(servers))]) + + servers_links = res['servers_links'] + self.assertEqual(servers_links[0]['rel'], 'next') + + href_parts = urlparse.urlparse(servers_links[0]['href']) + self.assertEqual('/v2/fake/servers/detail', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + expected = {'limit': ['3'], 'blah': ['2:t'], + 'marker': [fakes.get_fake_uuid(2)]} + self.assertThat(params, matchers.DictMatches(expected)) + + def test_get_servers_with_too_big_limit(self): + req = fakes.HTTPRequest.blank('/fake/servers?limit=30') + res_dict = self.controller.index(req) + self.assertNotIn('servers_links', res_dict) + + def test_get_servers_with_bad_limit(self): + req = fakes.HTTPRequest.blank('/fake/servers?limit=asdf') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_get_servers_with_marker(self): + url = '/v2/fake/servers?marker=%s' % fakes.get_fake_uuid(2) + req = fakes.HTTPRequest.blank(url) + servers = self.controller.index(req)['servers'] + self.assertEqual([s['name'] for s in servers], ["server4", "server5"]) + + def test_get_servers_with_limit_and_marker(self): + url = '/v2/fake/servers?limit=2&marker=%s' % fakes.get_fake_uuid(1) + req = fakes.HTTPRequest.blank(url) + servers = self.controller.index(req)['servers'] + self.assertEqual([s['name'] for s in servers], ['server3', 'server4']) + + def test_get_servers_with_bad_marker(self): + req = fakes.HTTPRequest.blank('/fake/servers?limit=2&marker=asdf') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_get_servers_with_bad_option(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?unknownoption=whee') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_allows_image(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + self.assertIsNotNone(search_opts) + self.assertIn('image', search_opts) + self.assertEqual(search_opts['image'], '12345') + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?image=12345') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_tenant_id_filter_converts_to_project_id_for_admin(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False): + self.assertIsNotNone(filters) + self.assertEqual(filters['project_id'], 'newfake') + self.assertFalse(filters.get('tenant_id')) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers' + '?all_tenants=1&tenant_id=newfake', + use_admin_context=True) + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_param_normal(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False): + self.assertNotIn('project_id', filters) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?all_tenants', + use_admin_context=True) + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_param_one(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False): + self.assertNotIn('project_id', filters) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?all_tenants=1', + use_admin_context=True) + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_param_zero(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False): + self.assertNotIn('all_tenants', filters) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?all_tenants=0', + use_admin_context=True) + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_param_false(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False): + self.assertNotIn('all_tenants', filters) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?all_tenants=false', + use_admin_context=True) + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_param_invalid(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None): + self.assertNotIn('all_tenants', filters) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?all_tenants=xxx', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req) + + def test_admin_restricted_tenant(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False): + self.assertIsNotNone(filters) + self.assertEqual(filters['project_id'], 'fake') + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers', + use_admin_context=True) + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_pass_policy(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None, use_slave=False): + self.assertIsNotNone(filters) + self.assertNotIn('project_id', filters) + return [fakes.stub_instance(100)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + rules = { + "compute:get_all_tenants": + common_policy.parse_rule("project_id:fake"), + "compute:get_all": + common_policy.parse_rule("project_id:fake"), + } + + policy.set_rules(rules) + + req = fakes.HTTPRequest.blank('/fake/servers?all_tenants=1') + res = self.controller.index(req) + + self.assertIn('servers', res) + + def test_all_tenants_fail_policy(self): + def fake_get_all(context, filters=None, sort_key=None, + sort_dir='desc', limit=None, marker=None, + columns_to_join=None): + self.assertIsNotNone(filters) + return [fakes.stub_instance(100)] + + rules = { + "compute:get_all_tenants": + common_policy.parse_rule("project_id:non_fake"), + "compute:get_all": + common_policy.parse_rule("project_id:fake"), + } + + policy.set_rules(rules) + self.stubs.Set(db, 'instance_get_all_by_filters', + fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?all_tenants=1') + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.index, req) + + def test_get_servers_allows_flavor(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + self.assertIsNotNone(search_opts) + self.assertIn('flavor', search_opts) + # flavor is an integer ID + self.assertEqual(search_opts['flavor'], '12345') + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?flavor=12345') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_with_bad_flavor(self): + req = fakes.HTTPRequest.blank('/fake/servers?flavor=abcde') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 0) + + def test_get_server_details_with_bad_flavor(self): + req = fakes.HTTPRequest.blank('/fake/servers/detail?flavor=abcde') + servers = self.controller.detail(req)['servers'] + + self.assertThat(servers, testtools.matchers.HasLength(0)) + + def test_get_servers_allows_status(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + self.assertIsNotNone(search_opts) + self.assertIn('vm_state', search_opts) + self.assertEqual(search_opts['vm_state'], [vm_states.ACTIVE]) + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?status=active') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + @mock.patch.object(compute_api.API, 'get_all') + def test_get_servers_allows_multi_status(self, get_all_mock): + server_uuid0 = str(uuid.uuid4()) + server_uuid1 = str(uuid.uuid4()) + db_list = [fakes.stub_instance(100, uuid=server_uuid0), + fakes.stub_instance(101, uuid=server_uuid1)] + get_all_mock.return_value = instance_obj._make_instance_list( + context, instance_obj.InstanceList(), db_list, FIELDS) + + req = fakes.HTTPRequest.blank( + '/fake/servers?status=active&status=error') + servers = self.controller.index(req)['servers'] + self.assertEqual(2, len(servers)) + self.assertEqual(server_uuid0, servers[0]['id']) + self.assertEqual(server_uuid1, servers[1]['id']) + expected_search_opts = dict(deleted=False, + vm_state=[vm_states.ACTIVE, + vm_states.ERROR], + project_id='fake') + get_all_mock.assert_called_once_with(mock.ANY, + search_opts=expected_search_opts, limit=mock.ANY, + marker=mock.ANY, want_objects=mock.ANY) + + @mock.patch.object(compute_api.API, 'get_all') + def test_get_servers_system_metadata_filter(self, get_all_mock): + server_uuid0 = str(uuid.uuid4()) + server_uuid1 = str(uuid.uuid4()) + expected_system_metadata = u'{"some_value": "some_key"}' + db_list = [fakes.stub_instance(100, uuid=server_uuid0), + fakes.stub_instance(101, uuid=server_uuid1)] + get_all_mock.return_value = instance_obj._make_instance_list( + context, instance_obj.InstanceList(), db_list, FIELDS) + + req = fakes.HTTPRequest.blank( + '/fake/servers?status=active&status=error&system_metadata=' + + urllib.quote(expected_system_metadata), + use_admin_context=True) + servers = self.controller.index(req)['servers'] + self.assertEqual(2, len(servers)) + self.assertEqual(server_uuid0, servers[0]['id']) + self.assertEqual(server_uuid1, servers[1]['id']) + expected_search_opts = dict( + deleted=False, vm_state=[vm_states.ACTIVE, vm_states.ERROR], + system_metadata=expected_system_metadata, project_id='fake') + get_all_mock.assert_called_once_with(mock.ANY, + search_opts=expected_search_opts, limit=mock.ANY, + marker=mock.ANY, want_objects=mock.ANY) + + @mock.patch.object(compute_api.API, 'get_all') + def test_get_servers_flavor_not_found(self, get_all_mock): + get_all_mock.side_effect = exception.FlavorNotFound(flavor_id=1) + + req = fakes.HTTPRequest.blank( + '/fake/servers?status=active&flavor=abc') + servers = self.controller.index(req)['servers'] + self.assertEqual(0, len(servers)) + + @mock.patch.object(compute_api.API, 'get_all') + def test_get_servers_allows_invalid_status(self, get_all_mock): + server_uuid0 = str(uuid.uuid4()) + server_uuid1 = str(uuid.uuid4()) + db_list = [fakes.stub_instance(100, uuid=server_uuid0), + fakes.stub_instance(101, uuid=server_uuid1)] + get_all_mock.return_value = instance_obj._make_instance_list( + context, instance_obj.InstanceList(), db_list, FIELDS) + + req = fakes.HTTPRequest.blank( + '/fake/servers?status=active&status=invalid') + servers = self.controller.index(req)['servers'] + self.assertEqual(2, len(servers)) + self.assertEqual(server_uuid0, servers[0]['id']) + self.assertEqual(server_uuid1, servers[1]['id']) + expected_search_opts = dict(deleted=False, + vm_state=[vm_states.ACTIVE], + project_id='fake') + get_all_mock.assert_called_once_with(mock.ANY, + search_opts=expected_search_opts, limit=mock.ANY, + marker=mock.ANY, want_objects=mock.ANY) + + def test_get_servers_allows_task_status(self): + server_uuid = str(uuid.uuid4()) + task_state = task_states.REBOOTING + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + self.assertIsNotNone(search_opts) + self.assertIn('task_state', search_opts) + self.assertEqual([task_states.REBOOT_PENDING, + task_states.REBOOT_STARTED, + task_states.REBOOTING], + search_opts['task_state']) + db_list = [fakes.stub_instance(100, uuid=server_uuid, + task_state=task_state)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/servers?status=reboot') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_resize_status(self): + # Test when resize status, it maps list of vm states. + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + self.assertIn('vm_state', search_opts) + self.assertEqual(search_opts['vm_state'], + [vm_states.ACTIVE, vm_states.STOPPED]) + + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?status=resize') + + servers = self.controller.detail(req)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_invalid_status(self): + # Test getting servers by invalid status. + req = fakes.HTTPRequest.blank('/fake/servers?status=baloney', + use_admin_context=False) + servers = self.controller.index(req)['servers'] + self.assertEqual(len(servers), 0) + + def test_get_servers_deleted_status_as_user(self): + req = fakes.HTTPRequest.blank('/fake/servers?status=deleted', + use_admin_context=False) + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.detail, req) + + def test_get_servers_deleted_status_as_admin(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + self.assertIn('vm_state', search_opts) + self.assertEqual(search_opts['vm_state'], ['deleted']) + + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?status=deleted', + use_admin_context=True) + + servers = self.controller.detail(req)['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_allows_name(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + self.assertIsNotNone(search_opts) + self.assertIn('name', search_opts) + self.assertEqual(search_opts['name'], 'whee.*') + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?name=whee.*') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_allows_changes_since(self): + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + self.assertIsNotNone(search_opts) + self.assertIn('changes-since', search_opts) + changes_since = datetime.datetime(2011, 1, 24, 17, 8, 1, + tzinfo=iso8601.iso8601.UTC) + self.assertEqual(search_opts['changes-since'], changes_since) + self.assertNotIn('deleted', search_opts) + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + params = 'changes-since=2011-01-24T17:08:01Z' + req = fakes.HTTPRequest.blank('/fake/servers?%s' % params) + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_allows_changes_since_bad_value(self): + params = 'changes-since=asdf' + req = fakes.HTTPRequest.blank('/fake/servers?%s' % params) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) + + def test_get_servers_admin_filters_as_user(self): + """Test getting servers by admin-only or unknown options when + context is not admin. Make sure the admin and unknown options + are stripped before they get to compute_api.get_all() + """ + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + self.assertIsNotNone(search_opts) + # Allowed by user + self.assertIn('name', search_opts) + self.assertIn('ip', search_opts) + # OSAPI converts status to vm_state + self.assertIn('vm_state', search_opts) + # Allowed only by admins with admin API on + self.assertNotIn('unknown_option', search_opts) + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" + req = fakes.HTTPRequest.blank('/fake/servers?%s' % query_str) + res = self.controller.index(req) + + servers = res['servers'] + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_admin_options_as_admin(self): + """Test getting servers by admin-only or unknown options when + context is admin. All options should be passed + """ + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + self.assertIsNotNone(search_opts) + # Allowed by user + self.assertIn('name', search_opts) + # OSAPI converts status to vm_state + self.assertIn('vm_state', search_opts) + # Allowed only by admins with admin API on + self.assertIn('ip', search_opts) + self.assertIn('unknown_option', search_opts) + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + query_str = "name=foo&ip=10.*&status=active&unknown_option=meow" + req = fakes.HTTPRequest.blank('/fake/servers?%s' % query_str, + use_admin_context=True) + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_allows_ip(self): + """Test getting servers by ip.""" + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + self.assertIsNotNone(search_opts) + self.assertIn('ip', search_opts) + self.assertEqual(search_opts['ip'], '10\..*') + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?ip=10\..*') + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_admin_allows_ip6(self): + """Test getting servers by ip6 with admin_api enabled and + admin context + """ + server_uuid = str(uuid.uuid4()) + + def fake_get_all(compute_self, context, search_opts=None, + sort_key=None, sort_dir='desc', + limit=None, marker=None, want_objects=False): + self.assertIsNotNone(search_opts) + self.assertIn('ip6', search_opts) + self.assertEqual(search_opts['ip6'], 'ffff.*') + db_list = [fakes.stub_instance(100, uuid=server_uuid)] + return instance_obj._make_instance_list( + context, objects.InstanceList(), db_list, FIELDS) + + self.stubs.Set(compute_api.API, 'get_all', fake_get_all) + + req = fakes.HTTPRequest.blank('/fake/servers?ip6=ffff.*', + use_admin_context=True) + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_all_server_details(self): + expected_flavor = { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": 'http://localhost/fake/flavors/1', + }, + ], + } + expected_image = { + "id": "10", + "links": [ + { + "rel": "bookmark", + "href": 'http://localhost/fake/images/10', + }, + ], + } + req = fakes.HTTPRequest.blank('/fake/servers/detail') + res_dict = self.controller.detail(req) + + for i, s in enumerate(res_dict['servers']): + self.assertEqual(s['id'], fakes.get_fake_uuid(i)) + self.assertEqual(s['hostId'], '') + self.assertEqual(s['name'], 'server%d' % (i + 1)) + self.assertEqual(s['image'], expected_image) + self.assertEqual(s['flavor'], expected_flavor) + self.assertEqual(s['status'], 'BUILD') + self.assertEqual(s['metadata']['seq'], str(i + 1)) + + def test_get_all_server_details_with_host(self): + """We want to make sure that if two instances are on the same host, + then they return the same hostId. If two instances are on different + hosts, they should return different hostId's. In this test, there + are 5 instances - 2 on one host and 3 on another. + """ + + def return_servers_with_host(context, *args, **kwargs): + return [fakes.stub_instance(i + 1, 'fake', 'fake', host=i % 2, + uuid=fakes.get_fake_uuid(i)) + for i in xrange(5)] + + self.stubs.Set(db, 'instance_get_all_by_filters', + return_servers_with_host) + + req = fakes.HTTPRequest.blank('/fake/servers/detail') + res_dict = self.controller.detail(req) + + server_list = res_dict['servers'] + host_ids = [server_list[0]['hostId'], server_list[1]['hostId']] + self.assertTrue(host_ids[0] and host_ids[1]) + self.assertNotEqual(host_ids[0], host_ids[1]) + + for i, s in enumerate(server_list): + self.assertEqual(s['id'], fakes.get_fake_uuid(i)) + self.assertEqual(s['hostId'], host_ids[i % 2]) + self.assertEqual(s['name'], 'server%d' % (i + 1)) + + +class ServersControllerUpdateTest(ControllerTest): + + def _get_request(self, body=None, content_type='json', options=None): + if options: + self.stubs.Set(db, 'instance_get', + fakes.fake_instance_get(**options)) + req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID) + req.method = 'PUT' + req.content_type = 'application/%s' % content_type + req.body = jsonutils.dumps(body) + return req + + def test_update_server_all_attributes(self): + body = {'server': { + 'name': 'server_test', + 'accessIPv4': '0.0.0.0', + 'accessIPv6': 'beef::0123', + }} + req = self._get_request(body, {'name': 'server_test', + 'access_ipv4': '0.0.0.0', + 'access_ipv6': 'beef::0123'}) + res_dict = self.controller.update(req, FAKE_UUID, body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['name'], 'server_test') + self.assertEqual(res_dict['server']['accessIPv4'], '0.0.0.0') + self.assertEqual(res_dict['server']['accessIPv6'], 'beef::123') + + def test_update_server_invalid_xml_raises_lookup(self): + body = """<?xml version="1.0" encoding="TF-8"?> + <metadata + xmlns="http://docs.openstack.org/compute/api/v1.1" + key="Label"></meta>""" + req = self._get_request(body, content_type='xml') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_update_server_invalid_xml_raises_expat(self): + body = """<?xml version="1.0" encoding="UTF-8"?> + <metadata + xmlns="http://docs.openstack.org/compute/api/v1.1" + key="Label"></meta>""" + req = self._get_request(body, content_type='xml') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_update_server_name(self): + body = {'server': {'name': 'server_test'}} + req = self._get_request(body, {'name': 'server_test'}) + res_dict = self.controller.update(req, FAKE_UUID, body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['name'], 'server_test') + + def test_update_server_name_too_long(self): + body = {'server': {'name': 'x' * 256}} + req = self._get_request(body, {'name': 'server_test'}) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, FAKE_UUID, body) + + def test_update_server_name_all_blank_spaces(self): + body = {'server': {'name': ' ' * 64}} + req = self._get_request(body, {'name': 'server_test'}) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, FAKE_UUID, body) + + def test_update_server_access_ipv4(self): + body = {'server': {'accessIPv4': '0.0.0.0'}} + req = self._get_request(body, {'access_ipv4': '0.0.0.0'}) + res_dict = self.controller.update(req, FAKE_UUID, body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['accessIPv4'], '0.0.0.0') + + def test_update_server_access_ipv4_bad_format(self): + body = {'server': {'accessIPv4': 'bad_format'}} + req = self._get_request(body, {'access_ipv4': '0.0.0.0'}) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, FAKE_UUID, body) + + def test_update_server_access_ipv4_none(self): + body = {'server': {'accessIPv4': None}} + req = self._get_request(body, {'access_ipv4': '0.0.0.0'}) + res_dict = self.controller.update(req, FAKE_UUID, body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['accessIPv4'], '') + + def test_update_server_access_ipv4_blank(self): + body = {'server': {'accessIPv4': ''}} + req = self._get_request(body, {'access_ipv4': '0.0.0.0'}) + res_dict = self.controller.update(req, FAKE_UUID, body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['accessIPv4'], '') + + def test_update_server_access_ipv6(self): + body = {'server': {'accessIPv6': 'beef::0123'}} + req = self._get_request(body, {'access_ipv6': 'beef::0123'}) + res_dict = self.controller.update(req, FAKE_UUID, body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['accessIPv6'], 'beef::123') + + def test_update_server_access_ipv6_bad_format(self): + body = {'server': {'accessIPv6': 'bad_format'}} + req = self._get_request(body, {'access_ipv6': 'beef::0123'}) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, FAKE_UUID, body) + + def test_update_server_access_ipv6_none(self): + body = {'server': {'accessIPv6': None}} + req = self._get_request(body, {'access_ipv6': 'beef::0123'}) + res_dict = self.controller.update(req, FAKE_UUID, body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['accessIPv6'], '') + + def test_update_server_access_ipv6_blank(self): + body = {'server': {'accessIPv6': ''}} + req = self._get_request(body, {'access_ipv6': 'beef::0123'}) + res_dict = self.controller.update(req, FAKE_UUID, body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['accessIPv6'], '') + + def test_update_server_personality(self): + body = { + 'server': { + 'personality': [] + } + } + req = self._get_request(body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, req, FAKE_UUID, body) + + def test_update_server_adminPass_ignored(self): + inst_dict = dict(name='server_test', adminPass='bacon') + body = dict(server=inst_dict) + + def server_update(context, id, params): + filtered_dict = { + 'display_name': 'server_test', + } + self.assertEqual(params, filtered_dict) + filtered_dict['uuid'] = id + return filtered_dict + + self.stubs.Set(db, 'instance_update', server_update) + # FIXME (comstud) + # self.stubs.Set(db, 'instance_get', + # return_server_with_attributes(name='server_test')) + + req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID) + req.method = 'PUT' + req.content_type = "application/json" + req.body = jsonutils.dumps(body) + res_dict = self.controller.update(req, FAKE_UUID, body) + + self.assertEqual(res_dict['server']['id'], FAKE_UUID) + self.assertEqual(res_dict['server']['name'], 'server_test') + + def test_update_server_not_found(self): + def fake_get(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(compute_api.API, 'get', fake_get) + body = {'server': {'name': 'server_test'}} + req = self._get_request(body) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.update, + req, FAKE_UUID, body) + + def test_update_server_not_found_on_update(self): + def fake_update(*args, **kwargs): + raise exception.InstanceNotFound(instance_id='fake') + + self.stubs.Set(db, 'instance_update_and_get_original', fake_update) + body = {'server': {'name': 'server_test'}} + req = self._get_request(body) + self.assertRaises(webob.exc.HTTPNotFound, self.controller.update, + req, FAKE_UUID, body) + + def test_update_server_policy_fail(self): + rule = {'compute:update': common_policy.parse_rule('role:admin')} + policy.set_rules(rule) + body = {'server': {'name': 'server_test'}} + req = self._get_request(body, {'name': 'server_test'}) + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.update, req, FAKE_UUID, body) + + +class ServersControllerDeleteTest(ControllerTest): + + def setUp(self): + super(ServersControllerDeleteTest, self).setUp() + self.server_delete_called = False + + def instance_destroy_mock(*args, **kwargs): + self.server_delete_called = True + deleted_at = timeutils.utcnow() + return fake_instance.fake_db_instance(deleted_at=deleted_at) + + self.stubs.Set(db, 'instance_destroy', instance_destroy_mock) + + def _create_delete_request(self, uuid): + fakes.stub_out_instance_quota(self.stubs, 0, 10) + req = fakes.HTTPRequest.blank('/v2/fake/servers/%s' % uuid) + req.method = 'DELETE' + return req + + def _delete_server_instance(self, uuid=FAKE_UUID): + req = self._create_delete_request(uuid) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE)) + self.controller.delete(req, uuid) + + def test_delete_server_instance(self): + self._delete_server_instance() + self.assertTrue(self.server_delete_called) + + def test_delete_server_instance_not_found(self): + self.assertRaises(webob.exc.HTTPNotFound, + self._delete_server_instance, + uuid='non-existent-uuid') + + def test_delete_locked_server(self): + req = self._create_delete_request(FAKE_UUID) + self.stubs.Set(compute_api.API, delete_types.SOFT_DELETE, + fakes.fake_actions_to_locked_server) + self.stubs.Set(compute_api.API, delete_types.DELETE, + fakes.fake_actions_to_locked_server) + + self.assertRaises(webob.exc.HTTPConflict, self.controller.delete, + req, FAKE_UUID) + + def test_delete_server_instance_while_building(self): + fakes.stub_out_instance_quota(self.stubs, 0, 10) + request = self._create_delete_request(FAKE_UUID) + self.controller.delete(request, FAKE_UUID) + + self.assertTrue(self.server_delete_called) + + def test_delete_server_instance_while_deleting_host_up(self): + req = self._create_delete_request(FAKE_UUID) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE, + task_state=task_states.DELETING, + host='fake_host')) + self.stubs.Set(objects.Instance, 'save', + lambda *args, **kwargs: None) + + @classmethod + def fake_get_by_compute_host(cls, context, host): + return {'updated_at': timeutils.utcnow()} + self.stubs.Set(objects.Service, 'get_by_compute_host', + fake_get_by_compute_host) + + self.controller.delete(req, FAKE_UUID) + # Delete request can be ignored, because it's been accepted and + # forwarded to the compute service already. + self.assertFalse(self.server_delete_called) + + def test_delete_server_instance_while_deleting_host_down(self): + fake_network.stub_out_network_cleanup(self.stubs) + req = self._create_delete_request(FAKE_UUID) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE, + task_state=task_states.DELETING, + host='fake_host')) + self.stubs.Set(objects.Instance, 'save', + lambda *args, **kwargs: None) + + @classmethod + def fake_get_by_compute_host(cls, context, host): + return {'updated_at': datetime.datetime.min} + self.stubs.Set(objects.Service, 'get_by_compute_host', + fake_get_by_compute_host) + + self.controller.delete(req, FAKE_UUID) + # Delete request would be ignored, because it's been accepted before + # but since the host is down, api should remove the instance anyway. + self.assertTrue(self.server_delete_called) + + def test_delete_server_instance_while_resize(self): + req = self._create_delete_request(FAKE_UUID) + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE, + task_state=task_states.RESIZE_PREP)) + + self.controller.delete(req, FAKE_UUID) + # Delete shoud be allowed in any case, even during resizing, + # because it may get stuck. + self.assertTrue(self.server_delete_called) + + def test_delete_server_instance_if_not_launched(self): + self.flags(reclaim_instance_interval=3600) + req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID) + req.method = 'DELETE' + + self.server_delete_called = False + + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(launched_at=None)) + + def instance_destroy_mock(*args, **kwargs): + self.server_delete_called = True + deleted_at = timeutils.utcnow() + return fake_instance.fake_db_instance(deleted_at=deleted_at) + self.stubs.Set(db, 'instance_destroy', instance_destroy_mock) + + self.controller.delete(req, FAKE_UUID) + # delete() should be called for instance which has never been active, + # even if reclaim_instance_interval has been set. + self.assertEqual(self.server_delete_called, True) + + +class ServersControllerRebuildInstanceTest(ControllerTest): + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + image_href = 'http://localhost/v2/fake/images/%s' % image_uuid + + def setUp(self): + super(ServersControllerRebuildInstanceTest, self).setUp() + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_states.ACTIVE)) + self.body = { + 'rebuild': { + 'name': 'new_name', + 'imageRef': self.image_href, + 'metadata': { + 'open': 'stack', + }, + 'personality': [ + { + "path": "/etc/banner.txt", + "contents": "MQ==", + }, + ], + }, + } + self.req = fakes.HTTPRequest.blank('/fake/servers/a/action') + self.req.method = 'POST' + self.req.headers["content-type"] = "application/json" + + def test_rebuild_instance_with_access_ipv4_bad_format(self): + # proper local hrefs must start with 'http://localhost/v2/' + self.body['rebuild']['accessIPv4'] = 'bad_format' + self.body['rebuild']['accessIPv6'] = 'fead::1234' + self.body['rebuild']['metadata']['hello'] = 'world' + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + self.req, FAKE_UUID, self.body) + + def test_rebuild_instance_with_blank_metadata_key(self): + self.body['rebuild']['accessIPv4'] = '0.0.0.0' + self.body['rebuild']['accessIPv6'] = 'fead::1234' + self.body['rebuild']['metadata'][''] = 'world' + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + self.req, FAKE_UUID, self.body) + + def test_rebuild_instance_with_metadata_key_too_long(self): + self.body['rebuild']['accessIPv4'] = '0.0.0.0' + self.body['rebuild']['accessIPv6'] = 'fead::1234' + self.body['rebuild']['metadata'][('a' * 260)] = 'world' + + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller._action_rebuild, + self.req, FAKE_UUID, self.body) + + def test_rebuild_instance_with_metadata_value_too_long(self): + self.body['rebuild']['accessIPv4'] = '0.0.0.0' + self.body['rebuild']['accessIPv6'] = 'fead::1234' + self.body['rebuild']['metadata']['key1'] = ('a' * 260) + + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller._action_rebuild, + self.req, FAKE_UUID, self.body) + + def test_rebuild_instance_fails_when_min_ram_too_small(self): + # make min_ram larger than our instance ram size + def fake_get_image(self, context, image_href, **kwargs): + return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + name='public image', is_public=True, + status='active', properties={'key1': 'value1'}, + min_ram="4096", min_disk="10") + + self.stubs.Set(fake._FakeImageService, 'show', fake_get_image) + + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + self.req, FAKE_UUID, self.body) + + def test_rebuild_instance_fails_when_min_disk_too_small(self): + # make min_disk larger than our instance disk size + def fake_get_image(self, context, image_href, **kwargs): + return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + name='public image', is_public=True, + status='active', properties={'key1': 'value1'}, + min_ram="128", min_disk="100000") + + self.stubs.Set(fake._FakeImageService, 'show', fake_get_image) + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, self.req, + FAKE_UUID, self.body) + + def test_rebuild_instance_image_too_large(self): + # make image size larger than our instance disk size + size = str(1000 * (1024 ** 3)) + + def fake_get_image(self, context, image_href, **kwargs): + return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + name='public image', is_public=True, + status='active', size=size) + + self.stubs.Set(fake._FakeImageService, 'show', fake_get_image) + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, self.req, FAKE_UUID, self.body) + + def test_rebuild_instance_with_deleted_image(self): + def fake_get_image(self, context, image_href, **kwargs): + return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + name='public image', is_public=True, + status='DELETED') + + self.stubs.Set(fake._FakeImageService, 'show', fake_get_image) + + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, self.req, FAKE_UUID, self.body) + + def test_rebuild_instance_onset_file_limit_over_quota(self): + def fake_get_image(self, context, image_href, **kwargs): + return dict(id='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', + name='public image', is_public=True, status='active') + + with contextlib.nested( + mock.patch.object(fake._FakeImageService, 'show', + side_effect=fake_get_image), + mock.patch.object(self.controller.compute_api, 'rebuild', + side_effect=exception.OnsetFileLimitExceeded) + ) as ( + show_mock, rebuild_mock + ): + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPForbidden, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + + def test_rebuild_instance_with_access_ipv6_bad_format(self): + # proper local hrefs must start with 'http://localhost/v2/' + self.body['rebuild']['accessIPv4'] = '1.2.3.4' + self.body['rebuild']['accessIPv6'] = 'bad_format' + self.body['rebuild']['metadata']['hello'] = 'world' + self.req.body = jsonutils.dumps(self.body) + self.req.headers["content-type"] = "application/json" + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, + self.req, FAKE_UUID, self.body) + + def test_rebuild_instance_with_null_image_ref(self): + self.body['rebuild']['imageRef'] = None + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._action_rebuild, self.req, FAKE_UUID, + self.body) + + +class ServerStatusTest(test.TestCase): + + def setUp(self): + super(ServerStatusTest, self).setUp() + fakes.stub_out_nw_api(self.stubs) + + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = servers.Controller(self.ext_mgr) + + def _fake_get_server(context, req, id): + return fakes.stub_instance(id) + + self.stubs.Set(self.controller, '_get_server', _fake_get_server) + + def _get_with_state(self, vm_state, task_state=None): + self.stubs.Set(db, 'instance_get_by_uuid', + fakes.fake_instance_get(vm_state=vm_state, + task_state=task_state)) + + request = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID) + return self.controller.show(request, FAKE_UUID) + + def _req_with_policy_fail(self, policy_rule_name): + rule = {'compute:%s' % policy_rule_name: + common_policy.parse_rule('role:admin')} + policy.set_rules(rule) + return fakes.HTTPRequest.blank('/fake/servers/1234/action') + + def test_active(self): + response = self._get_with_state(vm_states.ACTIVE) + self.assertEqual(response['server']['status'], 'ACTIVE') + + def test_reboot(self): + response = self._get_with_state(vm_states.ACTIVE, + task_states.REBOOTING) + self.assertEqual(response['server']['status'], 'REBOOT') + + def test_reboot_hard(self): + response = self._get_with_state(vm_states.ACTIVE, + task_states.REBOOTING_HARD) + self.assertEqual(response['server']['status'], 'HARD_REBOOT') + + def test_reboot_resize_policy_fail(self): + req = self._req_with_policy_fail('reboot') + self.assertRaises(exception.PolicyNotAuthorized, + self.controller._action_reboot, req, '1234', + {'reboot': {'type': 'HARD'}}) + + def test_rebuild(self): + response = self._get_with_state(vm_states.ACTIVE, + task_states.REBUILDING) + self.assertEqual(response['server']['status'], 'REBUILD') + + def test_rebuild_error(self): + response = self._get_with_state(vm_states.ERROR) + self.assertEqual(response['server']['status'], 'ERROR') + + def test_resize(self): + response = self._get_with_state(vm_states.ACTIVE, + task_states.RESIZE_PREP) + self.assertEqual(response['server']['status'], 'RESIZE') + + def test_confirm_resize_policy_fail(self): + req = self._req_with_policy_fail('confirm_resize') + self.assertRaises(exception.PolicyNotAuthorized, + self.controller._action_confirm_resize, req, '1234', {}) + + def test_verify_resize(self): + response = self._get_with_state(vm_states.RESIZED, None) + self.assertEqual(response['server']['status'], 'VERIFY_RESIZE') + + def test_revert_resize(self): + response = self._get_with_state(vm_states.RESIZED, + task_states.RESIZE_REVERTING) + self.assertEqual(response['server']['status'], 'REVERT_RESIZE') + + def test_revert_resize_policy_fail(self): + req = self._req_with_policy_fail('revert_resize') + self.assertRaises(exception.PolicyNotAuthorized, + self.controller._action_revert_resize, req, '1234', {}) + + def test_password_update(self): + response = self._get_with_state(vm_states.ACTIVE, + task_states.UPDATING_PASSWORD) + self.assertEqual(response['server']['status'], 'PASSWORD') + + def test_stopped(self): + response = self._get_with_state(vm_states.STOPPED) + self.assertEqual(response['server']['status'], 'SHUTOFF') + + +class ServersControllerCreateTest(test.TestCase): + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + + def setUp(self): + """Shared implementation for tests below that create instance.""" + super(ServersControllerCreateTest, self).setUp() + + self.flags(verbose=True, + enable_instance_password=True) + self.instance_cache_num = 0 + self.instance_cache_by_id = {} + self.instance_cache_by_uuid = {} + + fakes.stub_out_nw_api(self.stubs) + + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = servers.Controller(self.ext_mgr) + + self.volume_id = 'fake' + + def instance_create(context, inst): + inst_type = flavors.get_flavor_by_flavor_id(3) + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + def_image_ref = 'http://localhost/images/%s' % image_uuid + self.instance_cache_num += 1 + instance = fake_instance.fake_db_instance(**{ + 'id': self.instance_cache_num, + 'display_name': inst['display_name'] or 'test', + 'uuid': FAKE_UUID, + 'instance_type': inst_type, + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': 'fead::1234', + 'image_ref': inst.get('image_ref', def_image_ref), + 'user_id': 'fake', + 'project_id': 'fake', + 'reservation_id': inst['reservation_id'], + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "config_drive": None, + "progress": 0, + "fixed_ips": [], + "task_state": "", + "vm_state": "", + "root_device_name": inst.get('root_device_name', 'vda'), + "security_groups": inst['security_groups'], + }) + + self.instance_cache_by_id[instance['id']] = instance + self.instance_cache_by_uuid[instance['uuid']] = instance + return instance + + def instance_get(context, instance_id): + """Stub for compute/api create() pulling in instance after + scheduling + """ + return self.instance_cache_by_id[instance_id] + + def instance_update(context, uuid, values): + instance = self.instance_cache_by_uuid[uuid] + instance.update(values) + return instance + + def server_update(context, instance_uuid, params, update_cells=False): + inst = self.instance_cache_by_uuid[instance_uuid] + inst.update(params) + return inst + + def server_update_and_get_original( + context, instance_uuid, params, update_cells=False, + columns_to_join=None): + inst = self.instance_cache_by_uuid[instance_uuid] + inst.update(params) + return (inst, inst) + + def fake_method(*args, **kwargs): + pass + + def project_get_networks(context, user_id): + return dict(id='1', host='localhost') + + def queue_get_for(context, *args): + return 'network_topic' + + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + fake.stub_out_image_service(self.stubs) + self.stubs.Set(uuid, 'uuid4', fake_gen_uuid) + self.stubs.Set(db, 'instance_add_security_group', + return_security_group) + self.stubs.Set(db, 'project_get_networks', + project_get_networks) + self.stubs.Set(db, 'instance_create', instance_create) + self.stubs.Set(db, 'instance_system_metadata_update', + fake_method) + self.stubs.Set(db, 'instance_get', instance_get) + self.stubs.Set(db, 'instance_update', instance_update) + self.stubs.Set(db, 'instance_update_and_get_original', + server_update_and_get_original) + self.stubs.Set(manager.VlanManager, 'allocate_fixed_ip', + fake_method) + self.body = { + 'server': { + 'min_count': 2, + 'name': 'server_test', + 'imageRef': self.image_uuid, + 'flavorRef': self.flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': [ + { + "path": "/etc/banner.txt", + "contents": "MQ==", + }, + ], + }, + } + self.bdm = [{'delete_on_termination': 1, + 'device_name': 123, + 'volume_size': 1, + 'volume_id': '11111111-1111-1111-1111-111111111111'}] + + self.req = fakes.HTTPRequest.blank('/fake/servers') + self.req.method = 'POST' + self.req.headers["content-type"] = "application/json" + + def _check_admin_pass_len(self, server_dict): + """utility function - check server_dict for adminPass length.""" + self.assertEqual(CONF.password_length, + len(server_dict["adminPass"])) + + def _check_admin_pass_missing(self, server_dict): + """utility function - check server_dict for absence of adminPass.""" + self.assertNotIn("adminPass", server_dict) + + def _test_create_instance(self, flavor=2): + image_uuid = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + self.body['server']['imageRef'] = image_uuid + self.body['server']['flavorRef'] = flavor + self.req.body = jsonutils.dumps(self.body) + server = self.controller.create(self.req, self.body).obj['server'] + self._check_admin_pass_len(server) + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_private_flavor(self): + values = { + 'name': 'fake_name', + 'memory_mb': 512, + 'vcpus': 1, + 'root_gb': 10, + 'ephemeral_gb': 10, + 'flavorid': '1324', + 'swap': 0, + 'rxtx_factor': 0.5, + 'vcpu_weight': 1, + 'disabled': False, + 'is_public': False, + } + db.flavor_create(context.get_admin_context(), values) + self.assertRaises(webob.exc.HTTPBadRequest, self._test_create_instance, + flavor=1324) + + def test_create_server_bad_image_href(self): + image_href = 1 + self.body['server']['imageRef'] = image_href, + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + self.req, self.body) + + def test_create_server_with_invalid_networks_parameter(self): + self.ext_mgr.extensions = {'os-networks': 'fake'} + self.body['server']['networks'] = { + 'uuid': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'} + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + self.req, + self.body) + + def test_create_server_with_deleted_image(self): + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + # Get the fake image service so we can set the status to deleted + (image_service, image_id) = glance.get_remote_image_service( + context, '') + image_service.update(context, image_uuid, {'status': 'DELETED'}) + self.addCleanup(image_service.update, context, image_uuid, + {'status': 'active'}) + + self.body['server']['flavorRef'] = 2 + self.req.body = jsonutils.dumps(self.body) + with testtools.ExpectedException( + webob.exc.HTTPBadRequest, + 'Image 76fa36fc-c930-4bf3-8c8a-ea2a2420deb6 is not active.'): + self.controller.create(self.req, self.body) + + def test_create_server_image_too_large(self): + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + # Get the fake image service so we can set the status to deleted + (image_service, image_id) = glance.get_remote_image_service(context, + image_uuid) + image = image_service.show(context, image_id) + orig_size = image['size'] + new_size = str(1000 * (1024 ** 3)) + image_service.update(context, image_uuid, {'size': new_size}) + + self.addCleanup(image_service.update, context, image_uuid, + {'size': orig_size}) + + self.body['server']['flavorRef'] = 2 + self.req.body = jsonutils.dumps(self.body) + with testtools.ExpectedException( + webob.exc.HTTPBadRequest, + "Flavor's disk is too small for requested image."): + self.controller.create(self.req, self.body) + + def test_create_instance_invalid_negative_min(self): + self.ext_mgr.extensions = {'os-multiple-create': 'fake'} + self.body['server']['min_count'] = -1 + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + self.req, + self.body) + + def test_create_instance_invalid_negative_max(self): + self.ext_mgr.extensions = {'os-multiple-create': 'fake'} + self.body['server']['max_count'] = -1 + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + self.req, + self.body) + + def test_create_instance_invalid_alpha_min(self): + self.ext_mgr.extensions = {'os-multiple-create': 'fake'} + self.body['server']['min_count'] = 'abcd', + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + self.req, + self.body) + + def test_create_instance_invalid_alpha_max(self): + self.ext_mgr.extensions = {'os-multiple-create': 'fake'} + self.body['server']['max_count'] = 'abcd', + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + self.req, + self.body) + + def test_create_multiple_instances(self): + """Test creating multiple instances but not asking for + reservation_id + """ + self.ext_mgr.extensions = {'os-multiple-create': 'fake'} + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + self.assertEqual(FAKE_UUID, res["server"]["id"]) + self._check_admin_pass_len(res["server"]) + + def test_create_multiple_instances_pass_disabled(self): + """Test creating multiple instances but not asking for + reservation_id + """ + self.ext_mgr.extensions = {'os-multiple-create': 'fake'} + self.flags(enable_instance_password=False) + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + self.assertEqual(FAKE_UUID, res["server"]["id"]) + self._check_admin_pass_missing(res["server"]) + + def test_create_multiple_instances_resv_id_return(self): + """Test creating multiple instances with asking for + reservation_id + """ + self.ext_mgr.extensions = {'os-multiple-create': 'fake'} + self.body['server']['return_reservation_id'] = True + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body) + reservation_id = res.obj.get('reservation_id') + self.assertNotEqual(reservation_id, "") + self.assertIsNotNone(reservation_id) + self.assertTrue(len(reservation_id) > 1) + + def test_create_multiple_instances_with_multiple_volume_bdm(self): + """Test that a BadRequest is raised if multiple instances + are requested with a list of block device mappings for volumes. + """ + self.ext_mgr.extensions = {'os-multiple-create': 'fake'} + min_count = 2 + bdm = [{'device_name': 'foo1', 'volume_id': 'vol-xxxx'}, + {'device_name': 'foo2', 'volume_id': 'vol-yyyy'} + ] + params = { + 'block_device_mapping': bdm, + 'min_count': min_count + } + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['min_count'], 2) + self.assertEqual(len(kwargs['block_device_mapping']), 2) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params, no_image=True) + + def test_create_multiple_instances_with_single_volume_bdm(self): + """Test that a BadRequest is raised if multiple instances + are requested to boot from a single volume. + """ + self.ext_mgr.extensions = {'os-multiple-create': 'fake'} + min_count = 2 + bdm = [{'device_name': 'foo1', 'volume_id': 'vol-xxxx'}] + params = { + 'block_device_mapping': bdm, + 'min_count': min_count + } + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['min_count'], 2) + self.assertEqual(kwargs['block_device_mapping']['volume_id'], + 'vol-xxxx') + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params, no_image=True) + + def test_create_multiple_instance_with_non_integer_max_count(self): + self.ext_mgr.extensions = {'os-multiple-create': 'fake'} + self.body['server']['max_count'] = 2.5 + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_multiple_instance_with_non_integer_min_count(self): + self.ext_mgr.extensions = {'os-multiple-create': 'fake'} + self.body['server']['min_count'] = 2.5 + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_instance_image_ref_is_bookmark(self): + image_href = 'http://localhost/fake/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + server = res['server'] + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_image_ref_is_invalid(self): + image_uuid = 'this_is_not_a_valid_uuid' + image_href = 'http://localhost/fake/images/%s' % image_uuid + self.body['server']['imageRef'] = image_href + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + self.req, self.body) + + def test_create_instance_no_key_pair(self): + fakes.stub_out_key_pair_funcs(self.stubs, have_key_pair=False) + self._test_create_instance() + + def _test_create_extra(self, params, no_image=False): + self.body['server']['flavorRef'] = 2 + if no_image: + self.body['server'].pop('imageRef', None) + self.body['server'].update(params) + self.req.body = jsonutils.dumps(self.body) + self.assertIn('server', + self.controller.create(self.req, self.body).obj) + + def test_create_instance_with_security_group_enabled(self): + self.ext_mgr.extensions = {'os-security-groups': 'fake'} + group = 'foo' + old_create = compute_api.API.create + + def sec_group_get(ctx, proj, name): + if name == group: + return True + else: + raise exception.SecurityGroupNotFoundForProject( + project_id=proj, security_group_id=name) + + def create(*args, **kwargs): + self.assertEqual(kwargs['security_group'], [group]) + return old_create(*args, **kwargs) + + self.stubs.Set(db, 'security_group_get_by_name', sec_group_get) + # negative test + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, + {'security_groups': [{'name': 'bogus'}]}) + # positive test - extra assert in create path + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra({'security_groups': [{'name': group}]}) + + def test_create_instance_with_non_unique_secgroup_name(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network}] + params = {'networks': requested_networks, + 'security_groups': [{'name': 'dup'}, {'name': 'dup'}]} + + def fake_create(*args, **kwargs): + raise exception.NoUniqueMatch("No Unique match found for ...") + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPConflict, + self._test_create_extra, params) + + def test_create_instance_with_port_with_no_fixed_ips(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + port_id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'port': port_id}] + params = {'networks': requested_networks} + + def fake_create(*args, **kwargs): + raise exception.PortRequiresFixedIP(port_id=port_id) + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + @mock.patch.object(compute_api.API, 'create') + def test_create_instance_raise_user_data_too_large(self, mock_create): + mock_create.side_effect = exception.InstanceUserDataTooLarge( + maxsize=1, length=2) + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + self.req, self.body) + + @mock.patch.object(compute_api.API, 'create') + def test_create_instance_raise_auto_disk_config_exc(self, mock_create): + mock_create.side_effect = exception.AutoDiskConfigDisabledByImage( + image='dummy') + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + self.req, self.body) + + @mock.patch.object(compute_api.API, 'create', + side_effect=exception.InstanceExists( + name='instance-name')) + def test_create_instance_raise_instance_exists(self, mock_create): + self.assertRaises(webob.exc.HTTPConflict, + self.controller.create, + self.req, self.body) + + def test_create_instance_with_network_with_no_subnet(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network}] + params = {'networks': requested_networks} + + def fake_create(*args, **kwargs): + raise exception.NetworkRequiresSubnet(network_uuid=network) + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + def test_create_instance_with_access_ip(self): + self.body['server']['accessIPv4'] = '1.2.3.4' + self.body['server']['accessIPv6'] = 'fead::1234' + + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + server = res['server'] + self._check_admin_pass_len(server) + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_with_access_ip_pass_disabled(self): + # test with admin passwords disabled See lp bug 921814 + self.flags(enable_instance_password=False) + self.body['server']['accessIPv4'] = '1.2.3.4' + self.body['server']['accessIPv6'] = 'fead::1234' + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + + server = res['server'] + self._check_admin_pass_missing(server) + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_bad_format_access_ip_v4(self): + self.body['server']['accessIPv4'] = 'bad_format' + self.body['server']['accessIPv6'] = 'fead::1234' + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + self.req, self.body) + + def test_create_instance_bad_format_access_ip_v6(self): + self.body['server']['accessIPv4'] = '1.2.3.4' + self.body['server']['accessIPv6'] = 'bad_format' + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + self.req, self.body) + + def test_create_instance_name_all_blank_spaces(self): + self.body['server']['name'] = ' ' * 64 + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_instance_name_too_long(self): + self.body['server']['name'] = 'X' * 256 + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + self.req, self.body) + + def test_create_instance(self): + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + + server = res['server'] + self._check_admin_pass_len(server) + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_pass_disabled(self): + self.flags(enable_instance_password=False) + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + + server = res['server'] + self._check_admin_pass_missing(server) + self.assertEqual(FAKE_UUID, server['id']) + + @mock.patch('nova.virt.hardware.VirtNUMAInstanceTopology.get_constraints') + def test_create_instance_numa_topology_wrong(self, numa_constraints_mock): + numa_constraints_mock.side_effect = ( + exception.ImageNUMATopologyIncomplete) + image_href = 'http://localhost/v2/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_instance_too_much_metadata(self): + self.flags(quota_metadata_items=1) + self.body['server']['metadata']['vote'] = 'fiddletown' + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPForbidden, + self.controller.create, self.req, self.body) + + def test_create_instance_metadata_key_too_long(self): + self.flags(quota_metadata_items=1) + self.body['server']['metadata'] = {('a' * 260): '12345'} + + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller.create, self.req, self.body) + + def test_create_instance_metadata_value_too_long(self): + self.flags(quota_metadata_items=1) + self.body['server']['metadata'] = {'key1': ('a' * 260)} + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, + self.controller.create, self.req, self.body) + + def test_create_instance_metadata_key_blank(self): + self.flags(quota_metadata_items=1) + self.body['server']['metadata'] = {'': 'abcd'} + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_instance_metadata_not_dict(self): + self.flags(quota_metadata_items=1) + self.body['server']['metadata'] = 'string' + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_instance_metadata_key_not_string(self): + self.flags(quota_metadata_items=1) + self.body['server']['metadata'] = {1: 'test'} + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_instance_metadata_value_not_string(self): + self.flags(quota_metadata_items=1) + self.body['server']['metadata'] = {'test': ['a', 'list']} + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_user_data_malformed_bad_request(self): + self.ext_mgr.extensions = {'os-user-data': 'fake'} + params = {'user_data': 'u1234!'} + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + @mock.patch('nova.compute.api.API.create', + side_effect=exception.KeypairNotFound(name='nonexistentkey', + user_id=1)) + def test_create_instance_invalid_key_name(self, mock_create): + self.body['server']['key_name'] = 'nonexistentkey' + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_instance_valid_key_name(self): + self.body['server']['key_name'] = 'key' + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + + self.assertEqual(FAKE_UUID, res["server"]["id"]) + self._check_admin_pass_len(res["server"]) + + def test_create_instance_invalid_flavor_href(self): + flavor_ref = 'http://localhost/v2/flavors/asdf' + self.body['server']['flavorRef'] = flavor_ref + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_instance_invalid_flavor_id_int(self): + flavor_ref = -1 + self.body['server']['flavorRef'] = flavor_ref + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_instance_bad_flavor_href(self): + flavor_ref = 'http://localhost/v2/flavors/17' + self.body['server']['flavorRef'] = flavor_ref + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_instance_with_config_drive(self): + self.ext_mgr.extensions = {'os-config-drive': 'fake'} + self.body['server']['config_drive'] = "true" + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + server = res['server'] + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_with_bad_config_drive(self): + self.ext_mgr.extensions = {'os-config-drive': 'fake'} + self.body['server']['config_drive'] = 'adcd' + self.req.body = jsonutils.dumps(self.body) + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_instance_without_config_drive(self): + self.ext_mgr.extensions = {'os-config-drive': 'fake'} + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + server = res['server'] + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_with_config_drive_disabled(self): + config_drive = [{'config_drive': 'foo'}] + params = {'config_drive': config_drive} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertIsNone(kwargs['config_drive']) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_bad_href(self): + image_href = 'asdf' + self.body['server']['imageRef'] = image_href + self.req.body = jsonutils.dumps(self.body) + + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_instance_local_href(self): + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + + server = res['server'] + self.assertEqual(FAKE_UUID, server['id']) + + def test_create_instance_admin_pass(self): + self.body['server']['flavorRef'] = 3, + self.body['server']['adminPass'] = 'testpass' + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + + server = res['server'] + self.assertEqual(server['adminPass'], self.body['server']['adminPass']) + + def test_create_instance_admin_pass_pass_disabled(self): + self.flags(enable_instance_password=False) + self.body['server']['flavorRef'] = 3, + self.body['server']['adminPass'] = 'testpass' + self.req.body = jsonutils.dumps(self.body) + res = self.controller.create(self.req, self.body).obj + + server = res['server'] + self.assertIn('adminPass', self.body['server']) + self.assertNotIn('adminPass', server) + + def test_create_instance_admin_pass_empty(self): + self.body['server']['flavorRef'] = 3, + self.body['server']['adminPass'] = '' + self.req.body = jsonutils.dumps(self.body) + + # The fact that the action doesn't raise is enough validation + self.controller.create(self.req, self.body) + + def test_create_instance_with_security_group_disabled(self): + group = 'foo' + params = {'security_groups': [{'name': group}]} + old_create = compute_api.API.create + + def create(*args, **kwargs): + # NOTE(vish): if the security groups extension is not + # enabled, then security groups passed in + # are ignored. + self.assertEqual(kwargs['security_group'], ['default']) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_disk_config_enabled(self): + self.ext_mgr.extensions = {'OS-DCF': 'fake'} + # NOTE(vish): the extension converts OS-DCF:disk_config into + # auto_disk_config, so we are testing with + # the_internal_value + params = {'auto_disk_config': 'AUTO'} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['auto_disk_config'], 'AUTO') + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_disk_config_disabled(self): + params = {'auto_disk_config': True} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['auto_disk_config'], False) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_scheduler_hints_enabled(self): + self.ext_mgr.extensions = {'OS-SCH-HNT': 'fake'} + hints = {'a': 'b'} + params = {'scheduler_hints': hints} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['scheduler_hints'], hints) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_scheduler_hints_disabled(self): + hints = {'a': 'b'} + params = {'scheduler_hints': hints} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['scheduler_hints'], {}) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_volumes_enabled_no_image(self): + """Test that the create will fail if there is no image + and no bdms supplied in the request + """ + self.ext_mgr.extensions = {'os-volumes': 'fake'} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertNotIn('imageRef', kwargs) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, {}, no_image=True) + + def test_create_instance_with_bdm_v2_enabled_no_image(self): + self.ext_mgr.extensions = {'os-block-device-mapping-v2-boot': 'fake'} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertNotIn('imageRef', kwargs) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, {}, no_image=True) + + def test_create_instance_with_user_data_enabled(self): + self.ext_mgr.extensions = {'os-user-data': 'fake'} + user_data = 'fake' + params = {'user_data': user_data} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['user_data'], user_data) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_user_data_disabled(self): + user_data = 'fake' + params = {'user_data': user_data} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertIsNone(kwargs['user_data']) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_keypairs_enabled(self): + self.ext_mgr.extensions = {'os-keypairs': 'fake'} + key_name = 'green' + + params = {'key_name': key_name} + old_create = compute_api.API.create + + # NOTE(sdague): key pair goes back to the database, + # so we need to stub it out for tests + def key_pair_get(context, user_id, name): + return dict(test_keypair.fake_keypair, + public_key='FAKE_KEY', + fingerprint='FAKE_FINGERPRINT', + name=name) + + def create(*args, **kwargs): + self.assertEqual(kwargs['key_name'], key_name) + return old_create(*args, **kwargs) + + self.stubs.Set(db, 'key_pair_get', key_pair_get) + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_keypairs_disabled(self): + key_name = 'green' + + params = {'key_name': key_name} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertIsNone(kwargs['key_name']) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_availability_zone_enabled(self): + self.ext_mgr.extensions = {'os-availability-zone': 'fake'} + availability_zone = 'fake' + params = {'availability_zone': availability_zone} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['availability_zone'], availability_zone) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + + try: + self._test_create_extra(params) + except webob.exc.HTTPBadRequest as e: + expected = 'The requested availability zone is not available' + self.assertEqual(e.explanation, expected) + admin_context = context.get_admin_context() + db.service_create(admin_context, {'host': 'host1_zones', + 'binary': "nova-compute", + 'topic': 'compute', + 'report_count': 0}) + agg = db.aggregate_create(admin_context, + {'name': 'agg1'}, {'availability_zone': availability_zone}) + db.aggregate_host_add(admin_context, agg['id'], 'host1_zones') + self._test_create_extra(params) + + def test_create_instance_with_availability_zone_disabled(self): + availability_zone = 'fake' + params = {'availability_zone': availability_zone} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertIsNone(kwargs['availability_zone']) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_multiple_create_enabled(self): + self.ext_mgr.extensions = {'os-multiple-create': 'fake'} + min_count = 2 + max_count = 3 + params = { + 'min_count': min_count, + 'max_count': max_count, + } + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['min_count'], 2) + self.assertEqual(kwargs['max_count'], 3) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_multiple_create_disabled(self): + min_count = 2 + max_count = 3 + params = { + 'min_count': min_count, + 'max_count': max_count, + } + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertEqual(kwargs['min_count'], 1) + self.assertEqual(kwargs['max_count'], 1) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_networks_enabled(self): + self.ext_mgr.extensions = {'os-networks': 'fake'} + net_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + requested_networks = [{'uuid': net_uuid}] + params = {'networks': requested_networks} + old_create = compute_api.API.create + + def create(*args, **kwargs): + result = [('76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', None)] + self.assertEqual(result, kwargs['requested_networks'].as_tuples()) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_neutronv2_port_in_use(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network, 'port': port}] + params = {'networks': requested_networks} + + def fake_create(*args, **kwargs): + raise exception.PortInUse(port_id=port) + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPConflict, + self._test_create_extra, params) + + def test_create_instance_with_neturonv2_not_found_network(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + requested_networks = [{'uuid': network}] + params = {'networks': requested_networks} + + def fake_create(*args, **kwargs): + raise exception.NetworkNotFound(network_id=network) + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + def test_create_instance_with_neutronv2_port_not_found(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + requested_networks = [{'uuid': network, 'port': port}] + params = {'networks': requested_networks} + + def fake_create(*args, **kwargs): + raise exception.PortNotFound(port_id=port) + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + @mock.patch.object(compute_api.API, 'create') + def test_create_multiple_instance_with_specified_ip_neutronv2(self, + _api_mock): + _api_mock.side_effect = exception.InvalidFixedIpAndMaxCountRequest( + reason="") + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + address = '10.0.0.1' + self.body['server']['max_count'] = 2 + requested_networks = [{'uuid': network, 'fixed_ip': address, + 'port': port}] + params = {'networks': requested_networks} + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + def test_create_multiple_instance_with_neutronv2_port(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' + self.body['server']['max_count'] = 2 + requested_networks = [{'uuid': network, 'port': port}] + params = {'networks': requested_networks} + + def fake_create(*args, **kwargs): + msg = _("Unable to launch multiple instances with" + " a single configured port ID. Please launch your" + " instance one by one with different ports.") + raise exception.MultiplePortsNotApplicable(reason=msg) + + self.stubs.Set(compute_api.API, 'create', fake_create) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + + def test_create_instance_with_networks_disabled_neutronv2(self): + self.flags(network_api_class='nova.network.neutronv2.api.API') + net_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + requested_networks = [{'uuid': net_uuid}] + params = {'networks': requested_networks} + old_create = compute_api.API.create + + def create(*args, **kwargs): + result = [('76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', None, + None, None)] + self.assertEqual(result, kwargs['requested_networks'].as_tuples()) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_with_networks_disabled(self): + self.ext_mgr.extensions = {} + net_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + requested_networks = [{'uuid': net_uuid}] + params = {'networks': requested_networks} + old_create = compute_api.API.create + + def create(*args, **kwargs): + self.assertIsNone(kwargs['requested_networks']) + return old_create(*args, **kwargs) + + self.stubs.Set(compute_api.API, 'create', create) + self._test_create_extra(params) + + def test_create_instance_invalid_personality(self): + + def fake_create(*args, **kwargs): + codec = 'utf8' + content = 'b25zLiINCg0KLVJpY2hhcmQgQ$$%QQmFjaA==' + start_position = 19 + end_position = 20 + msg = 'invalid start byte' + raise UnicodeDecodeError(codec, content, start_position, + end_position, msg) + self.stubs.Set(compute_api.API, 'create', fake_create) + self.body['server']['personality'] = [ + { + "path": "/etc/banner.txt", + "contents": "b25zLiINCg0KLVJpY2hhcmQgQ$$%QQmFjaA==", + }, + ] + self.req.body = jsonutils.dumps(self.body) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, self.body) + + def test_create_location(self): + selfhref = 'http://localhost/v2/fake/servers/%s' % FAKE_UUID + image_href = 'http://localhost/v2/images/%s' % self.image_uuid + self.body['server']['imageRef'] = image_href + self.req.body = jsonutils.dumps(self.body) + robj = self.controller.create(self.req, self.body) + self.assertEqual(robj['Location'], selfhref) + + def _do_test_create_instance_above_quota(self, resource, allowed, quota, + expected_msg): + fakes.stub_out_instance_quota(self.stubs, allowed, quota, resource) + self.body['server']['flavorRef'] = 3 + self.req.body = jsonutils.dumps(self.body) + try: + self.controller.create(self.req, self.body).obj['server'] + self.fail('expected quota to be exceeded') + except webob.exc.HTTPForbidden as e: + self.assertEqual(e.explanation, expected_msg) + + def test_create_instance_above_quota_instances(self): + msg = _('Quota exceeded for instances: Requested 1, but' + ' already used 10 of 10 instances') + self._do_test_create_instance_above_quota('instances', 0, 10, msg) + + def test_create_instance_above_quota_ram(self): + msg = _('Quota exceeded for ram: Requested 4096, but' + ' already used 8192 of 10240 ram') + self._do_test_create_instance_above_quota('ram', 2048, 10 * 1024, msg) + + def test_create_instance_above_quota_cores(self): + msg = _('Quota exceeded for cores: Requested 2, but' + ' already used 9 of 10 cores') + self._do_test_create_instance_above_quota('cores', 1, 10, msg) + + def test_create_instance_above_quota_group_members(self): + ctxt = context.get_admin_context() + fake_group = objects.InstanceGroup(ctxt) + fake_group.create() + + def fake_count(context, name, group, user_id): + self.assertEqual(name, "server_group_members") + self.assertEqual(group.uuid, fake_group.uuid) + self.assertEqual(user_id, + self.req.environ['nova.context'].user_id) + return 10 + + def fake_limit_check(context, **kwargs): + if 'server_group_members' in kwargs: + raise exception.OverQuota(overs={}) + + def fake_instance_destroy(context, uuid, constraint): + return fakes.stub_instance(1) + + self.stubs.Set(fakes.QUOTAS, 'count', fake_count) + self.stubs.Set(fakes.QUOTAS, 'limit_check', fake_limit_check) + self.stubs.Set(db, 'instance_destroy', fake_instance_destroy) + self.ext_mgr.extensions = {'OS-SCH-HNT': 'fake', + 'os-server-group-quotas': 'fake'} + self.body['server']['scheduler_hints'] = {'group': fake_group.uuid} + self.req.body = jsonutils.dumps(self.body) + + expected_msg = "Quota exceeded, too many servers in group" + + try: + self.controller.create(self.req, self.body).obj['server'] + self.fail('expected quota to be exceeded') + except webob.exc.HTTPForbidden as e: + self.assertEqual(e.explanation, expected_msg) + + def test_create_instance_above_quota_server_groups(self): + + def fake_reserve(contex, **deltas): + if 'server_groups' in deltas: + raise exception.OverQuota(overs={}) + + def fake_instance_destroy(context, uuid, constraint): + return fakes.stub_instance(1) + + self.stubs.Set(fakes.QUOTAS, 'reserve', fake_reserve) + self.stubs.Set(db, 'instance_destroy', fake_instance_destroy) + self.ext_mgr.extensions = {'OS-SCH-HNT': 'fake', + 'os-server-group-quotas': 'fake'} + self.body['server']['scheduler_hints'] = {'group': 'fake-group'} + self.req.body = jsonutils.dumps(self.body) + + expected_msg = "Quota exceeded, too many server groups." + + try: + self.controller.create(self.req, self.body).obj['server'] + self.fail('expected quota to be exceeded') + except webob.exc.HTTPForbidden as e: + self.assertEqual(e.explanation, expected_msg) + + +class ServersControllerCreateTestWithMock(test.TestCase): + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + flavor_ref = 'http://localhost/123/flavors/3' + + def setUp(self): + """Shared implementation for tests below that create instance.""" + super(ServersControllerCreateTestWithMock, self).setUp() + + self.flags(verbose=True, + enable_instance_password=True) + self.instance_cache_num = 0 + self.instance_cache_by_id = {} + self.instance_cache_by_uuid = {} + + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = servers.Controller(self.ext_mgr) + + self.volume_id = 'fake' + + self.body = { + 'server': { + 'min_count': 2, + 'name': 'server_test', + 'imageRef': self.image_uuid, + 'flavorRef': self.flavor_ref, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': [ + { + "path": "/etc/banner.txt", + "contents": "MQ==", + }, + ], + }, + } + + self.req = fakes.HTTPRequest.blank('/fake/servers') + self.req.method = 'POST' + self.req.headers["content-type"] = "application/json" + + def _test_create_extra(self, params, no_image=False): + self.body['server']['flavorRef'] = 2 + if no_image: + self.body['server'].pop('imageRef', None) + self.body['server'].update(params) + self.req.body = jsonutils.dumps(self.body) + self.controller.create(self.req, self.body).obj['server'] + + @mock.patch.object(compute_api.API, 'create') + def test_create_instance_with_neutronv2_fixed_ip_already_in_use(self, + create_mock): + self.flags(network_api_class='nova.network.neutronv2.api.API') + network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + address = '10.0.2.3' + requested_networks = [{'uuid': network, 'fixed_ip': address}] + params = {'networks': requested_networks} + create_mock.side_effect = exception.FixedIpAlreadyInUse( + address=address, + instance_uuid=network) + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, params) + self.assertEqual(1, len(create_mock.call_args_list)) + + @mock.patch.object(compute_api.API, 'create', + side_effect=exception.InvalidVolume(reason='error')) + def test_create_instance_with_invalid_volume_error(self, create_mock): + # Tests that InvalidVolume is translated to a 400 error. + self.assertRaises(webob.exc.HTTPBadRequest, + self._test_create_extra, {}) + + +class TestServerCreateRequestXMLDeserializer(test.TestCase): + + def setUp(self): + super(TestServerCreateRequestXMLDeserializer, self).setUp() + self.deserializer = servers.CreateDeserializer() + + def test_minimal_request(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" + imageRef="1" + flavorRef="2"/>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + }, + } + self.assertEqual(request['body'], expected) + + def test_request_with_alternate_namespace_prefix(self): + serial_request = """ +<ns2:server xmlns:ns2="http://docs.openstack.org/compute/api/v2" + name="new-server-test" + imageRef="1" + flavorRef="2"> + <ns2:metadata><ns2:meta key="hello">world</ns2:meta></ns2:metadata> + </ns2:server> + """ + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + 'metadata': {"hello": "world"}, + }, + } + self.assertEqual(request['body'], expected) + + def test_request_with_scheduler_hints_and_alternate_namespace_prefix(self): + serial_request = """ +<ns2:server xmlns:ns2="http://docs.openstack.org/compute/api/v2" + name="new-server-test" + imageRef="1" + flavorRef="2"> + <ns2:metadata><ns2:meta key="hello">world</ns2:meta></ns2:metadata> + <os:scheduler_hints + xmlns:os="http://docs.openstack.org/compute/ext/scheduler-hints/api/v2"> + <hypervisor>xen</hypervisor> + <near>eb999657-dd6b-464e-8713-95c532ac3b18</near> + </os:scheduler_hints> + </ns2:server> + """ + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + 'OS-SCH-HNT:scheduler_hints': { + 'hypervisor': ['xen'], + 'near': ['eb999657-dd6b-464e-8713-95c532ac3b18'] + }, + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "metadata": { + "hello": "world" + } + } + } + self.assertEqual(request['body'], expected) + + def test_access_ipv4(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" + imageRef="1" + flavorRef="2" + accessIPv4="1.2.3.4"/>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "accessIPv4": "1.2.3.4", + }, + } + self.assertEqual(request['body'], expected) + + def test_access_ipv6(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" + imageRef="1" + flavorRef="2" + accessIPv6="fead::1234"/>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "accessIPv6": "fead::1234", + }, + } + self.assertEqual(request['body'], expected) + + def test_access_ip(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" + imageRef="1" + flavorRef="2" + accessIPv4="1.2.3.4" + accessIPv6="fead::1234"/>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", + }, + } + self.assertEqual(request['body'], expected) + + def test_admin_pass(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" + imageRef="1" + flavorRef="2" + adminPass="1234"/>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "adminPass": "1234", + }, + } + self.assertEqual(request['body'], expected) + + def test_image_link(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" + imageRef="http://localhost:8774/v2/images/2" + flavorRef="3"/>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "http://localhost:8774/v2/images/2", + "flavorRef": "3", + }, + } + self.assertEqual(request['body'], expected) + + def test_flavor_link(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" + imageRef="1" + flavorRef="http://localhost:8774/v2/flavors/3"/>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "http://localhost:8774/v2/flavors/3", + }, + } + self.assertEqual(request['body'], expected) + + def test_empty_metadata_personality(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" + imageRef="1" + flavorRef="2"> + <metadata/> + <personality/> +</server>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "metadata": {}, + "personality": [], + }, + } + self.assertEqual(request['body'], expected) + + def test_multiple_metadata_items(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" + imageRef="1" + flavorRef="2"> + <metadata> + <meta key="one">two</meta> + <meta key="open">snack</meta> + </metadata> +</server>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "metadata": {"one": "two", "open": "snack"}, + }, + } + self.assertEqual(request['body'], expected) + + def test_multiple_personality_files(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" + imageRef="1" + flavorRef="2"> + <personality> + <file path="/etc/banner.txt">MQ==</file> + <file path="/etc/hosts">Mg==</file> + </personality> +</server>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "personality": [ + {"path": "/etc/banner.txt", "contents": "MQ=="}, + {"path": "/etc/hosts", "contents": "Mg=="}, + ], + }, + } + self.assertThat(request['body'], matchers.DictMatches(expected)) + + def test_spec_request(self): + image_bookmark_link = ("http://servers.api.openstack.org/1234/" + "images/52415800-8b69-11e0-9b19-734f6f006e54") + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + imageRef="%s" + flavorRef="52415800-8b69-11e0-9b19-734f1195ff37" + name="new-server-test"> + <metadata> + <meta key="My Server Name">Apache1</meta> + </metadata> + <personality> + <file path="/etc/banner.txt">Mg==</file> + </personality> +</server>""" % (image_bookmark_link) + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "new-server-test", + "imageRef": ("http://servers.api.openstack.org/1234/" + "images/52415800-8b69-11e0-9b19-734f6f006e54"), + "flavorRef": "52415800-8b69-11e0-9b19-734f1195ff37", + "metadata": {"My Server Name": "Apache1"}, + "personality": [ + { + "path": "/etc/banner.txt", + "contents": "Mg==", + }, + ], + }, + } + self.assertEqual(request['body'], expected) + + def test_request_with_empty_networks(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" imageRef="1" flavorRef="1"> + <networks/> +</server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [], + }} + self.assertEqual(request['body'], expected) + + def test_request_with_one_network(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" imageRef="1" flavorRef="1"> + <networks> + <network uuid="1" fixed_ip="10.0.1.12"/> + </networks> +</server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"}], + }} + self.assertEqual(request['body'], expected) + + def test_request_with_two_networks(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" imageRef="1" flavorRef="1"> + <networks> + <network uuid="1" fixed_ip="10.0.1.12"/> + <network uuid="2" fixed_ip="10.0.2.12"/> + </networks> +</server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"}, + {"uuid": "2", "fixed_ip": "10.0.2.12"}], + }} + self.assertEqual(request['body'], expected) + + def test_request_with_second_network_node_ignored(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" imageRef="1" flavorRef="1"> + <networks> + <network uuid="1" fixed_ip="10.0.1.12"/> + </networks> + <networks> + <network uuid="2" fixed_ip="10.0.2.12"/> + </networks> +</server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"}], + }} + self.assertEqual(request['body'], expected) + + def test_request_with_one_network_missing_id(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" imageRef="1" flavorRef="1"> + <networks> + <network fixed_ip="10.0.1.12"/> + </networks> +</server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"fixed_ip": "10.0.1.12"}], + }} + self.assertEqual(request['body'], expected) + + def test_request_with_one_network_missing_fixed_ip(self): + serial_request = """ +<server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" imageRef="1" flavorRef="1"> + <networks> + <network uuid="1"/> + </networks> +</server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1"}], + }} + self.assertEqual(request['body'], expected) + + def test_request_with_one_network_empty_id(self): + serial_request = """ + <server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" imageRef="1" flavorRef="1"> + <networks> + <network uuid="" fixed_ip="10.0.1.12"/> + </networks> + </server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "", "fixed_ip": "10.0.1.12"}], + }} + self.assertEqual(request['body'], expected) + + def test_request_with_one_network_empty_fixed_ip(self): + serial_request = """ + <server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" imageRef="1" flavorRef="1"> + <networks> + <network uuid="1" fixed_ip=""/> + </networks> + </server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1", "fixed_ip": ""}], + }} + self.assertEqual(request['body'], expected) + + def test_request_with_networks_duplicate_ids(self): + serial_request = """ + <server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" imageRef="1" flavorRef="1"> + <networks> + <network uuid="1" fixed_ip="10.0.1.12"/> + <network uuid="1" fixed_ip="10.0.2.12"/> + </networks> + </server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "networks": [{"uuid": "1", "fixed_ip": "10.0.1.12"}, + {"uuid": "1", "fixed_ip": "10.0.2.12"}], + }} + self.assertEqual(request['body'], expected) + + def test_request_with_availability_zone(self): + serial_request = """ + <server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" imageRef="1" flavorRef="1" + availability_zone="some_zone:some_host"> + </server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "availability_zone": "some_zone:some_host", + }} + self.assertEqual(request['body'], expected) + + def test_request_with_multiple_create_args(self): + serial_request = """ + <server xmlns="http://docs.openstack.org/compute/api/v2" + name="new-server-test" imageRef="1" flavorRef="1" + min_count="1" max_count="3" return_reservation_id="True"> + </server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "min_count": "1", + "max_count": "3", + "return_reservation_id": True, + }} + self.assertEqual(request['body'], expected) + + def test_request_with_disk_config(self): + serial_request = """ + <server xmlns="http://docs.openstack.org/compute/api/v2" + xmlns:OS-DCF="http://docs.openstack.org/compute/ext/disk_config/api/v1.1" + name="new-server-test" imageRef="1" flavorRef="1" + OS-DCF:diskConfig="AUTO"> + </server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "OS-DCF:diskConfig": "AUTO", + }} + self.assertEqual(request['body'], expected) + + def test_request_with_scheduler_hints(self): + serial_request = """ + <server xmlns="http://docs.openstack.org/compute/api/v2" + xmlns:OS-SCH-HNT= + "http://docs.openstack.org/compute/ext/scheduler-hints/api/v2" + name="new-server-test" imageRef="1" flavorRef="1"> + <OS-SCH-HNT:scheduler_hints> + <different_host> + 7329b667-50c7-46a6-b913-cb2a09dfeee0 + </different_host> + <different_host> + f31efb24-34d2-43e1-8b44-316052956a39 + </different_host> + </OS-SCH-HNT:scheduler_hints> + </server>""" + request = self.deserializer.deserialize(serial_request) + expected = {"server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "1", + "OS-SCH-HNT:scheduler_hints": { + "different_host": [ + "7329b667-50c7-46a6-b913-cb2a09dfeee0", + "f31efb24-34d2-43e1-8b44-316052956a39", + ] + } + }} + self.assertEqual(request['body'], expected) + + def test_request_with_config_drive(self): + serial_request = """ + <server xmlns="http://docs.openstack.org/compute/api/v2" + name="config_drive_test" + imageRef="1" + flavorRef="1" + config_drive="true"/>""" + request = self.deserializer.deserialize(serial_request) + expected = { + "server": { + "name": "config_drive_test", + "imageRef": "1", + "flavorRef": "1", + "config_drive": "true" + }, + } + self.assertEqual(request['body'], expected) + + def test_corrupt_xml(self): + """Should throw a 400 error on corrupt xml.""" + self.assertRaises( + exception.MalformedRequestBody, + self.deserializer.deserialize, + utils.killer_xml_body()) + + +class TestServerActionRequestXMLDeserializer(test.TestCase): + + def setUp(self): + super(TestServerActionRequestXMLDeserializer, self).setUp() + self.deserializer = servers.ActionDeserializer() + + def _generate_request(self, action, disk_cfg, ref): + return """ +<%(action)s xmlns="http://docs.openstack.org/compute/api/v1.1" + xmlns:OS-DCF="http://docs.openstack.org/compute/ext/disk_config/api/v1.1" + %(disk_config)s="MANUAL" %(ref)s="1"/>""" % ( + {'action': action, 'disk_config': disk_cfg, 'ref': ref}) + + def _generate_expected(self, action, ref): + return { + "%s" % action: { + "%s" % ref: "1", + "OS-DCF:diskConfig": "MANUAL", + }, + } + + def test_rebuild_request(self): + serial_request = self._generate_request("rebuild", "OS-DCF:diskConfig", + "imageRef") + request = self.deserializer.deserialize(serial_request) + expected = self._generate_expected("rebuild", "imageRef") + self.assertEqual(request['body'], expected) + + def test_rebuild_request_auto_disk_config_compat(self): + serial_request = self._generate_request("rebuild", "auto_disk_config", + "imageRef") + request = self.deserializer.deserialize(serial_request) + expected = self._generate_expected("rebuild", "imageRef") + self.assertEqual(request['body'], expected) + + def test_resize_request(self): + serial_request = self._generate_request("resize", "OS-DCF:diskConfig", + "flavorRef") + request = self.deserializer.deserialize(serial_request) + expected = self._generate_expected("resize", "flavorRef") + self.assertEqual(request['body'], expected) + + def test_resize_request_auto_disk_config_compat(self): + serial_request = self._generate_request("resize", "auto_disk_config", + "flavorRef") + request = self.deserializer.deserialize(serial_request) + expected = self._generate_expected("resize", "flavorRef") + self.assertEqual(request['body'], expected) + + +class TestAddressesXMLSerialization(test.TestCase): + + index_serializer = ips.AddressesTemplate() + show_serializer = ips.NetworkTemplate() + + def _serializer_test_data(self): + return { + 'network_2': [ + {'addr': '192.168.0.1', 'version': 4}, + {'addr': 'fe80::beef', 'version': 6}, + ], + } + + def test_xml_declaration(self): + output = self.show_serializer.serialize(self._serializer_test_data()) + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) + + def test_show(self): + output = self.show_serializer.serialize(self._serializer_test_data()) + root = etree.XML(output) + network = self._serializer_test_data()['network_2'] + self.assertEqual(str(root.get('id')), 'network_2') + ip_elems = root.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) + + def test_index(self): + fixture = { + 'addresses': { + 'network_1': [ + {'addr': '192.168.0.3', 'version': 4}, + {'addr': '192.168.0.5', 'version': 4}, + ], + 'network_2': [ + {'addr': '192.168.0.1', 'version': 4}, + {'addr': 'fe80::beef', 'version': 6}, + ], + }, + } + output = self.index_serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'addresses') + addresses_dict = fixture['addresses'] + network_elems = root.findall('{0}network'.format(NS)) + self.assertEqual(len(network_elems), 2) + for i, network_elem in enumerate(network_elems): + network = addresses_dict.items()[i] + self.assertEqual(str(network_elem.get('id')), str(network[0])) + ip_elems = network_elem.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[1][z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) + + +class ServersViewBuilderTest(test.TestCase): + + image_bookmark = "http://localhost/fake/images/5" + flavor_bookmark = "http://localhost/fake/flavors/1" + + def setUp(self): + super(ServersViewBuilderTest, self).setUp() + self.flags(use_ipv6=True) + db_inst = fakes.stub_instance( + id=1, + image_ref="5", + uuid="deadbeef-feed-edee-beef-d0ea7beefedd", + display_name="test_server", + include_fake_metadata=False) + + privates = ['172.19.0.1'] + publics = ['192.168.0.3'] + public6s = ['b33f::fdee:ddff:fecc:bbaa'] + + def nw_info(*args, **kwargs): + return [(None, {'label': 'public', + 'ips': [dict(ip=ip) for ip in publics], + 'ip6s': [dict(ip=ip) for ip in public6s]}), + (None, {'label': 'private', + 'ips': [dict(ip=ip) for ip in privates]})] + + def floaters(*args, **kwargs): + return [] + + fakes.stub_out_nw_api_get_instance_nw_info(self.stubs, nw_info) + fakes.stub_out_nw_api_get_floating_ips_by_fixed_address(self.stubs, + floaters) + + self.uuid = db_inst['uuid'] + self.view_builder = views.servers.ViewBuilder() + self.request = fakes.HTTPRequest.blank("/v2/fake") + self.request.context = context.RequestContext('fake', 'fake') + self.instance = fake_instance.fake_instance_obj( + self.request.context, + expected_attrs=instance_obj.INSTANCE_DEFAULT_FIELDS, + **db_inst) + self.self_link = "http://localhost/v2/fake/servers/%s" % self.uuid + self.bookmark_link = "http://localhost/fake/servers/%s" % self.uuid + self.expected_detailed_server = { + "server": { + "id": self.uuid, + "user_id": "fake_user", + "tenant_id": "fake_project", + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 0, + "name": "test_server", + "status": "BUILD", + "accessIPv4": "", + "accessIPv6": "", + "hostId": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": self.image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": self.flavor_bookmark, + }, + ], + }, + "addresses": { + 'test1': [ + {'version': 4, 'addr': '192.168.1.100'}, + {'version': 6, 'addr': '2001:db8:0:1::1'} + ] + }, + "metadata": {}, + "links": [ + { + "rel": "self", + "href": self.self_link, + }, + { + "rel": "bookmark", + "href": self.bookmark_link, + }, + ], + } + } + + self.expected_server = { + "server": { + "id": self.uuid, + "name": "test_server", + "links": [ + { + "rel": "self", + "href": self.self_link, + }, + { + "rel": "bookmark", + "href": self.bookmark_link, + }, + ], + } + } + + def test_get_flavor_valid_flavor(self): + expected = {"id": "1", + "links": [{"rel": "bookmark", + "href": self.flavor_bookmark}]} + result = self.view_builder._get_flavor(self.request, self.instance) + self.assertEqual(result, expected) + + def test_build_server(self): + output = self.view_builder.basic(self.request, self.instance) + self.assertThat(output, + matchers.DictMatches(self.expected_server)) + + def test_build_server_with_project_id(self): + + output = self.view_builder.basic(self.request, self.instance) + self.assertThat(output, + matchers.DictMatches(self.expected_server)) + + def test_build_server_detail(self): + + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output, + matchers.DictMatches(self.expected_detailed_server)) + + def test_build_server_no_image(self): + self.instance["image_ref"] = "" + output = self.view_builder.show(self.request, self.instance) + self.assertEqual(output['server']['image'], "") + + def test_build_server_detail_with_fault(self): + self.instance['vm_state'] = vm_states.ERROR + self.instance['fault'] = fake_instance.fake_fault_obj( + self.request.context, self.uuid) + + self.expected_detailed_server["server"]["status"] = "ERROR" + self.expected_detailed_server["server"]["fault"] = { + "code": 404, + "created": "2010-10-10T12:00:00Z", + "message": "HTTPNotFound", + "details": "Stock details for test", + } + del self.expected_detailed_server["server"]["progress"] + + self.request.context = context.RequestContext('fake', 'fake') + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output, + matchers.DictMatches(self.expected_detailed_server)) + + def test_build_server_detail_with_fault_that_has_been_deleted(self): + self.instance['deleted'] = 1 + self.instance['vm_state'] = vm_states.ERROR + fault = fake_instance.fake_fault_obj(self.request.context, + self.uuid, code=500, + message="No valid host was found") + self.instance['fault'] = fault + + # Regardless of the vm_state deleted servers sholud have DELETED status + self.expected_detailed_server["server"]["status"] = "DELETED" + self.expected_detailed_server["server"]["fault"] = { + "code": 500, + "created": "2010-10-10T12:00:00Z", + "message": "No valid host was found", + } + del self.expected_detailed_server["server"]["progress"] + + self.request.context = context.RequestContext('fake', 'fake') + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output, + matchers.DictMatches(self.expected_detailed_server)) + + def test_build_server_detail_with_fault_no_details_not_admin(self): + self.instance['vm_state'] = vm_states.ERROR + self.instance['fault'] = fake_instance.fake_fault_obj( + self.request.context, + self.uuid, + code=500, + message='Error') + + expected_fault = {"code": 500, + "created": "2010-10-10T12:00:00Z", + "message": "Error"} + + self.request.context = context.RequestContext('fake', 'fake') + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output['server']['fault'], + matchers.DictMatches(expected_fault)) + + def test_build_server_detail_with_fault_admin(self): + self.instance['vm_state'] = vm_states.ERROR + self.instance['fault'] = fake_instance.fake_fault_obj( + self.request.context, + self.uuid, + code=500, + message='Error') + + expected_fault = {"code": 500, + "created": "2010-10-10T12:00:00Z", + "message": "Error", + 'details': 'Stock details for test'} + + self.request.environ['nova.context'].is_admin = True + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output['server']['fault'], + matchers.DictMatches(expected_fault)) + + def test_build_server_detail_with_fault_no_details_admin(self): + self.instance['vm_state'] = vm_states.ERROR + self.instance['fault'] = fake_instance.fake_fault_obj( + self.request.context, + self.uuid, + code=500, + message='Error', + details='') + + expected_fault = {"code": 500, + "created": "2010-10-10T12:00:00Z", + "message": "Error"} + + self.request.environ['nova.context'].is_admin = True + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output['server']['fault'], + matchers.DictMatches(expected_fault)) + + def test_build_server_detail_with_fault_but_active(self): + self.instance['vm_state'] = vm_states.ACTIVE + self.instance['progress'] = 100 + self.instance['fault'] = fake_instance.fake_fault_obj( + self.request.context, self.uuid) + + output = self.view_builder.show(self.request, self.instance) + self.assertNotIn('fault', output['server']) + + def test_build_server_detail_active_status(self): + # set the power state of the instance to running + self.instance['vm_state'] = vm_states.ACTIVE + self.instance['progress'] = 100 + + self.expected_detailed_server["server"]["status"] = "ACTIVE" + self.expected_detailed_server["server"]["progress"] = 100 + + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output, + matchers.DictMatches(self.expected_detailed_server)) + + def test_build_server_detail_with_accessipv4(self): + + access_ip_v4 = '1.2.3.4' + self.instance['access_ip_v4'] = access_ip_v4 + + self.expected_detailed_server["server"]["accessIPv4"] = access_ip_v4 + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output, + matchers.DictMatches(self.expected_detailed_server)) + + def test_build_server_detail_with_accessipv6(self): + + access_ip_v6 = 'fead::1234' + self.instance['access_ip_v6'] = access_ip_v6 + + self.expected_detailed_server["server"]["accessIPv6"] = access_ip_v6 + + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output, + matchers.DictMatches(self.expected_detailed_server)) + + def test_build_server_detail_with_metadata(self): + + metadata = [] + metadata.append(models.InstanceMetadata(key="Open", value="Stack")) + metadata = nova_utils.metadata_to_dict(metadata) + self.instance['metadata'] = metadata + + self.expected_detailed_server["server"]["metadata"] = {"Open": "Stack"} + output = self.view_builder.show(self.request, self.instance) + self.assertThat(output, + matchers.DictMatches(self.expected_detailed_server)) + + +class ServerXMLSerializationTest(test.TestCase): + + TIMESTAMP = "2010-10-11T10:30:22Z" + SERVER_HREF = 'http://localhost/v2/servers/%s' % FAKE_UUID + SERVER_NEXT = 'http://localhost/v2/servers?limit=%s&marker=%s' + SERVER_BOOKMARK = 'http://localhost/servers/%s' % FAKE_UUID + IMAGE_BOOKMARK = 'http://localhost/images/5' + FLAVOR_BOOKMARK = 'http://localhost/flavors/1' + USERS_ATTRIBUTES = ['name', 'id', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6'] + ADMINS_ATTRIBUTES = USERS_ATTRIBUTES + ['adminPass'] + + def setUp(self): + super(ServerXMLSerializationTest, self).setUp() + self.body = { + "server": { + 'id': FAKE_UUID, + 'user_id': 'fake_user_id', + 'tenant_id': 'fake_tenant_id', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + "progress": 0, + "name": "test_server-" + u'\u89e3\u7801', + "status": "BUILD", + "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": self.IMAGE_BOOKMARK, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": self.FLAVOR_BOOKMARK, + }, + ], + }, + "addresses": { + "network_one": [ + { + "version": 4, + "addr": "67.23.10.138", + }, + { + "version": 6, + "addr": "::babe:67.23.10.138", + }, + ], + "network_two": [ + { + "version": 4, + "addr": "67.23.10.139", + }, + { + "version": 6, + "addr": "::babe:67.23.10.139", + }, + ], + }, + "metadata": { + "Open": "Stack", + "Number": "1", + }, + 'links': [ + { + 'href': self.SERVER_HREF, + 'rel': 'self', + }, + { + 'href': self.SERVER_BOOKMARK, + 'rel': 'bookmark', + }, + ], + } + } + + def _validate_xml(self, root, server_dict): + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 2) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = server_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + image_root = root.find('{0}image'.format(NS)) + self.assertEqual(image_root.get('id'), server_dict['image']['id']) + link_nodes = image_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['image']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + flavor_root = root.find('{0}flavor'.format(NS)) + self.assertEqual(flavor_root.get('id'), server_dict['flavor']['id']) + link_nodes = flavor_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['flavor']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + addresses_root = root.find('{0}addresses'.format(NS)) + addresses_dict = server_dict['addresses'] + network_elems = addresses_root.findall('{0}network'.format(NS)) + self.assertEqual(len(network_elems), 2) + for i, network_elem in enumerate(network_elems): + network = addresses_dict.items()[i] + self.assertEqual(str(network_elem.get('id')), str(network[0])) + ip_elems = network_elem.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[1][z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) + + def _validate_required_attributes(self, root, server_dict, attributes): + for key in attributes: + expected = server_dict[key] + if not isinstance(expected, six.text_type): + expected = str(expected) + self.assertEqual(expected, root.get(key)) + + def test_xml_declaration(self): + serializer = servers.ServerTemplate() + + output = serializer.serialize(self.body) + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) + + def test_show(self): + serializer = servers.ServerTemplate() + + output = serializer.serialize(self.body) + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + server_dict = self.body['server'] + + self._validate_required_attributes(root, server_dict, + self.USERS_ATTRIBUTES) + self._validate_xml(root, server_dict) + + def test_create(self): + serializer = servers.FullServerTemplate() + + self.body["server"]["adminPass"] = "test_password" + + output = serializer.serialize(self.body) + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + server_dict = self.body['server'] + + self._validate_required_attributes(root, server_dict, + self.ADMINS_ATTRIBUTES) + self._validate_xml(root, server_dict) + + def test_index(self): + serializer = servers.MinimalServersTemplate() + + uuid1 = fakes.get_fake_uuid(1) + uuid2 = fakes.get_fake_uuid(2) + expected_server_href = 'http://localhost/v2/servers/%s' % uuid1 + expected_server_bookmark = 'http://localhost/servers/%s' % uuid1 + expected_server_href_2 = 'http://localhost/v2/servers/%s' % uuid2 + expected_server_bookmark_2 = 'http://localhost/servers/%s' % uuid2 + fixture = {"servers": [ + { + "id": fakes.get_fake_uuid(1), + "name": "test_server", + 'links': [ + { + 'href': expected_server_href, + 'rel': 'self', + }, + { + 'href': expected_server_bookmark, + 'rel': 'bookmark', + }, + ], + }, + { + "id": fakes.get_fake_uuid(2), + "name": "test_server_2", + 'links': [ + { + 'href': expected_server_href_2, + 'rel': 'self', + }, + { + 'href': expected_server_bookmark_2, + 'rel': 'bookmark', + }, + ], + }, + ]} + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'servers') + server_elems = root.findall('{0}server'.format(NS)) + self.assertEqual(len(server_elems), 2) + for i, server_elem in enumerate(server_elems): + server_dict = fixture['servers'][i] + for key in ['name', 'id']: + self.assertEqual(server_elem.get(key), str(server_dict[key])) + + link_nodes = server_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + def test_index_with_servers_links(self): + serializer = servers.MinimalServersTemplate() + + uuid1 = fakes.get_fake_uuid(1) + uuid2 = fakes.get_fake_uuid(2) + expected_server_href = 'http://localhost/v2/servers/%s' % uuid1 + expected_server_next = self.SERVER_NEXT % (2, 2) + expected_server_bookmark = 'http://localhost/servers/%s' % uuid1 + expected_server_href_2 = 'http://localhost/v2/servers/%s' % uuid2 + expected_server_bookmark_2 = 'http://localhost/servers/%s' % uuid2 + fixture = {"servers": [ + { + "id": fakes.get_fake_uuid(1), + "name": "test_server", + 'links': [ + { + 'href': expected_server_href, + 'rel': 'self', + }, + { + 'href': expected_server_bookmark, + 'rel': 'bookmark', + }, + ], + }, + { + "id": fakes.get_fake_uuid(2), + "name": "test_server_2", + 'links': [ + { + 'href': expected_server_href_2, + 'rel': 'self', + }, + { + 'href': expected_server_bookmark_2, + 'rel': 'bookmark', + }, + ], + }, + ], + "servers_links": [ + { + 'rel': 'next', + 'href': expected_server_next, + }, + ]} + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'servers') + server_elems = root.findall('{0}server'.format(NS)) + self.assertEqual(len(server_elems), 2) + for i, server_elem in enumerate(server_elems): + server_dict = fixture['servers'][i] + for key in ['name', 'id']: + self.assertEqual(server_elem.get(key), str(server_dict[key])) + + link_nodes = server_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + # Check servers_links + servers_links = root.findall('{0}link'.format(ATOMNS)) + for i, link in enumerate(fixture['servers_links']): + for key, value in link.items(): + self.assertEqual(servers_links[i].get(key), value) + + def test_detail(self): + serializer = servers.ServersTemplate() + + uuid1 = fakes.get_fake_uuid(1) + expected_server_href = 'http://localhost/v2/servers/%s' % uuid1 + expected_server_bookmark = 'http://localhost/servers/%s' % uuid1 + expected_image_bookmark = self.IMAGE_BOOKMARK + expected_flavor_bookmark = self.FLAVOR_BOOKMARK + + uuid2 = fakes.get_fake_uuid(2) + expected_server_href_2 = 'http://localhost/v2/servers/%s' % uuid2 + expected_server_bookmark_2 = 'http://localhost/servers/%s' % uuid2 + fixture = {"servers": [ + { + "id": fakes.get_fake_uuid(1), + "user_id": "fake", + "tenant_id": "fake", + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + "progress": 0, + "name": "test_server", + "status": "BUILD", + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", + "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": expected_image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": expected_flavor_bookmark, + }, + ], + }, + "addresses": { + "network_one": [ + { + "version": 4, + "addr": "67.23.10.138", + }, + { + "version": 6, + "addr": "::babe:67.23.10.138", + }, + ], + "network_two": [ + { + "version": 4, + "addr": "67.23.10.139", + }, + { + "version": 6, + "addr": "::babe:67.23.10.139", + }, + ], + }, + "metadata": { + "Open": "Stack", + "Number": "1", + }, + "links": [ + { + "href": expected_server_href, + "rel": "self", + }, + { + "href": expected_server_bookmark, + "rel": "bookmark", + }, + ], + }, + { + "id": fakes.get_fake_uuid(2), + "user_id": 'fake', + "tenant_id": 'fake', + 'created': self.TIMESTAMP, + 'updated': self.TIMESTAMP, + "progress": 100, + "name": "test_server_2", + "status": "ACTIVE", + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", + "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": expected_image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": expected_flavor_bookmark, + }, + ], + }, + "addresses": { + "network_one": [ + { + "version": 4, + "addr": "67.23.10.138", + }, + { + "version": 6, + "addr": "::babe:67.23.10.138", + }, + ], + "network_two": [ + { + "version": 4, + "addr": "67.23.10.139", + }, + { + "version": 6, + "addr": "::babe:67.23.10.139", + }, + ], + }, + "metadata": { + "Open": "Stack", + "Number": "2", + }, + "links": [ + { + "href": expected_server_href_2, + "rel": "self", + }, + { + "href": expected_server_bookmark_2, + "rel": "bookmark", + }, + ], + }, + ]} + + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'servers') + server_elems = root.findall('{0}server'.format(NS)) + self.assertEqual(len(server_elems), 2) + for i, server_elem in enumerate(server_elems): + server_dict = fixture['servers'][i] + self._validate_required_attributes(server_elem, server_dict, + self.USERS_ATTRIBUTES) + self._validate_xml(server_elem, server_dict) + + def test_update(self): + serializer = servers.ServerTemplate() + + self.body["server"]["fault"] = { + "code": 500, + "created": self.TIMESTAMP, + "message": "Error Message", + "details": "Fault details", + } + output = serializer.serialize(self.body) + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + server_dict = self.body['server'] + + self._validate_required_attributes(root, server_dict, + self.USERS_ATTRIBUTES) + + self._validate_xml(root, server_dict) + fault_root = root.find('{0}fault'.format(NS)) + fault_dict = server_dict['fault'] + self.assertEqual(fault_root.get("code"), str(fault_dict["code"])) + self.assertEqual(fault_root.get("created"), fault_dict["created"]) + msg_elem = fault_root.find('{0}message'.format(NS)) + self.assertEqual(msg_elem.text, fault_dict["message"]) + det_elem = fault_root.find('{0}details'.format(NS)) + self.assertEqual(det_elem.text, fault_dict["details"]) + + def test_action(self): + serializer = servers.FullServerTemplate() + + self.body["server"]["adminPass"] = u'\u89e3\u7801' + output = serializer.serialize(self.body) + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') + + server_dict = self.body['server'] + + self._validate_required_attributes(root, server_dict, + self.ADMINS_ATTRIBUTES) + + self._validate_xml(root, server_dict) + + +class ServersAllExtensionsTestCase(test.TestCase): + """Servers tests using default API router with all extensions enabled. + + The intent here is to catch cases where extensions end up throwing + an exception because of a malformed request before the core API + gets a chance to validate the request and return a 422 response. + + For example, ServerDiskConfigController extends servers.Controller:: + + | @wsgi.extends + | def create(self, req, body): + | if 'server' in body: + | self._set_disk_config(body['server']) + | resp_obj = (yield) + | self._show(req, resp_obj) + + we want to ensure that the extension isn't barfing on an invalid + body. + """ + + def setUp(self): + super(ServersAllExtensionsTestCase, self).setUp() + self.app = compute.APIRouter() + + def test_create_missing_server(self): + # Test create with malformed body. + + def fake_create(*args, **kwargs): + raise test.TestingException("Should not reach the compute API.") + + self.stubs.Set(compute_api.API, 'create', fake_create) + + req = fakes.HTTPRequest.blank('/fake/servers') + req.method = 'POST' + req.content_type = 'application/json' + body = {'foo': {'a': 'b'}} + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(422, res.status_int) + + def test_update_missing_server(self): + # Test update with malformed body. + + def fake_update(*args, **kwargs): + raise test.TestingException("Should not reach the compute API.") + + self.stubs.Set(compute_api.API, 'update', fake_update) + + req = fakes.HTTPRequest.blank('/fake/servers/1') + req.method = 'PUT' + req.content_type = 'application/json' + body = {'foo': {'a': 'b'}} + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(422, res.status_int) + + +class ServersUnprocessableEntityTestCase(test.TestCase): + """Tests of places we throw 422 Unprocessable Entity from.""" + + def setUp(self): + super(ServersUnprocessableEntityTestCase, self).setUp() + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = servers.Controller(self.ext_mgr) + + def _unprocessable_server_create(self, body): + req = fakes.HTTPRequest.blank('/fake/servers') + req.method = 'POST' + + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.controller.create, req, body) + + def test_create_server_no_body(self): + self._unprocessable_server_create(body=None) + + def test_create_server_missing_server(self): + body = {'foo': {'a': 'b'}} + self._unprocessable_server_create(body=body) + + def test_create_server_malformed_entity(self): + body = {'server': 'string'} + self._unprocessable_server_create(body=body) + + def _unprocessable_server_update(self, body): + req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID) + req.method = 'PUT' + + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.controller.update, req, FAKE_UUID, body) + + def test_update_server_no_body(self): + self._unprocessable_server_update(body=None) + + def test_update_server_missing_server(self): + body = {'foo': {'a': 'b'}} + self._unprocessable_server_update(body=body) + + def test_create_update_malformed_entity(self): + body = {'server': 'string'} + self._unprocessable_server_update(body=body) diff --git a/nova/tests/unit/api/openstack/compute/test_urlmap.py b/nova/tests/unit/api/openstack/compute/test_urlmap.py new file mode 100644 index 0000000000..c95cb95d2c --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_urlmap.py @@ -0,0 +1,171 @@ +# Copyright 2011 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. + +from oslo.serialization import jsonutils +import webob + +from nova import test +from nova.tests.unit.api.openstack import fakes +import nova.tests.unit.image.fake + + +class UrlmapTest(test.NoDBTestCase): + def setUp(self): + super(UrlmapTest, self).setUp() + fakes.stub_out_rate_limiting(self.stubs) + nova.tests.unit.image.fake.stub_out_image_service(self.stubs) + + def tearDown(self): + super(UrlmapTest, self).tearDown() + nova.tests.unit.image.fake.FakeImageService_reset() + + def test_path_version_v1_1(self): + # Test URL path specifying v1.1 returns v2 content. + req = webob.Request.blank('/v1.1/') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app(init_only=('versions',))) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['version']['id'], 'v2.0') + + def test_content_type_version_v1_1(self): + # Test Content-Type specifying v1.1 returns v2 content. + req = webob.Request.blank('/') + req.content_type = "application/json;version=1.1" + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app(init_only=('versions',))) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['version']['id'], 'v2.0') + + def test_accept_version_v1_1(self): + # Test Accept header specifying v1.1 returns v2 content. + req = webob.Request.blank('/') + req.accept = "application/json;version=1.1" + res = req.get_response(fakes.wsgi_app(init_only=('versions',))) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['version']['id'], 'v2.0') + + def test_path_version_v2(self): + # Test URL path specifying v2 returns v2 content. + req = webob.Request.blank('/v2/') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app(init_only=('versions',))) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['version']['id'], 'v2.0') + + def test_content_type_version_v2(self): + # Test Content-Type specifying v2 returns v2 content. + req = webob.Request.blank('/') + req.content_type = "application/json;version=2" + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app(init_only=('versions',))) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['version']['id'], 'v2.0') + + def test_accept_version_v2(self): + # Test Accept header specifying v2 returns v2 content. + req = webob.Request.blank('/') + req.accept = "application/json;version=2" + res = req.get_response(fakes.wsgi_app(init_only=('versions',))) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['version']['id'], 'v2.0') + + def test_path_content_type(self): + # Test URL path specifying JSON returns JSON content. + url = '/v2/fake/images/cedef40a-ed67-4d10-800e-17455edce175.json' + req = webob.Request.blank(url) + req.accept = "application/xml" + res = req.get_response(fakes.wsgi_app(init_only=('images',))) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['image']['id'], + 'cedef40a-ed67-4d10-800e-17455edce175') + + def test_accept_content_type(self): + # Test Accept header specifying JSON returns JSON content. + url = '/v2/fake/images/cedef40a-ed67-4d10-800e-17455edce175' + req = webob.Request.blank(url) + req.accept = "application/xml;q=0.8, application/json" + res = req.get_response(fakes.wsgi_app(init_only=('images',))) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['image']['id'], + 'cedef40a-ed67-4d10-800e-17455edce175') + + def test_path_version_v21(self): + # Test URL path specifying v2.1 returns v2.1 content. + req = webob.Request.blank('/v2.1/') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app_v21(init_only=('versions',))) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['version']['id'], 'v2.1') + + def test_content_type_version_v21(self): + # Test Content-Type specifying v2.1 returns v2 content. + req = webob.Request.blank('/') + req.content_type = "application/json;version=2.1" + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app_v21(init_only=('versions',))) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['version']['id'], 'v2.1') + + def test_accept_version_v21(self): + # Test Accept header specifying v2.1 returns v2.1 content. + req = webob.Request.blank('/') + req.accept = "application/json;version=2.1" + res = req.get_response(fakes.wsgi_app_v21(init_only=('versions',))) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['version']['id'], 'v2.1') + + def test_path_content_type_v21(self): + # Test URL path specifying JSON returns JSON content. + url = '/v2.1/fake/extensions/extensions.json' + req = webob.Request.blank(url) + req.accept = "application/xml" + res = req.get_response(fakes.wsgi_app_v21()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['extension']['name'], 'Extensions') + + def test_accept_content_type_v21(self): + # Test Accept header specifying JSON returns JSON content. + url = '/v2.1/fake/extensions/extensions' + req = webob.Request.blank(url) + req.accept = "application/xml;q=0.8, application/json" + res = req.get_response(fakes.wsgi_app_v21(init_only=('extensions',))) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + body = jsonutils.loads(res.body) + self.assertEqual(body['extension']['name'], 'Extensions') diff --git a/nova/tests/unit/api/openstack/compute/test_v21_extensions.py b/nova/tests/unit/api/openstack/compute/test_v21_extensions.py new file mode 100644 index 0000000000..7998dc82e5 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_v21_extensions.py @@ -0,0 +1,196 @@ +# Copyright 2013 IBM Corp. +# Copyright 2014 NEC Corporation. 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. + +from oslo.config import cfg +import stevedore +import webob.exc + +from nova.api import openstack +from nova.api.openstack import compute +from nova.api.openstack.compute import plugins +from nova.api.openstack import extensions +from nova import exception +from nova import test + +CONF = cfg.CONF + + +class fake_bad_extension(object): + name = "fake_bad_extension" + alias = "fake-bad" + + +class fake_stevedore_enabled_extensions(object): + def __init__(self, namespace, check_func, invoke_on_load=False, + invoke_args=(), invoke_kwds=None): + self.extensions = [] + + def map(self, func, *args, **kwds): + pass + + def __iter__(self): + return iter(self.extensions) + + +class fake_loaded_extension_info(object): + def __init__(self): + self.extensions = {} + + def register_extension(self, ext): + self.extensions[ext] = ext + return True + + def get_extensions(self): + return {'core1': None, 'core2': None, 'noncore1': None} + + +class ExtensionLoadingTestCase(test.NoDBTestCase): + + def _set_v21_core(self, core_extensions): + openstack.API_V3_CORE_EXTENSIONS = core_extensions + + def test_extensions_loaded(self): + app = compute.APIRouterV21() + self.assertIn('servers', app._loaded_extension_info.extensions) + + def test_check_bad_extension(self): + extension_info = plugins.LoadedExtensionInfo() + self.assertFalse(extension_info._check_extension(fake_bad_extension)) + + def test_extensions_blacklist(self): + app = compute.APIRouterV21() + self.assertIn('os-hosts', app._loaded_extension_info.extensions) + CONF.set_override('extensions_blacklist', ['os-hosts'], 'osapi_v3') + app = compute.APIRouterV21() + self.assertNotIn('os-hosts', app._loaded_extension_info.extensions) + + def test_extensions_whitelist_accept(self): + # NOTE(maurosr): just to avoid to get an exception raised for not + # loading all core api. + v21_core = openstack.API_V3_CORE_EXTENSIONS + openstack.API_V3_CORE_EXTENSIONS = set(['servers']) + self.addCleanup(self._set_v21_core, v21_core) + + app = compute.APIRouterV21() + self.assertIn('os-hosts', app._loaded_extension_info.extensions) + CONF.set_override('extensions_whitelist', ['servers', 'os-hosts'], + 'osapi_v3') + app = compute.APIRouterV21() + self.assertIn('os-hosts', app._loaded_extension_info.extensions) + + def test_extensions_whitelist_block(self): + # NOTE(maurosr): just to avoid to get an exception raised for not + # loading all core api. + v21_core = openstack.API_V3_CORE_EXTENSIONS + openstack.API_V3_CORE_EXTENSIONS = set(['servers']) + self.addCleanup(self._set_v21_core, v21_core) + + app = compute.APIRouterV21() + self.assertIn('os-hosts', app._loaded_extension_info.extensions) + CONF.set_override('extensions_whitelist', ['servers'], 'osapi_v3') + app = compute.APIRouterV21() + self.assertNotIn('os-hosts', app._loaded_extension_info.extensions) + + def test_blacklist_overrides_whitelist(self): + # NOTE(maurosr): just to avoid to get an exception raised for not + # loading all core api. + v21_core = openstack.API_V3_CORE_EXTENSIONS + openstack.API_V3_CORE_EXTENSIONS = set(['servers']) + self.addCleanup(self._set_v21_core, v21_core) + + app = compute.APIRouterV21() + self.assertIn('os-hosts', app._loaded_extension_info.extensions) + CONF.set_override('extensions_whitelist', ['servers', 'os-hosts'], + 'osapi_v3') + CONF.set_override('extensions_blacklist', ['os-hosts'], 'osapi_v3') + app = compute.APIRouterV21() + self.assertNotIn('os-hosts', app._loaded_extension_info.extensions) + self.assertIn('servers', app._loaded_extension_info.extensions) + self.assertEqual(1, len(app._loaded_extension_info.extensions)) + + def test_get_missing_core_extensions(self): + v21_core = openstack.API_V3_CORE_EXTENSIONS + openstack.API_V3_CORE_EXTENSIONS = set(['core1', 'core2']) + self.addCleanup(self._set_v21_core, v21_core) + self.assertEqual(0, len( + compute.APIRouterV21.get_missing_core_extensions( + ['core1', 'core2', 'noncore1']))) + missing_core = compute.APIRouterV21.get_missing_core_extensions( + ['core1']) + self.assertEqual(1, len(missing_core)) + self.assertIn('core2', missing_core) + missing_core = compute.APIRouterV21.get_missing_core_extensions([]) + self.assertEqual(2, len(missing_core)) + self.assertIn('core1', missing_core) + self.assertIn('core2', missing_core) + missing_core = compute.APIRouterV21.get_missing_core_extensions( + ['noncore1']) + self.assertEqual(2, len(missing_core)) + self.assertIn('core1', missing_core) + self.assertIn('core2', missing_core) + + def test_core_extensions_present(self): + self.stubs.Set(stevedore.enabled, 'EnabledExtensionManager', + fake_stevedore_enabled_extensions) + self.stubs.Set(plugins, 'LoadedExtensionInfo', + fake_loaded_extension_info) + v21_core = openstack.API_V3_CORE_EXTENSIONS + openstack.API_V3_CORE_EXTENSIONS = set(['core1', 'core2']) + self.addCleanup(self._set_v21_core, v21_core) + # if no core API extensions are missing then an exception will + # not be raised when creating an instance of compute.APIRouterV21 + compute.APIRouterV21() + + def test_core_extensions_missing(self): + self.stubs.Set(stevedore.enabled, 'EnabledExtensionManager', + fake_stevedore_enabled_extensions) + self.stubs.Set(plugins, 'LoadedExtensionInfo', + fake_loaded_extension_info) + self.assertRaises(exception.CoreAPIMissing, compute.APIRouterV21) + + def test_extensions_expected_error(self): + @extensions.expected_errors(404) + def fake_func(): + raise webob.exc.HTTPNotFound() + + self.assertRaises(webob.exc.HTTPNotFound, fake_func) + + def test_extensions_expected_error_from_list(self): + @extensions.expected_errors((404, 403)) + def fake_func(): + raise webob.exc.HTTPNotFound() + + self.assertRaises(webob.exc.HTTPNotFound, fake_func) + + def test_extensions_unexpected_error(self): + @extensions.expected_errors(404) + def fake_func(): + raise webob.exc.HTTPConflict() + + self.assertRaises(webob.exc.HTTPInternalServerError, fake_func) + + def test_extensions_unexpected_error_from_list(self): + @extensions.expected_errors((404, 413)) + def fake_func(): + raise webob.exc.HTTPConflict() + + self.assertRaises(webob.exc.HTTPInternalServerError, fake_func) + + def test_extensions_unexpected_policy_not_authorized_error(self): + @extensions.expected_errors(404) + def fake_func(): + raise exception.PolicyNotAuthorized(action="foo") + + self.assertRaises(exception.PolicyNotAuthorized, fake_func) diff --git a/nova/tests/unit/api/openstack/compute/test_v3_auth.py b/nova/tests/unit/api/openstack/compute/test_v3_auth.py new file mode 100644 index 0000000000..e728fa89d6 --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_v3_auth.py @@ -0,0 +1,62 @@ +# Copyright 2013 IBM Corp. +# Copyright 2010 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 webob +import webob.dec + +from nova import context +from nova import test +from nova.tests.unit.api.openstack import fakes + + +class TestNoAuthMiddlewareV3(test.NoDBTestCase): + + def setUp(self): + super(TestNoAuthMiddlewareV3, self).setUp() + self.stubs.Set(context, 'RequestContext', fakes.FakeRequestContext) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_networking(self.stubs) + + def test_authorize_user(self): + req = webob.Request.blank('/v2/fake') + req.headers['X-Auth-User'] = 'user1' + req.headers['X-Auth-Key'] = 'user1_key' + req.headers['X-Auth-Project-Id'] = 'user1_project' + result = req.get_response(fakes.wsgi_app_v21(use_no_auth=True)) + self.assertEqual(result.status, '204 No Content') + self.assertEqual(result.headers['X-Server-Management-Url'], + "http://localhost/v2/fake") + + def test_authorize_user_trailing_slash(self): + # make sure it works with trailing slash on the request + req = webob.Request.blank('/v2/fake/') + req.headers['X-Auth-User'] = 'user1' + req.headers['X-Auth-Key'] = 'user1_key' + req.headers['X-Auth-Project-Id'] = 'user1_project' + result = req.get_response(fakes.wsgi_app_v21(use_no_auth=True)) + self.assertEqual(result.status, '204 No Content') + self.assertEqual(result.headers['X-Server-Management-Url'], + "http://localhost/v2/fake") + + def test_auth_token_no_empty_headers(self): + req = webob.Request.blank('/v2/fake') + req.headers['X-Auth-User'] = 'user1' + req.headers['X-Auth-Key'] = 'user1_key' + req.headers['X-Auth-Project-Id'] = 'user1_project' + result = req.get_response(fakes.wsgi_app_v21(use_no_auth=True)) + self.assertEqual(result.status, '204 No Content') + self.assertNotIn('X-CDN-Management-Url', result.headers) + self.assertNotIn('X-Storage-Url', result.headers) diff --git a/nova/tests/unit/api/openstack/compute/test_v3_extensions.py b/nova/tests/unit/api/openstack/compute/test_v3_extensions.py new file mode 100644 index 0000000000..da6aa43d7f --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_v3_extensions.py @@ -0,0 +1,194 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +from oslo.config import cfg +import stevedore +import webob.exc + +from nova.api import openstack +from nova.api.openstack import compute +from nova.api.openstack.compute import plugins +from nova.api.openstack import extensions +from nova import exception +from nova import test + +CONF = cfg.CONF + + +class fake_bad_extension(object): + name = "fake_bad_extension" + alias = "fake-bad" + + +class fake_stevedore_enabled_extensions(object): + def __init__(self, namespace, check_func, invoke_on_load=False, + invoke_args=(), invoke_kwds=None): + self.extensions = [] + + def map(self, func, *args, **kwds): + pass + + def __iter__(self): + return iter(self.extensions) + + +class fake_loaded_extension_info(object): + def __init__(self): + self.extensions = {} + + def register_extension(self, ext): + self.extensions[ext] = ext + return True + + def get_extensions(self): + return {'core1': None, 'core2': None, 'noncore1': None} + + +class ExtensionLoadingTestCase(test.NoDBTestCase): + + def _set_v3_core(self, core_extensions): + openstack.API_V3_CORE_EXTENSIONS = core_extensions + + def test_extensions_loaded(self): + app = compute.APIRouterV3() + self.assertIn('servers', app._loaded_extension_info.extensions) + + def test_check_bad_extension(self): + extension_info = plugins.LoadedExtensionInfo() + self.assertFalse(extension_info._check_extension(fake_bad_extension)) + + def test_extensions_blacklist(self): + app = compute.APIRouterV3() + self.assertIn('os-hosts', app._loaded_extension_info.extensions) + CONF.set_override('extensions_blacklist', ['os-hosts'], 'osapi_v3') + app = compute.APIRouterV3() + self.assertNotIn('os-hosts', app._loaded_extension_info.extensions) + + def test_extensions_whitelist_accept(self): + # NOTE(maurosr): just to avoid to get an exception raised for not + # loading all core api. + v3_core = openstack.API_V3_CORE_EXTENSIONS + openstack.API_V3_CORE_EXTENSIONS = set(['servers']) + self.addCleanup(self._set_v3_core, v3_core) + + app = compute.APIRouterV3() + self.assertIn('os-hosts', app._loaded_extension_info.extensions) + CONF.set_override('extensions_whitelist', ['servers', 'os-hosts'], + 'osapi_v3') + app = compute.APIRouterV3() + self.assertIn('os-hosts', app._loaded_extension_info.extensions) + + def test_extensions_whitelist_block(self): + # NOTE(maurosr): just to avoid to get an exception raised for not + # loading all core api. + v3_core = openstack.API_V3_CORE_EXTENSIONS + openstack.API_V3_CORE_EXTENSIONS = set(['servers']) + self.addCleanup(self._set_v3_core, v3_core) + + app = compute.APIRouterV3() + self.assertIn('os-hosts', app._loaded_extension_info.extensions) + CONF.set_override('extensions_whitelist', ['servers'], 'osapi_v3') + app = compute.APIRouterV3() + self.assertNotIn('os-hosts', app._loaded_extension_info.extensions) + + def test_blacklist_overrides_whitelist(self): + # NOTE(maurosr): just to avoid to get an exception raised for not + # loading all core api. + v3_core = openstack.API_V3_CORE_EXTENSIONS + openstack.API_V3_CORE_EXTENSIONS = set(['servers']) + self.addCleanup(self._set_v3_core, v3_core) + + app = compute.APIRouterV3() + self.assertIn('os-hosts', app._loaded_extension_info.extensions) + CONF.set_override('extensions_whitelist', ['servers', 'os-hosts'], + 'osapi_v3') + CONF.set_override('extensions_blacklist', ['os-hosts'], 'osapi_v3') + app = compute.APIRouterV3() + self.assertNotIn('os-hosts', app._loaded_extension_info.extensions) + self.assertIn('servers', app._loaded_extension_info.extensions) + self.assertEqual(len(app._loaded_extension_info.extensions), 1) + + def test_get_missing_core_extensions(self): + v3_core = openstack.API_V3_CORE_EXTENSIONS + openstack.API_V3_CORE_EXTENSIONS = set(['core1', 'core2']) + self.addCleanup(self._set_v3_core, v3_core) + self.assertEqual(len(compute.APIRouterV3.get_missing_core_extensions( + ['core1', 'core2', 'noncore1'])), 0) + missing_core = compute.APIRouterV3.get_missing_core_extensions( + ['core1']) + self.assertEqual(len(missing_core), 1) + self.assertIn('core2', missing_core) + missing_core = compute.APIRouterV3.get_missing_core_extensions([]) + self.assertEqual(len(missing_core), 2) + self.assertIn('core1', missing_core) + self.assertIn('core2', missing_core) + missing_core = compute.APIRouterV3.get_missing_core_extensions( + ['noncore1']) + self.assertEqual(len(missing_core), 2) + self.assertIn('core1', missing_core) + self.assertIn('core2', missing_core) + + def test_core_extensions_present(self): + self.stubs.Set(stevedore.enabled, 'EnabledExtensionManager', + fake_stevedore_enabled_extensions) + self.stubs.Set(plugins, 'LoadedExtensionInfo', + fake_loaded_extension_info) + v3_core = openstack.API_V3_CORE_EXTENSIONS + openstack.API_V3_CORE_EXTENSIONS = set(['core1', 'core2']) + self.addCleanup(self._set_v3_core, v3_core) + # if no core API extensions are missing then an exception will + # not be raised when creating an instance of compute.APIRouterV3 + compute.APIRouterV3() + + def test_core_extensions_missing(self): + self.stubs.Set(stevedore.enabled, 'EnabledExtensionManager', + fake_stevedore_enabled_extensions) + self.stubs.Set(plugins, 'LoadedExtensionInfo', + fake_loaded_extension_info) + self.assertRaises(exception.CoreAPIMissing, compute.APIRouterV3) + + def test_extensions_expected_error(self): + @extensions.expected_errors(404) + def fake_func(): + raise webob.exc.HTTPNotFound() + + self.assertRaises(webob.exc.HTTPNotFound, fake_func) + + def test_extensions_expected_error_from_list(self): + @extensions.expected_errors((404, 403)) + def fake_func(): + raise webob.exc.HTTPNotFound() + + self.assertRaises(webob.exc.HTTPNotFound, fake_func) + + def test_extensions_unexpected_error(self): + @extensions.expected_errors(404) + def fake_func(): + raise webob.exc.HTTPConflict() + + self.assertRaises(webob.exc.HTTPInternalServerError, fake_func) + + def test_extensions_unexpected_error_from_list(self): + @extensions.expected_errors((404, 413)) + def fake_func(): + raise webob.exc.HTTPConflict() + + self.assertRaises(webob.exc.HTTPInternalServerError, fake_func) + + def test_extensions_unexpected_policy_not_authorized_error(self): + @extensions.expected_errors(404) + def fake_func(): + raise exception.PolicyNotAuthorized(action="foo") + + self.assertRaises(exception.PolicyNotAuthorized, fake_func) diff --git a/nova/tests/unit/api/openstack/compute/test_versions.py b/nova/tests/unit/api/openstack/compute/test_versions.py new file mode 100644 index 0000000000..fabd15e01c --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_versions.py @@ -0,0 +1,797 @@ +# Copyright 2010-2011 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 copy +import uuid as stdlib_uuid + +import feedparser +from lxml import etree +from oslo.serialization import jsonutils +import webob + +from nova.api.openstack.compute import versions +from nova.api.openstack.compute import views +from nova.api.openstack import xmlutil +from nova import test +from nova.tests.unit.api.openstack import common +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import matchers + + +NS = { + 'atom': 'http://www.w3.org/2005/Atom', + 'ns': 'http://docs.openstack.org/common/api/v1.0' +} + + +EXP_LINKS = { + 'v2.0': { + 'html': 'http://docs.openstack.org/', + }, + 'v2.1': { + 'html': 'http://docs.openstack.org/' + }, +} + + +EXP_VERSIONS = { + "v2.0": { + "id": "v2.0", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "describedby", + "type": "text/html", + "href": EXP_LINKS['v2.0']['html'], + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml;version=2", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=2", + }, + ], + }, + "v2.1": { + "id": "v2.1", + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2.1/", + }, + { + "rel": "describedby", + "type": "text/html", + "href": EXP_LINKS['v2.1']['html'], + }, + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json;version=2.1", + } + ], + } +} + + +class VersionsTestV20(test.NoDBTestCase): + + def test_get_version_list(self): + req = webob.Request.blank('/') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + versions = jsonutils.loads(res.body)["versions"] + expected = [ + { + "id": "v2.0", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/", + }], + }, + { + "id": "v2.1", + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/", + }], + }, + ] + self.assertEqual(versions, expected) + + def test_get_version_list_302(self): + req = webob.Request.blank('/v2') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 302) + redirect_req = webob.Request.blank('/v2/') + self.assertEqual(res.location, redirect_req.url) + + def _test_get_version_2_detail(self, url, accept=None): + if accept is None: + accept = "application/json" + req = webob.Request.blank(url) + req.accept = accept + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + version = jsonutils.loads(res.body) + expected = { + "version": { + "id": "v2.0", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/", + }, + { + "rel": "describedby", + "type": "text/html", + "href": EXP_LINKS['v2.0']['html'], + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/" + "vnd.openstack.compute+xml;version=2", + }, + { + "base": "application/json", + "type": "application/" + "vnd.openstack.compute+json;version=2", + }, + ], + }, + } + self.assertEqual(expected, version) + + def test_get_version_2_detail(self): + self._test_get_version_2_detail('/v2/') + + def test_get_version_2_detail_content_type(self): + accept = "application/json;version=2" + self._test_get_version_2_detail('/', accept=accept) + + def test_get_version_2_versions_invalid(self): + req = webob.Request.blank('/v2/versions/1234') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(404, res.status_int) + + def test_get_version_2_detail_xml(self): + req = webob.Request.blank('/v2/') + req.accept = "application/xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/xml") + + version = etree.XML(res.body) + xmlutil.validate_schema(version, 'version') + + expected = EXP_VERSIONS['v2.0'] + self.assertTrue(version.xpath('/ns:version', namespaces=NS)) + media_types = version.xpath('ns:media-types/ns:media-type', + namespaces=NS) + self.assertTrue(common.compare_media_types(media_types, + expected['media-types'])) + for key in ['id', 'status', 'updated']: + self.assertEqual(version.get(key), expected[key]) + links = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(links, + [{'rel': 'self', 'href': 'http://localhost/v2/'}] + + expected['links'])) + + def test_get_version_list_xml(self): + req = webob.Request.blank('/') + req.accept = "application/xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/xml") + + root = etree.XML(res.body) + xmlutil.validate_schema(root, 'versions') + + self.assertTrue(root.xpath('/ns:versions', namespaces=NS)) + versions = root.xpath('ns:version', namespaces=NS) + self.assertEqual(len(versions), 2) + + for i, v in enumerate(['v2.0', 'v2.1']): + version = versions[i] + expected = EXP_VERSIONS[v] + for key in ['id', 'status', 'updated']: + self.assertEqual(version.get(key), expected[key]) + (link,) = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(link, + [{'rel': 'self', 'href': 'http://localhost/%s/' % v}])) + + def test_get_version_2_detail_atom(self): + req = webob.Request.blank('/v2/') + req.accept = "application/atom+xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual("application/atom+xml", res.content_type) + + xmlutil.validate_schema(etree.XML(res.body), 'atom') + + f = feedparser.parse(res.body) + self.assertEqual(f.feed.title, 'About This Version') + self.assertEqual(f.feed.updated, '2011-01-21T11:33:21Z') + self.assertEqual(f.feed.id, 'http://localhost/v2/') + self.assertEqual(f.feed.author, 'Rackspace') + self.assertEqual(f.feed.author_detail.href, + 'http://www.rackspace.com/') + self.assertEqual(f.feed.links[0]['href'], 'http://localhost/v2/') + self.assertEqual(f.feed.links[0]['rel'], 'self') + + self.assertEqual(len(f.entries), 1) + entry = f.entries[0] + self.assertEqual(entry.id, 'http://localhost/v2/') + self.assertEqual(entry.title, 'Version v2.0') + self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v2.0 CURRENT (2011-01-21T11:33:21Z)') + self.assertEqual(len(entry.links), 2) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/') + self.assertEqual(entry.links[0]['rel'], 'self') + self.assertEqual(entry.links[1], { + 'href': EXP_LINKS['v2.0']['html'], + 'type': 'text/html', + 'rel': 'describedby'}) + + def test_get_version_list_atom(self): + req = webob.Request.blank('/') + req.accept = "application/atom+xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/atom+xml") + + f = feedparser.parse(res.body) + self.assertEqual(f.feed.title, 'Available API Versions') + self.assertEqual(f.feed.updated, '2013-07-23T11:33:21Z') + self.assertEqual(f.feed.id, 'http://localhost/') + self.assertEqual(f.feed.author, 'Rackspace') + self.assertEqual(f.feed.author_detail.href, + 'http://www.rackspace.com/') + self.assertEqual(f.feed.links[0]['href'], 'http://localhost/') + self.assertEqual(f.feed.links[0]['rel'], 'self') + + self.assertEqual(len(f.entries), 2) + entry = f.entries[0] + self.assertEqual(entry.id, 'http://localhost/v2/') + self.assertEqual(entry.title, 'Version v2.0') + self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v2.0 CURRENT (2011-01-21T11:33:21Z)') + self.assertEqual(len(entry.links), 1) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/') + self.assertEqual(entry.links[0]['rel'], 'self') + + entry = f.entries[1] + self.assertEqual(entry.id, 'http://localhost/v2/') + self.assertEqual(entry.title, 'Version v2.1') + self.assertEqual(entry.updated, '2013-07-23T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v2.1 EXPERIMENTAL (2013-07-23T11:33:21Z)') + self.assertEqual(len(entry.links), 1) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/') + self.assertEqual(entry.links[0]['rel'], 'self') + + def test_multi_choice_image(self): + req = webob.Request.blank('/images/1') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 300) + self.assertEqual(res.content_type, "application/json") + + expected = { + "choices": [ + { + "id": "v2.0", + "status": "CURRENT", + "links": [ + { + "href": "http://localhost/v2/images/1", + "rel": "self", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml" + ";version=2" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json" + ";version=2" + }, + ], + }, + { + "id": "v2.1", + "status": "EXPERIMENTAL", + "links": [ + { + "href": "http://localhost/v2/images/1", + "rel": "self", + }, + ], + "media-types": [ + { + "base": "application/json", + "type": + "application/vnd.openstack.compute+json;version=2.1", + } + ], + }, + ], } + + self.assertThat(jsonutils.loads(res.body), + matchers.DictMatches(expected)) + + def test_multi_choice_image_xml(self): + req = webob.Request.blank('/images/1') + req.accept = "application/xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 300) + self.assertEqual(res.content_type, "application/xml") + + root = etree.XML(res.body) + self.assertTrue(root.xpath('/ns:choices', namespaces=NS)) + versions = root.xpath('ns:version', namespaces=NS) + self.assertEqual(len(versions), 2) + + version = versions[0] + self.assertEqual(version.get('id'), 'v2.0') + self.assertEqual(version.get('status'), 'CURRENT') + media_types = version.xpath('ns:media-types/ns:media-type', + namespaces=NS) + self.assertTrue(common. + compare_media_types(media_types, + EXP_VERSIONS['v2.0']['media-types'] + )) + + links = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(links, + [{'rel': 'self', 'href': 'http://localhost/v2/images/1'}])) + + version = versions[1] + self.assertEqual(version.get('id'), 'v2.1') + self.assertEqual(version.get('status'), 'EXPERIMENTAL') + media_types = version.xpath('ns:media-types/ns:media-type', + namespaces=NS) + self.assertTrue(common. + compare_media_types(media_types, + EXP_VERSIONS['v2.1']['media-types'] + )) + + links = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(links, + [{'rel': 'self', 'href': 'http://localhost/v2/images/1'}])) + + def test_multi_choice_server_atom(self): + """Make sure multi choice responses do not have content-type + application/atom+xml (should use default of json) + """ + req = webob.Request.blank('/servers') + req.accept = "application/atom+xml" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 300) + self.assertEqual(res.content_type, "application/json") + + def test_multi_choice_server(self): + uuid = str(stdlib_uuid.uuid4()) + req = webob.Request.blank('/servers/' + uuid) + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 300) + self.assertEqual(res.content_type, "application/json") + + expected = { + "choices": [ + { + "id": "v2.0", + "status": "CURRENT", + "links": [ + { + "href": "http://localhost/v2/servers/" + uuid, + "rel": "self", + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml" + ";version=2" + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json" + ";version=2" + }, + ], + }, + { + "id": "v2.1", + "status": "EXPERIMENTAL", + "links": [ + { + "href": "http://localhost/v2/servers/" + uuid, + "rel": "self", + }, + ], + "media-types": [ + { + "base": "application/json", + "type": + "application/vnd.openstack.compute+json;version=2.1", + } + ], + }, + ], } + + self.assertThat(jsonutils.loads(res.body), + matchers.DictMatches(expected)) + + +class VersionsViewBuilderTests(test.NoDBTestCase): + def test_view_builder(self): + base_url = "http://example.org/" + + version_data = { + "v3.2.1": { + "id": "3.2.1", + "status": "CURRENT", + "updated": "2011-07-18T11:30:00Z", + } + } + + expected = { + "versions": [ + { + "id": "3.2.1", + "status": "CURRENT", + "updated": "2011-07-18T11:30:00Z", + "links": [ + { + "rel": "self", + "href": "http://example.org/v2/", + }, + ], + } + ] + } + + builder = views.versions.ViewBuilder(base_url) + output = builder.build_versions(version_data) + + self.assertEqual(output, expected) + + def test_generate_href(self): + base_url = "http://example.org/app/" + + expected = "http://example.org/app/v2/" + + builder = views.versions.ViewBuilder(base_url) + actual = builder.generate_href('v2') + + self.assertEqual(actual, expected) + + def test_generate_href_v21(self): + base_url = "http://example.org/app/" + + expected = "http://example.org/app/v2/" + + builder = views.versions.ViewBuilder(base_url) + actual = builder.generate_href('v2.1') + + self.assertEqual(actual, expected) + + def test_generate_href_unknown(self): + base_url = "http://example.org/app/" + + expected = "http://example.org/app/v2/" + + builder = views.versions.ViewBuilder(base_url) + actual = builder.generate_href('foo') + + self.assertEqual(actual, expected) + + +class VersionsSerializerTests(test.NoDBTestCase): + def test_versions_list_xml_serializer(self): + versions_data = { + 'versions': [ + { + "id": "2.7", + "updated": "2011-07-18T11:30:00Z", + "status": "DEPRECATED", + "links": [ + { + "rel": "self", + "href": "http://test/v2", + }, + ], + }, + ] + } + + serializer = versions.VersionsTemplate() + response = serializer.serialize(versions_data) + + root = etree.XML(response) + xmlutil.validate_schema(root, 'versions') + + self.assertTrue(root.xpath('/ns:versions', namespaces=NS)) + version_elems = root.xpath('ns:version', namespaces=NS) + self.assertEqual(len(version_elems), 1) + version = version_elems[0] + self.assertEqual(version.get('id'), versions_data['versions'][0]['id']) + self.assertEqual(version.get('status'), + versions_data['versions'][0]['status']) + + (link,) = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(link, [{ + 'rel': 'self', + 'href': 'http://test/v2', + 'type': 'application/atom+xml'}])) + + def test_versions_multi_xml_serializer(self): + versions_data = { + 'choices': [ + { + "id": "2.7", + "updated": "2011-07-18T11:30:00Z", + "status": "DEPRECATED", + "media-types": EXP_VERSIONS['v2.0']['media-types'], + "links": [ + { + "rel": "self", + "href": "http://test/v2/images", + }, + ], + }, + ] + } + + serializer = versions.ChoicesTemplate() + response = serializer.serialize(versions_data) + + root = etree.XML(response) + self.assertTrue(root.xpath('/ns:choices', namespaces=NS)) + (version,) = root.xpath('ns:version', namespaces=NS) + self.assertEqual(version.get('id'), versions_data['choices'][0]['id']) + self.assertEqual(version.get('status'), + versions_data['choices'][0]['status']) + + media_types = list(version)[0] + self.assertEqual(media_types.tag.split('}')[1], "media-types") + + media_types = version.xpath('ns:media-types/ns:media-type', + namespaces=NS) + self.assertTrue(common.compare_media_types(media_types, + versions_data['choices'][0]['media-types'])) + + (link,) = version.xpath('atom:link', namespaces=NS) + self.assertTrue(common.compare_links(link, + versions_data['choices'][0]['links'])) + + def test_versions_list_atom_serializer(self): + versions_data = { + 'versions': [ + { + "id": "2.9.8", + "updated": "2011-07-20T11:40:00Z", + "status": "CURRENT", + "links": [ + { + "rel": "self", + "href": "http://test/2.9.8", + }, + ], + }, + ] + } + + serializer = versions.VersionsAtomSerializer() + response = serializer.serialize(versions_data) + f = feedparser.parse(response) + + self.assertEqual(f.feed.title, 'Available API Versions') + self.assertEqual(f.feed.updated, '2011-07-20T11:40:00Z') + self.assertEqual(f.feed.id, 'http://test/') + self.assertEqual(f.feed.author, 'Rackspace') + self.assertEqual(f.feed.author_detail.href, + 'http://www.rackspace.com/') + self.assertEqual(f.feed.links[0]['href'], 'http://test/') + self.assertEqual(f.feed.links[0]['rel'], 'self') + + self.assertEqual(len(f.entries), 1) + entry = f.entries[0] + self.assertEqual(entry.id, 'http://test/2.9.8') + self.assertEqual(entry.title, 'Version 2.9.8') + self.assertEqual(entry.updated, '2011-07-20T11:40:00Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version 2.9.8 CURRENT (2011-07-20T11:40:00Z)') + self.assertEqual(len(entry.links), 1) + self.assertEqual(entry.links[0]['href'], 'http://test/2.9.8') + self.assertEqual(entry.links[0]['rel'], 'self') + + def test_version_detail_atom_serializer(self): + versions_data = { + "version": { + "id": "v2.0", + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "links": [ + { + "rel": "self", + "href": "http://localhost/v2/", + }, + { + "rel": "describedby", + "type": "text/html", + "href": EXP_LINKS['v2.0']['html'], + }, + ], + "media-types": [ + { + "base": "application/xml", + "type": "application/vnd.openstack.compute+xml" + ";version=2", + }, + { + "base": "application/json", + "type": "application/vnd.openstack.compute+json" + ";version=2", + } + ], + }, + } + + serializer = versions.VersionAtomSerializer() + response = serializer.serialize(versions_data) + f = feedparser.parse(response) + + self.assertEqual(f.feed.title, 'About This Version') + self.assertEqual(f.feed.updated, '2011-01-21T11:33:21Z') + self.assertEqual(f.feed.id, 'http://localhost/v2/') + self.assertEqual(f.feed.author, 'Rackspace') + self.assertEqual(f.feed.author_detail.href, + 'http://www.rackspace.com/') + self.assertEqual(f.feed.links[0]['href'], 'http://localhost/v2/') + self.assertEqual(f.feed.links[0]['rel'], 'self') + + self.assertEqual(len(f.entries), 1) + entry = f.entries[0] + self.assertEqual(entry.id, 'http://localhost/v2/') + self.assertEqual(entry.title, 'Version v2.0') + self.assertEqual(entry.updated, '2011-01-21T11:33:21Z') + self.assertEqual(len(entry.content), 1) + self.assertEqual(entry.content[0].value, + 'Version v2.0 CURRENT (2011-01-21T11:33:21Z)') + self.assertEqual(len(entry.links), 2) + self.assertEqual(entry.links[0]['href'], 'http://localhost/v2/') + self.assertEqual(entry.links[0]['rel'], 'self') + self.assertEqual(entry.links[1], { + 'rel': 'describedby', + 'type': 'text/html', + 'href': EXP_LINKS['v2.0']['html']}) + + def test_multi_choice_image_with_body(self): + req = webob.Request.blank('/images/1') + req.accept = "application/json" + req.method = 'POST' + req.content_type = "application/json" + req.body = "{\"foo\": \"bar\"}" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(300, res.status_int) + self.assertEqual("application/json", res.content_type) + + def test_get_version_list_with_body(self): + req = webob.Request.blank('/') + req.accept = "application/json" + req.method = 'POST' + req.content_type = "application/json" + req.body = "{\"foo\": \"bar\"}" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(200, res.status_int) + self.assertEqual("application/json", res.content_type) + + +# NOTE(oomichi): Now version API of v2.0 covers "/"(root). +# So this class tests "/v2.1" only for v2.1 API. +class VersionsTestV21(test.NoDBTestCase): + exp_versions = copy.deepcopy(EXP_VERSIONS) + exp_versions['v2.0']['links'].insert(0, + {'href': 'http://localhost/v2.1/', 'rel': 'self'}, + ) + + def test_get_version_list_302(self): + req = webob.Request.blank('/v2.1') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app_v21()) + self.assertEqual(res.status_int, 302) + redirect_req = webob.Request.blank('/v2.1/') + self.assertEqual(res.location, redirect_req.url) + + def test_get_version_21_detail(self): + req = webob.Request.blank('/v2.1/') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app_v21()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + version = jsonutils.loads(res.body) + expected = {"version": self.exp_versions['v2.1']} + self.assertEqual(expected, version) + + def test_get_version_21_versions_v21_detail(self): + req = webob.Request.blank('/v2.1/fake/versions/v2.1') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app_v21()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + version = jsonutils.loads(res.body) + expected = {"version": self.exp_versions['v2.1']} + self.assertEqual(expected, version) + + def test_get_version_21_versions_v20_detail(self): + req = webob.Request.blank('/v2.1/fake/versions/v2.0') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app_v21()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + version = jsonutils.loads(res.body) + expected = {"version": self.exp_versions['v2.0']} + self.assertEqual(expected, version) + + def test_get_version_21_versions_invalid(self): + req = webob.Request.blank('/v2.1/versions/1234') + req.accept = "application/json" + res = req.get_response(fakes.wsgi_app_v21()) + self.assertEqual(res.status_int, 404) + + def test_get_version_21_detail_content_type(self): + req = webob.Request.blank('/') + req.accept = "application/json;version=2.1" + res = req.get_response(fakes.wsgi_app_v21()) + self.assertEqual(res.status_int, 200) + self.assertEqual(res.content_type, "application/json") + version = jsonutils.loads(res.body) + expected = {"version": self.exp_versions['v2.1']} + self.assertEqual(expected, version) diff --git a/nova/tests/unit/api/openstack/fakes.py b/nova/tests/unit/api/openstack/fakes.py new file mode 100644 index 0000000000..34c072a634 --- /dev/null +++ b/nova/tests/unit/api/openstack/fakes.py @@ -0,0 +1,662 @@ +# Copyright 2010 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 datetime +import uuid + +from oslo.serialization import jsonutils +from oslo.utils import timeutils +import routes +import six +import webob +import webob.dec +import webob.request + +from nova.api import auth as api_auth +from nova.api import openstack as openstack_api +from nova.api.openstack import auth +from nova.api.openstack import compute +from nova.api.openstack.compute import limits +from nova.api.openstack.compute import versions +from nova.api.openstack import urlmap +from nova.api.openstack import wsgi as os_wsgi +from nova.compute import api as compute_api +from nova.compute import flavors +from nova.compute import vm_states +from nova import context +from nova.db.sqlalchemy import models +from nova import exception as exc +import nova.netconf +from nova.network import api as network_api +from nova import quota +from nova.tests.unit import fake_block_device +from nova.tests.unit import fake_network +from nova.tests.unit.objects import test_keypair +from nova import utils +from nova import wsgi + + +QUOTAS = quota.QUOTAS + + +FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +FAKE_UUIDS = {} + + +class Context(object): + pass + + +class FakeRouter(wsgi.Router): + def __init__(self, ext_mgr=None): + pass + + @webob.dec.wsgify + def __call__(self, req): + res = webob.Response() + res.status = '200' + res.headers['X-Test-Success'] = 'True' + return res + + +@webob.dec.wsgify +def fake_wsgi(self, req): + return self.application + + +def wsgi_app(inner_app_v2=None, fake_auth_context=None, + use_no_auth=False, ext_mgr=None, init_only=None): + if not inner_app_v2: + inner_app_v2 = compute.APIRouter(ext_mgr, init_only) + + if use_no_auth: + api_v2 = openstack_api.FaultWrapper(auth.NoAuthMiddleware( + limits.RateLimitingMiddleware(inner_app_v2))) + else: + if fake_auth_context is not None: + ctxt = fake_auth_context + else: + ctxt = context.RequestContext('fake', 'fake', auth_token=True) + api_v2 = openstack_api.FaultWrapper(api_auth.InjectContext(ctxt, + limits.RateLimitingMiddleware(inner_app_v2))) + + mapper = urlmap.URLMap() + mapper['/v2'] = api_v2 + mapper['/v1.1'] = api_v2 + mapper['/'] = openstack_api.FaultWrapper(versions.Versions()) + return mapper + + +def wsgi_app_v21(inner_app_v21=None, fake_auth_context=None, + use_no_auth=False, ext_mgr=None, init_only=None): + if not inner_app_v21: + inner_app_v21 = compute.APIRouterV21(init_only) + + if use_no_auth: + api_v21 = openstack_api.FaultWrapper(auth.NoAuthMiddlewareV3( + limits.RateLimitingMiddleware(inner_app_v21))) + else: + if fake_auth_context is not None: + ctxt = fake_auth_context + else: + ctxt = context.RequestContext('fake', 'fake', auth_token=True) + api_v21 = openstack_api.FaultWrapper(api_auth.InjectContext(ctxt, + limits.RateLimitingMiddleware(inner_app_v21))) + + mapper = urlmap.URLMap() + mapper['/v2'] = api_v21 + mapper['/v2.1'] = api_v21 + return mapper + + +def stub_out_key_pair_funcs(stubs, have_key_pair=True): + def key_pair(context, user_id): + return [dict(test_keypair.fake_keypair, + name='key', public_key='public_key')] + + def one_key_pair(context, user_id, name): + if name == 'key': + return dict(test_keypair.fake_keypair, + name='key', public_key='public_key') + else: + raise exc.KeypairNotFound(user_id=user_id, name=name) + + def no_key_pair(context, user_id): + return [] + + if have_key_pair: + stubs.Set(nova.db, 'key_pair_get_all_by_user', key_pair) + stubs.Set(nova.db, 'key_pair_get', one_key_pair) + else: + stubs.Set(nova.db, 'key_pair_get_all_by_user', no_key_pair) + + +def stub_out_rate_limiting(stubs): + def fake_rate_init(self, app): + super(limits.RateLimitingMiddleware, self).__init__(app) + self.application = app + + stubs.Set(nova.api.openstack.compute.limits.RateLimitingMiddleware, + '__init__', fake_rate_init) + + stubs.Set(nova.api.openstack.compute.limits.RateLimitingMiddleware, + '__call__', fake_wsgi) + + +def stub_out_instance_quota(stubs, allowed, quota, resource='instances'): + def fake_reserve(context, **deltas): + requested = deltas.pop(resource, 0) + if requested > allowed: + quotas = dict(instances=1, cores=1, ram=1) + quotas[resource] = quota + usages = dict(instances=dict(in_use=0, reserved=0), + cores=dict(in_use=0, reserved=0), + ram=dict(in_use=0, reserved=0)) + usages[resource]['in_use'] = (quotas[resource] * 0.9 - + allowed) + usages[resource]['reserved'] = quotas[resource] * 0.1 + headroom = dict( + (res, value - (usages[res]['in_use'] + usages[res]['reserved'])) + for res, value in quotas.iteritems() + ) + raise exc.OverQuota(overs=[resource], quotas=quotas, + usages=usages, headroom=headroom) + stubs.Set(QUOTAS, 'reserve', fake_reserve) + + +def stub_out_networking(stubs): + def get_my_ip(): + return '127.0.0.1' + stubs.Set(nova.netconf, '_get_my_ip', get_my_ip) + + +def stub_out_compute_api_snapshot(stubs): + + def snapshot(self, context, instance, name, extra_properties=None): + # emulate glance rejecting image names which are too long + if len(name) > 256: + raise exc.Invalid + return dict(id='123', status='ACTIVE', name=name, + properties=extra_properties) + + stubs.Set(compute_api.API, 'snapshot', snapshot) + + +class stub_out_compute_api_backup(object): + + def __init__(self, stubs): + self.stubs = stubs + self.extra_props_last_call = None + stubs.Set(compute_api.API, 'backup', self.backup) + + def backup(self, context, instance, name, backup_type, rotation, + extra_properties=None): + self.extra_props_last_call = extra_properties + props = dict(backup_type=backup_type, + rotation=rotation) + props.update(extra_properties or {}) + return dict(id='123', status='ACTIVE', name=name, properties=props) + + +def stub_out_nw_api_get_instance_nw_info(stubs, num_networks=1, func=None): + fake_network.stub_out_nw_api_get_instance_nw_info(stubs) + + +def stub_out_nw_api_get_floating_ips_by_fixed_address(stubs, func=None): + def get_floating_ips_by_fixed_address(self, context, fixed_ip): + return ['1.2.3.4'] + + if func is None: + func = get_floating_ips_by_fixed_address + stubs.Set(network_api.API, 'get_floating_ips_by_fixed_address', func) + + +def stub_out_nw_api(stubs, cls=None, private=None, publics=None): + if not private: + private = '192.168.0.3' + if not publics: + publics = ['1.2.3.4'] + + class Fake: + def get_instance_nw_info(*args, **kwargs): + pass + + def get_floating_ips_by_fixed_address(*args, **kwargs): + return publics + + def validate_networks(self, context, networks, max_count): + return max_count + + def create_pci_requests_for_sriov_ports(self, context, + system_metadata, + requested_networks): + pass + + if cls is None: + cls = Fake + stubs.Set(network_api, 'API', cls) + fake_network.stub_out_nw_api_get_instance_nw_info(stubs) + + +class FakeToken(object): + id_count = 0 + + def __getitem__(self, key): + return getattr(self, key) + + def __init__(self, **kwargs): + FakeToken.id_count += 1 + self.id = FakeToken.id_count + for k, v in kwargs.iteritems(): + setattr(self, k, v) + + +class FakeRequestContext(context.RequestContext): + def __init__(self, *args, **kwargs): + kwargs['auth_token'] = kwargs.get('auth_token', 'fake_auth_token') + return super(FakeRequestContext, self).__init__(*args, **kwargs) + + +class HTTPRequest(os_wsgi.Request): + + @staticmethod + def blank(*args, **kwargs): + kwargs['base_url'] = 'http://localhost/v2' + use_admin_context = kwargs.pop('use_admin_context', False) + out = os_wsgi.Request.blank(*args, **kwargs) + out.environ['nova.context'] = FakeRequestContext('fake_user', 'fake', + is_admin=use_admin_context) + return out + + +class HTTPRequestV3(os_wsgi.Request): + + @staticmethod + def blank(*args, **kwargs): + kwargs['base_url'] = 'http://localhost/v3' + use_admin_context = kwargs.pop('use_admin_context', False) + out = os_wsgi.Request.blank(*args, **kwargs) + out.environ['nova.context'] = FakeRequestContext('fake_user', 'fake', + is_admin=use_admin_context) + return out + + +class TestRouter(wsgi.Router): + def __init__(self, controller, mapper=None): + if not mapper: + mapper = routes.Mapper() + mapper.resource("test", "tests", + controller=os_wsgi.Resource(controller)) + super(TestRouter, self).__init__(mapper) + + +class FakeAuthDatabase(object): + data = {} + + @staticmethod + def auth_token_get(context, token_hash): + return FakeAuthDatabase.data.get(token_hash, None) + + @staticmethod + def auth_token_create(context, token): + fake_token = FakeToken(created_at=timeutils.utcnow(), **token) + FakeAuthDatabase.data[fake_token.token_hash] = fake_token + FakeAuthDatabase.data['id_%i' % fake_token.id] = fake_token + return fake_token + + @staticmethod + def auth_token_destroy(context, token_id): + token = FakeAuthDatabase.data.get('id_%i' % token_id) + if token and token.token_hash in FakeAuthDatabase.data: + del FakeAuthDatabase.data[token.token_hash] + del FakeAuthDatabase.data['id_%i' % token_id] + + +class FakeRateLimiter(object): + def __init__(self, application): + self.application = application + + @webob.dec.wsgify + def __call__(self, req): + return self.application + + +def create_info_cache(nw_cache): + if nw_cache is None: + pub0 = ('192.168.1.100',) + pub1 = ('2001:db8:0:1::1',) + + def _ip(ip): + return {'address': ip, 'type': 'fixed'} + + nw_cache = [ + {'address': 'aa:aa:aa:aa:aa:aa', + 'id': 1, + 'network': {'bridge': 'br0', + 'id': 1, + 'label': 'test1', + 'subnets': [{'cidr': '192.168.1.0/24', + 'ips': [_ip(ip) for ip in pub0]}, + {'cidr': 'b33f::/64', + 'ips': [_ip(ip) for ip in pub1]}]}}] + + if not isinstance(nw_cache, six.string_types): + nw_cache = jsonutils.dumps(nw_cache) + + return { + "info_cache": { + "network_info": nw_cache, + "deleted": False, + "created_at": None, + "deleted_at": None, + "updated_at": None, + } + } + + +def get_fake_uuid(token=0): + if token not in FAKE_UUIDS: + FAKE_UUIDS[token] = str(uuid.uuid4()) + return FAKE_UUIDS[token] + + +def fake_instance_get(**kwargs): + def _return_server(context, uuid, columns_to_join=None, use_slave=False): + return stub_instance(1, **kwargs) + return _return_server + + +def fake_actions_to_locked_server(self, context, instance, *args, **kwargs): + raise exc.InstanceIsLocked(instance_uuid=instance['uuid']) + + +def fake_instance_get_all_by_filters(num_servers=5, **kwargs): + def _return_servers(context, *args, **kwargs): + servers_list = [] + marker = None + limit = None + found_marker = False + if "marker" in kwargs: + marker = kwargs["marker"] + if "limit" in kwargs: + limit = kwargs["limit"] + + if 'columns_to_join' in kwargs: + kwargs.pop('columns_to_join') + + if 'use_slave' in kwargs: + kwargs.pop('use_slave') + + for i in xrange(num_servers): + uuid = get_fake_uuid(i) + server = stub_instance(id=i + 1, uuid=uuid, + **kwargs) + servers_list.append(server) + if marker is not None and uuid == marker: + found_marker = True + servers_list = [] + if marker is not None and not found_marker: + raise exc.MarkerNotFound(marker=marker) + if limit is not None: + servers_list = servers_list[:limit] + return servers_list + return _return_servers + + +def stub_instance(id, user_id=None, project_id=None, host=None, + node=None, vm_state=None, task_state=None, + reservation_id="", uuid=FAKE_UUID, image_ref="10", + flavor_id="1", name=None, key_name='', + access_ipv4=None, access_ipv6=None, progress=0, + auto_disk_config=False, display_name=None, + include_fake_metadata=True, config_drive=None, + power_state=None, nw_cache=None, metadata=None, + security_groups=None, root_device_name=None, + limit=None, marker=None, + launched_at=timeutils.utcnow(), + terminated_at=timeutils.utcnow(), + availability_zone='', locked_by=None, cleaned=False, + memory_mb=0, vcpus=0, root_gb=0, ephemeral_gb=0): + if user_id is None: + user_id = 'fake_user' + if project_id is None: + project_id = 'fake_project' + + if metadata: + metadata = [{'key': k, 'value': v} for k, v in metadata.items()] + elif include_fake_metadata: + metadata = [models.InstanceMetadata(key='seq', value=str(id))] + else: + metadata = [] + + inst_type = flavors.get_flavor_by_flavor_id(int(flavor_id)) + sys_meta = flavors.save_flavor_info({}, inst_type) + + if host is not None: + host = str(host) + + if key_name: + key_data = 'FAKE' + else: + key_data = '' + + if security_groups is None: + security_groups = [{"id": 1, "name": "test", "description": "Foo:", + "project_id": "project", "user_id": "user", + "created_at": None, "updated_at": None, + "deleted_at": None, "deleted": False}] + + # ReservationID isn't sent back, hack it in there. + server_name = name or "server%s" % id + if reservation_id != "": + server_name = "reservation_%s" % (reservation_id, ) + + info_cache = create_info_cache(nw_cache) + + instance = { + "id": int(id), + "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), + "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), + "deleted_at": datetime.datetime(2010, 12, 12, 10, 0, 0), + "deleted": None, + "user_id": user_id, + "project_id": project_id, + "image_ref": image_ref, + "kernel_id": "", + "ramdisk_id": "", + "launch_index": 0, + "key_name": key_name, + "key_data": key_data, + "config_drive": config_drive, + "vm_state": vm_state or vm_states.BUILDING, + "task_state": task_state, + "power_state": power_state, + "memory_mb": memory_mb, + "vcpus": vcpus, + "root_gb": root_gb, + "ephemeral_gb": ephemeral_gb, + "ephemeral_key_uuid": None, + "hostname": display_name or server_name, + "host": host, + "node": node, + "instance_type_id": 1, + "instance_type": inst_type, + "user_data": "", + "reservation_id": reservation_id, + "mac_address": "", + "scheduled_at": timeutils.utcnow(), + "launched_at": launched_at, + "terminated_at": terminated_at, + "availability_zone": availability_zone, + "display_name": display_name or server_name, + "display_description": "", + "locked": locked_by is not None, + "locked_by": locked_by, + "metadata": metadata, + "access_ip_v4": access_ipv4, + "access_ip_v6": access_ipv6, + "uuid": uuid, + "progress": progress, + "auto_disk_config": auto_disk_config, + "name": "instance-%s" % id, + "shutdown_terminate": True, + "disable_terminate": False, + "security_groups": security_groups, + "root_device_name": root_device_name, + "system_metadata": utils.dict_to_metadata(sys_meta), + "pci_devices": [], + "vm_mode": "", + "default_swap_device": "", + "default_ephemeral_device": "", + "launched_on": "", + "cell_name": "", + "architecture": "", + "os_type": "", + "cleaned": cleaned} + + instance.update(info_cache) + instance['info_cache']['instance_uuid'] = instance['uuid'] + + return instance + + +def stub_volume(id, **kwargs): + volume = { + 'id': id, + 'user_id': 'fakeuser', + 'project_id': 'fakeproject', + 'host': 'fakehost', + 'size': 1, + 'availability_zone': 'fakeaz', + 'instance_uuid': 'fakeuuid', + 'mountpoint': '/', + 'status': 'fakestatus', + 'attach_status': 'attached', + 'name': 'vol name', + 'display_name': 'displayname', + 'display_description': 'displaydesc', + 'created_at': datetime.datetime(1999, 1, 1, 1, 1, 1), + 'snapshot_id': None, + 'volume_type_id': 'fakevoltype', + 'volume_metadata': [], + 'volume_type': {'name': 'vol_type_name'}} + + volume.update(kwargs) + return volume + + +def stub_volume_create(self, context, size, name, description, snapshot, + **param): + vol = stub_volume('1') + vol['size'] = size + vol['display_name'] = name + vol['display_description'] = description + try: + vol['snapshot_id'] = snapshot['id'] + except (KeyError, TypeError): + vol['snapshot_id'] = None + vol['availability_zone'] = param.get('availability_zone', 'fakeaz') + return vol + + +def stub_volume_update(self, context, *args, **param): + pass + + +def stub_volume_delete(self, context, *args, **param): + pass + + +def stub_volume_get(self, context, volume_id): + return stub_volume(volume_id) + + +def stub_volume_notfound(self, context, volume_id): + raise exc.VolumeNotFound(volume_id=volume_id) + + +def stub_volume_get_all(context, search_opts=None): + return [stub_volume(100, project_id='fake'), + stub_volume(101, project_id='superfake'), + stub_volume(102, project_id='superduperfake')] + + +def stub_volume_check_attach(self, context, *args, **param): + pass + + +def stub_snapshot(id, **kwargs): + snapshot = { + 'id': id, + 'volume_id': 12, + 'status': 'available', + 'volume_size': 100, + 'created_at': timeutils.utcnow(), + 'display_name': 'Default name', + 'display_description': 'Default description', + 'project_id': 'fake' + } + + snapshot.update(kwargs) + return snapshot + + +def stub_snapshot_create(self, context, volume_id, name, description): + return stub_snapshot(100, volume_id=volume_id, display_name=name, + display_description=description) + + +def stub_compute_volume_snapshot_create(self, context, volume_id, create_info): + return {'snapshot': {'id': 100, 'volumeId': volume_id}} + + +def stub_snapshot_delete(self, context, snapshot_id): + if snapshot_id == '-1': + raise exc.NotFound + + +def stub_compute_volume_snapshot_delete(self, context, volume_id, snapshot_id, + delete_info): + pass + + +def stub_snapshot_get(self, context, snapshot_id): + if snapshot_id == '-1': + raise exc.NotFound + return stub_snapshot(snapshot_id) + + +def stub_snapshot_get_all(self, context): + return [stub_snapshot(100, project_id='fake'), + stub_snapshot(101, project_id='superfake'), + stub_snapshot(102, project_id='superduperfake')] + + +def stub_bdm_get_all_by_instance(context, instance_uuid, use_slave=False): + return [fake_block_device.FakeDbBlockDeviceDict( + {'id': 1, 'source_type': 'volume', 'destination_type': 'volume', + 'volume_id': 'volume_id1', 'instance_uuid': instance_uuid}), + fake_block_device.FakeDbBlockDeviceDict( + {'id': 2, 'source_type': 'volume', 'destination_type': 'volume', + 'volume_id': 'volume_id2', 'instance_uuid': instance_uuid})] + + +def fake_get_available_languages(): + existing_translations = ['en_GB', 'en_AU', 'de', 'zh_CN', 'en_US'] + return existing_translations + + +def fake_not_implemented(*args, **kwargs): + raise NotImplementedError() diff --git a/nova/tests/unit/api/openstack/test_common.py b/nova/tests/unit/api/openstack/test_common.py new file mode 100644 index 0000000000..a61f70cf95 --- /dev/null +++ b/nova/tests/unit/api/openstack/test_common.py @@ -0,0 +1,764 @@ +# Copyright 2010 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. + +""" +Test suites for 'common' code used throughout the OpenStack HTTP API. +""" + +import xml.dom.minidom as minidom + +from lxml import etree +import mock +import six +from testtools import matchers +import webob +import webob.exc +import webob.multidict + +from nova.api.openstack import common +from nova.api.openstack import xmlutil +from nova.compute import task_states +from nova.compute import vm_states +from nova import exception +from nova import test +from nova.tests.unit import utils + + +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" + + +class LimiterTest(test.TestCase): + """Unit tests for the `nova.api.openstack.common.limited` method which + takes in a list of items and, depending on the 'offset' and 'limit' GET + params, returns a subset or complete set of the given items. + """ + + def setUp(self): + """Run before each test.""" + super(LimiterTest, self).setUp() + self.tiny = range(1) + self.small = range(10) + self.medium = range(1000) + self.large = range(10000) + + def test_limiter_offset_zero(self): + # Test offset key works with 0. + req = webob.Request.blank('/?offset=0') + self.assertEqual(common.limited(self.tiny, req), self.tiny) + self.assertEqual(common.limited(self.small, req), self.small) + self.assertEqual(common.limited(self.medium, req), self.medium) + self.assertEqual(common.limited(self.large, req), self.large[:1000]) + + def test_limiter_offset_medium(self): + # Test offset key works with a medium sized number. + req = webob.Request.blank('/?offset=10') + self.assertEqual(common.limited(self.tiny, req), []) + self.assertEqual(common.limited(self.small, req), self.small[10:]) + self.assertEqual(common.limited(self.medium, req), self.medium[10:]) + self.assertEqual(common.limited(self.large, req), self.large[10:1010]) + + def test_limiter_offset_over_max(self): + # Test offset key works with a number over 1000 (max_limit). + req = webob.Request.blank('/?offset=1001') + self.assertEqual(common.limited(self.tiny, req), []) + self.assertEqual(common.limited(self.small, req), []) + self.assertEqual(common.limited(self.medium, req), []) + self.assertEqual( + common.limited(self.large, req), self.large[1001:2001]) + + def test_limiter_offset_blank(self): + # Test offset key works with a blank offset. + req = webob.Request.blank('/?offset=') + self.assertRaises( + webob.exc.HTTPBadRequest, common.limited, self.tiny, req) + + def test_limiter_offset_bad(self): + # Test offset key works with a BAD offset. + req = webob.Request.blank(u'/?offset=\u0020aa') + self.assertRaises( + webob.exc.HTTPBadRequest, common.limited, self.tiny, req) + + def test_limiter_nothing(self): + # Test request with no offset or limit. + req = webob.Request.blank('/') + self.assertEqual(common.limited(self.tiny, req), self.tiny) + self.assertEqual(common.limited(self.small, req), self.small) + self.assertEqual(common.limited(self.medium, req), self.medium) + self.assertEqual(common.limited(self.large, req), self.large[:1000]) + + def test_limiter_limit_zero(self): + # Test limit of zero. + req = webob.Request.blank('/?limit=0') + self.assertEqual(common.limited(self.tiny, req), self.tiny) + self.assertEqual(common.limited(self.small, req), self.small) + self.assertEqual(common.limited(self.medium, req), self.medium) + self.assertEqual(common.limited(self.large, req), self.large[:1000]) + + def test_limiter_limit_medium(self): + # Test limit of 10. + req = webob.Request.blank('/?limit=10') + self.assertEqual(common.limited(self.tiny, req), self.tiny) + self.assertEqual(common.limited(self.small, req), self.small) + self.assertEqual(common.limited(self.medium, req), self.medium[:10]) + self.assertEqual(common.limited(self.large, req), self.large[:10]) + + def test_limiter_limit_over_max(self): + # Test limit of 3000. + req = webob.Request.blank('/?limit=3000') + self.assertEqual(common.limited(self.tiny, req), self.tiny) + self.assertEqual(common.limited(self.small, req), self.small) + self.assertEqual(common.limited(self.medium, req), self.medium) + self.assertEqual(common.limited(self.large, req), self.large[:1000]) + + def test_limiter_limit_and_offset(self): + # Test request with both limit and offset. + items = range(2000) + req = webob.Request.blank('/?offset=1&limit=3') + self.assertEqual(common.limited(items, req), items[1:4]) + req = webob.Request.blank('/?offset=3&limit=0') + self.assertEqual(common.limited(items, req), items[3:1003]) + req = webob.Request.blank('/?offset=3&limit=1500') + self.assertEqual(common.limited(items, req), items[3:1003]) + req = webob.Request.blank('/?offset=3000&limit=10') + self.assertEqual(common.limited(items, req), []) + + def test_limiter_custom_max_limit(self): + # Test a max_limit other than 1000. + items = range(2000) + req = webob.Request.blank('/?offset=1&limit=3') + self.assertEqual( + common.limited(items, req, max_limit=2000), items[1:4]) + req = webob.Request.blank('/?offset=3&limit=0') + self.assertEqual( + common.limited(items, req, max_limit=2000), items[3:]) + req = webob.Request.blank('/?offset=3&limit=2500') + self.assertEqual( + common.limited(items, req, max_limit=2000), items[3:]) + req = webob.Request.blank('/?offset=3000&limit=10') + self.assertEqual(common.limited(items, req, max_limit=2000), []) + + def test_limiter_negative_limit(self): + # Test a negative limit. + req = webob.Request.blank('/?limit=-3000') + self.assertRaises( + webob.exc.HTTPBadRequest, common.limited, self.tiny, req) + + def test_limiter_negative_offset(self): + # Test a negative offset. + req = webob.Request.blank('/?offset=-30') + self.assertRaises( + webob.exc.HTTPBadRequest, common.limited, self.tiny, req) + + +class SortParamUtilsTest(test.TestCase): + + def test_get_sort_params_defaults(self): + '''Verifies the default sort key and direction.''' + sort_keys, sort_dirs = common.get_sort_params({}) + self.assertEqual(['created_at'], sort_keys) + self.assertEqual(['desc'], sort_dirs) + + def test_get_sort_params_override_defaults(self): + '''Verifies that the defaults can be overriden.''' + sort_keys, sort_dirs = common.get_sort_params({}, default_key='key1', + default_dir='dir1') + self.assertEqual(['key1'], sort_keys) + self.assertEqual(['dir1'], sort_dirs) + + sort_keys, sort_dirs = common.get_sort_params({}, default_key=None, + default_dir=None) + self.assertEqual([], sort_keys) + self.assertEqual([], sort_dirs) + + def test_get_sort_params_single_value(self): + '''Verifies a single sort key and direction.''' + params = webob.multidict.MultiDict() + params.add('sort_key', 'key1') + params.add('sort_dir', 'dir1') + sort_keys, sort_dirs = common.get_sort_params(params) + self.assertEqual(['key1'], sort_keys) + self.assertEqual(['dir1'], sort_dirs) + + def test_get_sort_params_single_with_default(self): + '''Verifies a single sort value with a default.''' + params = webob.multidict.MultiDict() + params.add('sort_key', 'key1') + sort_keys, sort_dirs = common.get_sort_params(params) + self.assertEqual(['key1'], sort_keys) + # sort_key was supplied, sort_dir should be defaulted + self.assertEqual(['desc'], sort_dirs) + + params = webob.multidict.MultiDict() + params.add('sort_dir', 'dir1') + sort_keys, sort_dirs = common.get_sort_params(params) + self.assertEqual(['created_at'], sort_keys) + # sort_dir was supplied, sort_key should be defaulted + self.assertEqual(['dir1'], sort_dirs) + + def test_get_sort_params_multiple_values(self): + '''Verifies multiple sort parameter values.''' + params = webob.multidict.MultiDict() + params.add('sort_key', 'key1') + params.add('sort_key', 'key2') + params.add('sort_key', 'key3') + params.add('sort_dir', 'dir1') + params.add('sort_dir', 'dir2') + params.add('sort_dir', 'dir3') + sort_keys, sort_dirs = common.get_sort_params(params) + self.assertEqual(['key1', 'key2', 'key3'], sort_keys) + self.assertEqual(['dir1', 'dir2', 'dir3'], sort_dirs) + # Also ensure that the input parameters are not modified + sort_key_vals = [] + sort_dir_vals = [] + while 'sort_key' in params: + sort_key_vals.append(params.pop('sort_key')) + while 'sort_dir' in params: + sort_dir_vals.append(params.pop('sort_dir')) + self.assertEqual(['key1', 'key2', 'key3'], sort_key_vals) + self.assertEqual(['dir1', 'dir2', 'dir3'], sort_dir_vals) + self.assertEqual(0, len(params)) + + +class PaginationParamsTest(test.TestCase): + """Unit tests for the `nova.api.openstack.common.get_pagination_params` + method which takes in a request object and returns 'marker' and 'limit' + GET params. + """ + + def test_no_params(self): + # Test no params. + req = webob.Request.blank('/') + self.assertEqual(common.get_pagination_params(req), {}) + + def test_valid_marker(self): + # Test valid marker param. + req = webob.Request.blank( + '/?marker=263abb28-1de6-412f-b00b-f0ee0c4333c2') + self.assertEqual(common.get_pagination_params(req), + {'marker': '263abb28-1de6-412f-b00b-f0ee0c4333c2'}) + + def test_valid_limit(self): + # Test valid limit param. + req = webob.Request.blank('/?limit=10') + self.assertEqual(common.get_pagination_params(req), {'limit': 10}) + + def test_invalid_limit(self): + # Test invalid limit param. + req = webob.Request.blank('/?limit=-2') + self.assertRaises( + webob.exc.HTTPBadRequest, common.get_pagination_params, req) + + def test_valid_limit_and_marker(self): + # Test valid limit and marker parameters. + marker = '263abb28-1de6-412f-b00b-f0ee0c4333c2' + req = webob.Request.blank('/?limit=20&marker=%s' % marker) + self.assertEqual(common.get_pagination_params(req), + {'marker': marker, 'limit': 20}) + + def test_valid_page_size(self): + # Test valid page_size param. + req = webob.Request.blank('/?page_size=10') + self.assertEqual(common.get_pagination_params(req), + {'page_size': 10}) + + def test_invalid_page_size(self): + # Test invalid page_size param. + req = webob.Request.blank('/?page_size=-2') + self.assertRaises( + webob.exc.HTTPBadRequest, common.get_pagination_params, req) + + def test_valid_limit_and_page_size(self): + # Test valid limit and page_size parameters. + req = webob.Request.blank('/?limit=20&page_size=5') + self.assertEqual(common.get_pagination_params(req), + {'page_size': 5, 'limit': 20}) + + +class MiscFunctionsTest(test.TestCase): + + def test_remove_major_version_from_href(self): + fixture = 'http://www.testsite.com/v1/images' + expected = 'http://www.testsite.com/images' + actual = common.remove_version_from_href(fixture) + self.assertEqual(actual, expected) + + def test_remove_version_from_href(self): + fixture = 'http://www.testsite.com/v1.1/images' + expected = 'http://www.testsite.com/images' + actual = common.remove_version_from_href(fixture) + self.assertEqual(actual, expected) + + def test_remove_version_from_href_2(self): + fixture = 'http://www.testsite.com/v1.1/' + expected = 'http://www.testsite.com/' + actual = common.remove_version_from_href(fixture) + self.assertEqual(actual, expected) + + def test_remove_version_from_href_3(self): + fixture = 'http://www.testsite.com/v10.10' + expected = 'http://www.testsite.com' + actual = common.remove_version_from_href(fixture) + self.assertEqual(actual, expected) + + def test_remove_version_from_href_4(self): + fixture = 'http://www.testsite.com/v1.1/images/v10.5' + expected = 'http://www.testsite.com/images/v10.5' + actual = common.remove_version_from_href(fixture) + self.assertEqual(actual, expected) + + def test_remove_version_from_href_bad_request(self): + fixture = 'http://www.testsite.com/1.1/images' + self.assertRaises(ValueError, + common.remove_version_from_href, + fixture) + + def test_remove_version_from_href_bad_request_2(self): + fixture = 'http://www.testsite.com/v/images' + self.assertRaises(ValueError, + common.remove_version_from_href, + fixture) + + def test_remove_version_from_href_bad_request_3(self): + fixture = 'http://www.testsite.com/v1.1images' + self.assertRaises(ValueError, + common.remove_version_from_href, + fixture) + + def test_get_id_from_href_with_int_url(self): + fixture = 'http://www.testsite.com/dir/45' + actual = common.get_id_from_href(fixture) + expected = '45' + self.assertEqual(actual, expected) + + def test_get_id_from_href_with_int(self): + fixture = '45' + actual = common.get_id_from_href(fixture) + expected = '45' + self.assertEqual(actual, expected) + + def test_get_id_from_href_with_int_url_query(self): + fixture = 'http://www.testsite.com/dir/45?asdf=jkl' + actual = common.get_id_from_href(fixture) + expected = '45' + self.assertEqual(actual, expected) + + def test_get_id_from_href_with_uuid_url(self): + fixture = 'http://www.testsite.com/dir/abc123' + actual = common.get_id_from_href(fixture) + expected = "abc123" + self.assertEqual(actual, expected) + + def test_get_id_from_href_with_uuid_url_query(self): + fixture = 'http://www.testsite.com/dir/abc123?asdf=jkl' + actual = common.get_id_from_href(fixture) + expected = "abc123" + self.assertEqual(actual, expected) + + def test_get_id_from_href_with_uuid(self): + fixture = 'abc123' + actual = common.get_id_from_href(fixture) + expected = 'abc123' + self.assertEqual(actual, expected) + + def test_raise_http_conflict_for_instance_invalid_state(self): + exc = exception.InstanceInvalidState(attr='fake_attr', + state='fake_state', method='fake_method', + instance_uuid='fake') + try: + common.raise_http_conflict_for_instance_invalid_state(exc, + 'meow', 'fake_server_id') + except webob.exc.HTTPConflict as e: + self.assertEqual(six.text_type(e), + "Cannot 'meow' instance fake_server_id while it is in " + "fake_attr fake_state") + else: + self.fail("webob.exc.HTTPConflict was not raised") + + def test_check_img_metadata_properties_quota_valid_metadata(self): + ctxt = utils.get_test_admin_context() + metadata1 = {"key": "value"} + actual = common.check_img_metadata_properties_quota(ctxt, metadata1) + self.assertIsNone(actual) + + metadata2 = {"key": "v" * 260} + actual = common.check_img_metadata_properties_quota(ctxt, metadata2) + self.assertIsNone(actual) + + metadata3 = {"key": ""} + actual = common.check_img_metadata_properties_quota(ctxt, metadata3) + self.assertIsNone(actual) + + def test_check_img_metadata_properties_quota_inv_metadata(self): + ctxt = utils.get_test_admin_context() + metadata1 = {"a" * 260: "value"} + self.assertRaises(webob.exc.HTTPBadRequest, + common.check_img_metadata_properties_quota, ctxt, metadata1) + + metadata2 = {"": "value"} + self.assertRaises(webob.exc.HTTPBadRequest, + common.check_img_metadata_properties_quota, ctxt, metadata2) + + metadata3 = "invalid metadata" + self.assertRaises(webob.exc.HTTPBadRequest, + common.check_img_metadata_properties_quota, ctxt, metadata3) + + metadata4 = None + self.assertIsNone(common.check_img_metadata_properties_quota(ctxt, + metadata4)) + metadata5 = {} + self.assertIsNone(common.check_img_metadata_properties_quota(ctxt, + metadata5)) + + def test_status_from_state(self): + for vm_state in (vm_states.ACTIVE, vm_states.STOPPED): + for task_state in (task_states.RESIZE_PREP, + task_states.RESIZE_MIGRATING, + task_states.RESIZE_MIGRATED, + task_states.RESIZE_FINISH): + actual = common.status_from_state(vm_state, task_state) + expected = 'RESIZE' + self.assertEqual(expected, actual) + + def test_status_rebuild_from_state(self): + for vm_state in (vm_states.ACTIVE, vm_states.STOPPED, + vm_states.ERROR): + for task_state in (task_states.REBUILDING, + task_states.REBUILD_BLOCK_DEVICE_MAPPING, + task_states.REBUILD_SPAWNING): + actual = common.status_from_state(vm_state, task_state) + expected = 'REBUILD' + self.assertEqual(expected, actual) + + def test_task_and_vm_state_from_status(self): + fixture1 = ['reboot'] + actual = common.task_and_vm_state_from_status(fixture1) + expected = [vm_states.ACTIVE], [task_states.REBOOT_PENDING, + task_states.REBOOT_STARTED, + task_states.REBOOTING] + self.assertEqual(expected, actual) + + fixture2 = ['resize'] + actual = common.task_and_vm_state_from_status(fixture2) + expected = ([vm_states.ACTIVE, vm_states.STOPPED], + [task_states.RESIZE_FINISH, + task_states.RESIZE_MIGRATED, + task_states.RESIZE_MIGRATING, + task_states.RESIZE_PREP]) + self.assertEqual(expected, actual) + + fixture3 = ['resize', 'reboot'] + actual = common.task_and_vm_state_from_status(fixture3) + expected = ([vm_states.ACTIVE, vm_states.STOPPED], + [task_states.REBOOT_PENDING, + task_states.REBOOT_STARTED, + task_states.REBOOTING, + task_states.RESIZE_FINISH, + task_states.RESIZE_MIGRATED, + task_states.RESIZE_MIGRATING, + task_states.RESIZE_PREP]) + self.assertEqual(expected, actual) + + +class TestCollectionLinks(test.NoDBTestCase): + """Tests the _get_collection_links method.""" + + @mock.patch('nova.api.openstack.common.ViewBuilder._get_next_link') + def test_items_less_than_limit(self, href_link_mock): + items = [ + {"uuid": "123"} + ] + req = mock.MagicMock() + params = mock.PropertyMock(return_value=dict(limit=10)) + type(req).params = params + + builder = common.ViewBuilder() + results = builder._get_collection_links(req, items, "ignored", "uuid") + + self.assertFalse(href_link_mock.called) + self.assertThat(results, matchers.HasLength(0)) + + @mock.patch('nova.api.openstack.common.ViewBuilder._get_next_link') + def test_items_equals_given_limit(self, href_link_mock): + items = [ + {"uuid": "123"} + ] + req = mock.MagicMock() + params = mock.PropertyMock(return_value=dict(limit=1)) + type(req).params = params + + builder = common.ViewBuilder() + results = builder._get_collection_links(req, items, + mock.sentinel.coll_key, + "uuid") + + href_link_mock.assert_called_once_with(req, "123", + mock.sentinel.coll_key) + self.assertThat(results, matchers.HasLength(1)) + + @mock.patch('nova.api.openstack.common.ViewBuilder._get_next_link') + def test_items_equals_default_limit(self, href_link_mock): + items = [ + {"uuid": "123"} + ] + req = mock.MagicMock() + params = mock.PropertyMock(return_value=dict()) + type(req).params = params + self.flags(osapi_max_limit=1) + + builder = common.ViewBuilder() + results = builder._get_collection_links(req, items, + mock.sentinel.coll_key, + "uuid") + + href_link_mock.assert_called_once_with(req, "123", + mock.sentinel.coll_key) + self.assertThat(results, matchers.HasLength(1)) + + @mock.patch('nova.api.openstack.common.ViewBuilder._get_next_link') + def test_items_equals_default_limit_with_given(self, href_link_mock): + items = [ + {"uuid": "123"} + ] + req = mock.MagicMock() + # Given limit is greater than default max, only return default max + params = mock.PropertyMock(return_value=dict(limit=2)) + type(req).params = params + self.flags(osapi_max_limit=1) + + builder = common.ViewBuilder() + results = builder._get_collection_links(req, items, + mock.sentinel.coll_key, + "uuid") + + href_link_mock.assert_called_once_with(req, "123", + mock.sentinel.coll_key) + self.assertThat(results, matchers.HasLength(1)) + + +class MetadataXMLDeserializationTest(test.TestCase): + + deserializer = common.MetadataXMLDeserializer() + + def test_create(self): + request_body = """ + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> + <meta key='123'>asdf</meta> + <meta key='567'>jkl;</meta> + </metadata>""" + output = self.deserializer.deserialize(request_body, 'create') + expected = {"body": {"metadata": {"123": "asdf", "567": "jkl;"}}} + self.assertEqual(output, expected) + + def test_create_empty(self): + request_body = """ + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"/>""" + output = self.deserializer.deserialize(request_body, 'create') + expected = {"body": {"metadata": {}}} + self.assertEqual(output, expected) + + def test_update_all(self): + request_body = """ + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> + <meta key='123'>asdf</meta> + <meta key='567'>jkl;</meta> + </metadata>""" + output = self.deserializer.deserialize(request_body, 'update_all') + expected = {"body": {"metadata": {"123": "asdf", "567": "jkl;"}}} + self.assertEqual(output, expected) + + def test_update(self): + request_body = """ + <meta xmlns="http://docs.openstack.org/compute/api/v1.1" + key='123'>asdf</meta>""" + output = self.deserializer.deserialize(request_body, 'update') + expected = {"body": {"meta": {"123": "asdf"}}} + self.assertEqual(output, expected) + + +class MetadataXMLSerializationTest(test.TestCase): + + def test_xml_declaration(self): + serializer = common.MetadataTemplate() + fixture = { + 'metadata': { + 'one': 'two', + 'three': 'four', + }, + } + + output = serializer.serialize(fixture) + has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>") + self.assertTrue(has_dec) + + def test_index(self): + serializer = common.MetadataTemplate() + fixture = { + 'metadata': { + 'one': 'two', + 'three': 'four', + }, + } + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'metadata') + metadata_dict = fixture['metadata'] + metadata_elems = root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 2) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = metadata_dict.items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + def test_index_null(self): + serializer = common.MetadataTemplate() + fixture = { + 'metadata': { + None: None, + }, + } + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'metadata') + metadata_dict = fixture['metadata'] + metadata_elems = root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = metadata_dict.items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + def test_index_unicode(self): + serializer = common.MetadataTemplate() + fixture = { + 'metadata': { + u'three': u'Jos\xe9', + }, + } + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'metadata') + metadata_dict = fixture['metadata'] + metadata_elems = root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 1) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = metadata_dict.items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(metadata_elem.text.strip(), meta_value) + + def test_show(self): + serializer = common.MetaItemTemplate() + fixture = { + 'meta': { + 'one': 'two', + }, + } + output = serializer.serialize(fixture) + root = etree.XML(output) + meta_dict = fixture['meta'] + (meta_key, meta_value) = meta_dict.items()[0] + self.assertEqual(str(root.get('key')), str(meta_key)) + self.assertEqual(root.text.strip(), meta_value) + + def test_update_all(self): + serializer = common.MetadataTemplate() + fixture = { + 'metadata': { + 'key6': 'value6', + 'key4': 'value4', + }, + } + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'metadata') + metadata_dict = fixture['metadata'] + metadata_elems = root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 2) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = metadata_dict.items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + def test_update_item(self): + serializer = common.MetaItemTemplate() + fixture = { + 'meta': { + 'one': 'two', + }, + } + output = serializer.serialize(fixture) + root = etree.XML(output) + meta_dict = fixture['meta'] + (meta_key, meta_value) = meta_dict.items()[0] + self.assertEqual(str(root.get('key')), str(meta_key)) + self.assertEqual(root.text.strip(), meta_value) + + def test_create(self): + serializer = common.MetadataTemplate() + fixture = { + 'metadata': { + 'key9': 'value9', + 'key2': 'value2', + 'key1': 'value1', + }, + } + output = serializer.serialize(fixture) + root = etree.XML(output) + xmlutil.validate_schema(root, 'metadata') + metadata_dict = fixture['metadata'] + metadata_elems = root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 3) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = metadata_dict.items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + actual = minidom.parseString(output.replace(" ", "")) + + expected = minidom.parseString(""" + <metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> + <meta key="key2">value2</meta> + <meta key="key9">value9</meta> + <meta key="key1">value1</meta> + </metadata> + """.replace(" ", "").replace("\n", "")) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_metadata_deserializer(self): + """Should throw a 400 error on corrupt xml.""" + deserializer = common.MetadataXMLDeserializer() + self.assertRaises( + exception.MalformedRequestBody, + deserializer.deserialize, + utils.killer_xml_body()) + + +class LinkPrefixTest(test.NoDBTestCase): + + def test_update_link_prefix(self): + vb = common.ViewBuilder() + result = vb._update_link_prefix("http://192.168.0.243:24/", + "http://127.0.0.1/compute") + self.assertEqual("http://127.0.0.1/compute", result) + + result = vb._update_link_prefix("http://foo.x.com/v1", + "http://new.prefix.com") + self.assertEqual("http://new.prefix.com/v1", result) + + result = vb._update_link_prefix( + "http://foo.x.com/v1", + "http://new.prefix.com:20455/new_extra_prefix") + self.assertEqual("http://new.prefix.com:20455/new_extra_prefix/v1", + result) diff --git a/nova/tests/unit/api/openstack/test_faults.py b/nova/tests/unit/api/openstack/test_faults.py new file mode 100644 index 0000000000..b52a7e5896 --- /dev/null +++ b/nova/tests/unit/api/openstack/test_faults.py @@ -0,0 +1,315 @@ +# Copyright 2013 IBM Corp. +# Copyright 2010 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. + +from xml.dom import minidom + +import mock +from oslo.serialization import jsonutils +import webob +import webob.dec +import webob.exc + +import nova.api.openstack +from nova.api.openstack import common +from nova.api.openstack import wsgi +from nova import exception +from nova import i18n +from nova.i18n import _ +from nova import test + + +class TestFaultWrapper(test.NoDBTestCase): + """Tests covering `nova.api.openstack:FaultWrapper` class.""" + + @mock.patch('oslo.i18n.translate') + @mock.patch('nova.i18n.get_available_languages') + def test_safe_exception_translated(self, mock_languages, mock_translate): + def fake_translate(value, locale): + return "I've been translated!" + + mock_translate.side_effect = fake_translate + + # Create an exception, passing a translatable message with a + # known value we can test for later. + safe_exception = exception.NotFound(_('Should be translated.')) + safe_exception.safe = True + safe_exception.code = 404 + + req = webob.Request.blank('/') + + def raiser(*args, **kwargs): + raise safe_exception + + wrapper = nova.api.openstack.FaultWrapper(raiser) + response = req.get_response(wrapper) + + # The text of the exception's message attribute (replaced + # above with a non-default value) should be passed to + # translate(). + mock_translate.assert_any_call(u'Should be translated.', None) + # The return value from translate() should appear in the response. + self.assertIn("I've been translated!", unicode(response.body)) + + +class TestFaults(test.NoDBTestCase): + """Tests covering `nova.api.openstack.faults:Fault` class.""" + + def _prepare_xml(self, xml_string): + """Remove characters from string which hinder XML equality testing.""" + xml_string = xml_string.replace(" ", "") + xml_string = xml_string.replace("\n", "") + xml_string = xml_string.replace("\t", "") + return xml_string + + def test_400_fault_json(self): + # Test fault serialized to JSON via file-extension and/or header. + requests = [ + webob.Request.blank('/.json'), + webob.Request.blank('/', headers={"Accept": "application/json"}), + ] + + for request in requests: + fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='scram')) + response = request.get_response(fault) + + expected = { + "badRequest": { + "message": "scram", + "code": 400, + }, + } + actual = jsonutils.loads(response.body) + + self.assertEqual(response.content_type, "application/json") + self.assertEqual(expected, actual) + + def test_413_fault_json(self): + # Test fault serialized to JSON via file-extension and/or header. + requests = [ + webob.Request.blank('/.json'), + webob.Request.blank('/', headers={"Accept": "application/json"}), + ] + + for request in requests: + exc = webob.exc.HTTPRequestEntityTooLarge + # NOTE(aloga): we intentionally pass an integer for the + # 'Retry-After' header. It should be then converted to a str + fault = wsgi.Fault(exc(explanation='sorry', + headers={'Retry-After': 4})) + response = request.get_response(fault) + + expected = { + "overLimit": { + "message": "sorry", + "code": 413, + "retryAfter": "4", + }, + } + actual = jsonutils.loads(response.body) + + self.assertEqual(response.content_type, "application/json") + self.assertEqual(expected, actual) + + def test_429_fault_json(self): + # Test fault serialized to JSON via file-extension and/or header. + requests = [ + webob.Request.blank('/.json'), + webob.Request.blank('/', headers={"Accept": "application/json"}), + ] + + for request in requests: + exc = webob.exc.HTTPTooManyRequests + # NOTE(aloga): we intentionally pass an integer for the + # 'Retry-After' header. It should be then converted to a str + fault = wsgi.Fault(exc(explanation='sorry', + headers={'Retry-After': 4})) + response = request.get_response(fault) + + expected = { + "overLimit": { + "message": "sorry", + "code": 429, + "retryAfter": "4", + }, + } + actual = jsonutils.loads(response.body) + + self.assertEqual(response.content_type, "application/json") + self.assertEqual(expected, actual) + + def test_raise(self): + # Ensure the ability to raise :class:`Fault` in WSGI-ified methods. + @webob.dec.wsgify + def raiser(req): + raise wsgi.Fault(webob.exc.HTTPNotFound(explanation='whut?')) + + req = webob.Request.blank('/.xml') + resp = req.get_response(raiser) + self.assertEqual(resp.content_type, "application/xml") + self.assertEqual(resp.status_int, 404) + self.assertIn('whut?', resp.body) + + def test_raise_403(self): + # Ensure the ability to raise :class:`Fault` in WSGI-ified methods. + @webob.dec.wsgify + def raiser(req): + raise wsgi.Fault(webob.exc.HTTPForbidden(explanation='whut?')) + + req = webob.Request.blank('/.xml') + resp = req.get_response(raiser) + self.assertEqual(resp.content_type, "application/xml") + self.assertEqual(resp.status_int, 403) + self.assertNotIn('resizeNotAllowed', resp.body) + self.assertIn('forbidden', resp.body) + + def test_raise_localize_explanation(self): + msgid = "String with params: %s" + params = ('blah', ) + lazy_gettext = i18n._ + expl = lazy_gettext(msgid) % params + + @webob.dec.wsgify + def raiser(req): + raise wsgi.Fault(webob.exc.HTTPNotFound(explanation=expl)) + + req = webob.Request.blank('/.xml') + resp = req.get_response(raiser) + self.assertEqual(resp.content_type, "application/xml") + self.assertEqual(resp.status_int, 404) + self.assertIn((msgid % params), resp.body) + + def test_fault_has_status_int(self): + # Ensure the status_int is set correctly on faults. + fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='what?')) + self.assertEqual(fault.status_int, 400) + + def test_xml_serializer(self): + # Ensure that a v1.1 request responds with a v1.1 xmlns. + request = webob.Request.blank('/v1.1', + headers={"Accept": "application/xml"}) + + fault = wsgi.Fault(webob.exc.HTTPBadRequest(explanation='scram')) + response = request.get_response(fault) + + self.assertIn(common.XML_NS_V11, response.body) + self.assertEqual(response.content_type, "application/xml") + self.assertEqual(response.status_int, 400) + + +class FaultsXMLSerializationTestV11(test.NoDBTestCase): + """Tests covering `nova.api.openstack.faults:Fault` class.""" + + def _prepare_xml(self, xml_string): + xml_string = xml_string.replace(" ", "") + xml_string = xml_string.replace("\n", "") + xml_string = xml_string.replace("\t", "") + return xml_string + + def test_400_fault(self): + metadata = {'attributes': {"badRequest": 'code'}} + serializer = wsgi.XMLDictSerializer(metadata=metadata, + xmlns=common.XML_NS_V11) + + fixture = { + "badRequest": { + "message": "scram", + "code": 400, + }, + } + + output = serializer.serialize(fixture) + actual = minidom.parseString(self._prepare_xml(output)) + + expected = minidom.parseString(self._prepare_xml(""" + <badRequest code="400" xmlns="%s"> + <message>scram</message> + </badRequest> + """) % common.XML_NS_V11) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_413_fault(self): + metadata = {'attributes': {"overLimit": 'code'}} + serializer = wsgi.XMLDictSerializer(metadata=metadata, + xmlns=common.XML_NS_V11) + + fixture = { + "overLimit": { + "message": "sorry", + "code": 413, + "retryAfter": 4, + }, + } + + output = serializer.serialize(fixture) + actual = minidom.parseString(self._prepare_xml(output)) + + expected = minidom.parseString(self._prepare_xml(""" + <overLimit code="413" xmlns="%s"> + <message>sorry</message> + <retryAfter>4</retryAfter> + </overLimit> + """) % common.XML_NS_V11) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_429_fault(self): + metadata = {'attributes': {"overLimit": 'code'}} + serializer = wsgi.XMLDictSerializer(metadata=metadata, + xmlns=common.XML_NS_V11) + + fixture = { + "overLimit": { + "message": "sorry", + "code": 429, + "retryAfter": 4, + }, + } + + output = serializer.serialize(fixture) + actual = minidom.parseString(self._prepare_xml(output)) + + expected = minidom.parseString(self._prepare_xml(""" + <overLimit code="429" xmlns="%s"> + <message>sorry</message> + <retryAfter>4</retryAfter> + </overLimit> + """) % common.XML_NS_V11) + + self.assertEqual(expected.toxml(), actual.toxml()) + + def test_404_fault(self): + metadata = {'attributes': {"itemNotFound": 'code'}} + serializer = wsgi.XMLDictSerializer(metadata=metadata, + xmlns=common.XML_NS_V11) + + fixture = { + "itemNotFound": { + "message": "sorry", + "code": 404, + }, + } + + output = serializer.serialize(fixture) + actual = minidom.parseString(self._prepare_xml(output)) + + expected = minidom.parseString(self._prepare_xml(""" + <itemNotFound code="404" xmlns="%s"> + <message>sorry</message> + </itemNotFound> + """) % common.XML_NS_V11) + + self.assertEqual(expected.toxml(), actual.toxml()) diff --git a/nova/tests/unit/api/openstack/test_mapper.py b/nova/tests/unit/api/openstack/test_mapper.py new file mode 100644 index 0000000000..b872be546f --- /dev/null +++ b/nova/tests/unit/api/openstack/test_mapper.py @@ -0,0 +1,46 @@ +# Copyright 2013 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 webob + +from nova.api import openstack as openstack_api +from nova import test +from nova.tests.unit.api.openstack import fakes + + +class MapperTest(test.NoDBTestCase): + def test_resource_project_prefix(self): + class Controller(object): + def index(self, req): + return 'foo' + + app = fakes.TestRouter(Controller(), + openstack_api.ProjectMapper()) + req = webob.Request.blank('/1234/tests') + resp = req.get_response(app) + self.assertEqual(resp.body, 'foo') + self.assertEqual(resp.status_int, 200) + + def test_resource_no_project_prefix(self): + class Controller(object): + def index(self, req): + return 'foo' + + app = fakes.TestRouter(Controller(), + openstack_api.PlainMapper()) + req = webob.Request.blank('/tests') + resp = req.get_response(app) + self.assertEqual(resp.body, 'foo') + self.assertEqual(resp.status_int, 200) diff --git a/nova/tests/unit/api/openstack/test_wsgi.py b/nova/tests/unit/api/openstack/test_wsgi.py new file mode 100644 index 0000000000..7607101628 --- /dev/null +++ b/nova/tests/unit/api/openstack/test_wsgi.py @@ -0,0 +1,1244 @@ +# 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 inspect + +import webob + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova import exception +from nova import i18n +from nova import test +from nova.tests.unit.api.openstack import fakes +from nova.tests.unit import utils + + +class RequestTest(test.NoDBTestCase): + def test_content_type_missing(self): + request = wsgi.Request.blank('/tests/123', method='POST') + request.body = "<body />" + self.assertIsNone(request.get_content_type()) + + def test_content_type_unsupported(self): + request = wsgi.Request.blank('/tests/123', method='POST') + request.headers["Content-Type"] = "text/html" + request.body = "asdf<br />" + self.assertRaises(exception.InvalidContentType, + request.get_content_type) + + def test_content_type_with_charset(self): + request = wsgi.Request.blank('/tests/123') + request.headers["Content-Type"] = "application/json; charset=UTF-8" + result = request.get_content_type() + self.assertEqual(result, "application/json") + + def test_content_type_from_accept(self): + for content_type in ('application/xml', + 'application/vnd.openstack.compute+xml', + 'application/json', + 'application/vnd.openstack.compute+json'): + request = wsgi.Request.blank('/tests/123') + request.headers["Accept"] = content_type + result = request.best_match_content_type() + self.assertEqual(result, content_type) + + def test_content_type_from_accept_best(self): + request = wsgi.Request.blank('/tests/123') + request.headers["Accept"] = "application/xml, application/json" + result = request.best_match_content_type() + self.assertEqual(result, "application/json") + + request = wsgi.Request.blank('/tests/123') + request.headers["Accept"] = ("application/json; q=0.3, " + "application/xml; q=0.9") + result = request.best_match_content_type() + self.assertEqual(result, "application/xml") + + def test_content_type_from_query_extension(self): + request = wsgi.Request.blank('/tests/123.xml') + result = request.best_match_content_type() + self.assertEqual(result, "application/xml") + + request = wsgi.Request.blank('/tests/123.json') + result = request.best_match_content_type() + self.assertEqual(result, "application/json") + + request = wsgi.Request.blank('/tests/123.invalid') + result = request.best_match_content_type() + self.assertEqual(result, "application/json") + + def test_content_type_accept_and_query_extension(self): + request = wsgi.Request.blank('/tests/123.xml') + request.headers["Accept"] = "application/json" + result = request.best_match_content_type() + self.assertEqual(result, "application/xml") + + def test_content_type_accept_default(self): + request = wsgi.Request.blank('/tests/123.unsupported') + request.headers["Accept"] = "application/unsupported1" + result = request.best_match_content_type() + self.assertEqual(result, "application/json") + + def test_cache_and_retrieve_instances(self): + request = wsgi.Request.blank('/foo') + instances = [] + for x in xrange(3): + instances.append({'uuid': 'uuid%s' % x}) + # Store 2 + request.cache_db_instances(instances[:2]) + # Store 1 + request.cache_db_instance(instances[2]) + self.assertEqual(request.get_db_instance('uuid0'), + instances[0]) + self.assertEqual(request.get_db_instance('uuid1'), + instances[1]) + self.assertEqual(request.get_db_instance('uuid2'), + instances[2]) + self.assertIsNone(request.get_db_instance('uuid3')) + self.assertEqual(request.get_db_instances(), + {'uuid0': instances[0], + 'uuid1': instances[1], + 'uuid2': instances[2]}) + + def test_cache_and_retrieve_compute_nodes(self): + request = wsgi.Request.blank('/foo') + compute_nodes = [] + for x in xrange(3): + compute_nodes.append({'id': 'id%s' % x}) + # Store 2 + request.cache_db_compute_nodes(compute_nodes[:2]) + # Store 1 + request.cache_db_compute_node(compute_nodes[2]) + self.assertEqual(request.get_db_compute_node('id0'), + compute_nodes[0]) + self.assertEqual(request.get_db_compute_node('id1'), + compute_nodes[1]) + self.assertEqual(request.get_db_compute_node('id2'), + compute_nodes[2]) + self.assertIsNone(request.get_db_compute_node('id3')) + self.assertEqual(request.get_db_compute_nodes(), + {'id0': compute_nodes[0], + 'id1': compute_nodes[1], + 'id2': compute_nodes[2]}) + + def test_from_request(self): + self.stubs.Set(i18n, 'get_available_languages', + fakes.fake_get_available_languages) + + request = wsgi.Request.blank('/') + accepted = 'bogus;q=1.1, en-gb;q=0.7,en-us,en;q=.5,*;q=.7' + request.headers = {'Accept-Language': accepted} + self.assertEqual(request.best_match_language(), 'en_US') + + def test_asterisk(self): + # asterisk should match first available if there + # are not any other available matches + self.stubs.Set(i18n, 'get_available_languages', + fakes.fake_get_available_languages) + + request = wsgi.Request.blank('/') + accepted = '*,es;q=.5' + request.headers = {'Accept-Language': accepted} + self.assertEqual(request.best_match_language(), 'en_GB') + + def test_prefix(self): + self.stubs.Set(i18n, 'get_available_languages', + fakes.fake_get_available_languages) + + request = wsgi.Request.blank('/') + accepted = 'zh' + request.headers = {'Accept-Language': accepted} + self.assertEqual(request.best_match_language(), 'zh_CN') + + def test_secondary(self): + self.stubs.Set(i18n, 'get_available_languages', + fakes.fake_get_available_languages) + + request = wsgi.Request.blank('/') + accepted = 'nn,en-gb;q=.5' + request.headers = {'Accept-Language': accepted} + self.assertEqual(request.best_match_language(), 'en_GB') + + def test_none_found(self): + self.stubs.Set(i18n, 'get_available_languages', + fakes.fake_get_available_languages) + + request = wsgi.Request.blank('/') + accepted = 'nb-no' + request.headers = {'Accept-Language': accepted} + self.assertIs(request.best_match_language(), None) + + def test_no_lang_header(self): + self.stubs.Set(i18n, 'get_available_languages', + fakes.fake_get_available_languages) + + request = wsgi.Request.blank('/') + accepted = '' + request.headers = {'Accept-Language': accepted} + self.assertIs(request.best_match_language(), None) + + +class ActionDispatcherTest(test.NoDBTestCase): + def test_dispatch(self): + serializer = wsgi.ActionDispatcher() + serializer.create = lambda x: 'pants' + self.assertEqual(serializer.dispatch({}, action='create'), 'pants') + + def test_dispatch_action_None(self): + serializer = wsgi.ActionDispatcher() + serializer.create = lambda x: 'pants' + serializer.default = lambda x: 'trousers' + self.assertEqual(serializer.dispatch({}, action=None), 'trousers') + + def test_dispatch_default(self): + serializer = wsgi.ActionDispatcher() + serializer.create = lambda x: 'pants' + serializer.default = lambda x: 'trousers' + self.assertEqual(serializer.dispatch({}, action='update'), 'trousers') + + +class DictSerializerTest(test.NoDBTestCase): + def test_dispatch_default(self): + serializer = wsgi.DictSerializer() + self.assertEqual(serializer.serialize({}, 'update'), '') + + +class XMLDictSerializerTest(test.NoDBTestCase): + def test_xml(self): + input_dict = dict(servers=dict(a=(2, 3))) + expected_xml = '<serversxmlns="asdf"><a>(2,3)</a></servers>' + serializer = wsgi.XMLDictSerializer(xmlns="asdf") + result = serializer.serialize(input_dict) + result = result.replace('\n', '').replace(' ', '') + self.assertEqual(result, expected_xml) + + def test_xml_contains_unicode(self): + input_dict = dict(test=u'\u89e3\u7801') + expected_xml = '<test>\xe8\xa7\xa3\xe7\xa0\x81</test>' + serializer = wsgi.XMLDictSerializer() + result = serializer.serialize(input_dict) + result = result.replace('\n', '').replace(' ', '') + self.assertEqual(expected_xml, result) + + +class JSONDictSerializerTest(test.NoDBTestCase): + def test_json(self): + input_dict = dict(servers=dict(a=(2, 3))) + expected_json = '{"servers":{"a":[2,3]}}' + serializer = wsgi.JSONDictSerializer() + result = serializer.serialize(input_dict) + result = result.replace('\n', '').replace(' ', '') + self.assertEqual(result, expected_json) + + +class TextDeserializerTest(test.NoDBTestCase): + def test_dispatch_default(self): + deserializer = wsgi.TextDeserializer() + self.assertEqual(deserializer.deserialize({}, 'update'), {}) + + +class JSONDeserializerTest(test.NoDBTestCase): + def test_json(self): + data = """{"a": { + "a1": "1", + "a2": "2", + "bs": ["1", "2", "3", {"c": {"c1": "1"}}], + "d": {"e": "1"}, + "f": "1"}}""" + as_dict = { + 'body': { + 'a': { + 'a1': '1', + 'a2': '2', + 'bs': ['1', '2', '3', {'c': {'c1': '1'}}], + 'd': {'e': '1'}, + 'f': '1', + }, + }, + } + deserializer = wsgi.JSONDeserializer() + self.assertEqual(deserializer.deserialize(data), as_dict) + + def test_json_valid_utf8(self): + data = """{"server": {"min_count": 1, "flavorRef": "1", + "name": "\xe6\xa6\x82\xe5\xbf\xb5", + "imageRef": "10bab10c-1304-47d", + "max_count": 1}} """ + as_dict = { + 'body': { + u'server': { + u'min_count': 1, u'flavorRef': u'1', + u'name': u'\u6982\u5ff5', + u'imageRef': u'10bab10c-1304-47d', + u'max_count': 1 + } + } + } + deserializer = wsgi.JSONDeserializer() + self.assertEqual(deserializer.deserialize(data), as_dict) + + def test_json_invalid_utf8(self): + """Send invalid utf-8 to JSONDeserializer.""" + data = """{"server": {"min_count": 1, "flavorRef": "1", + "name": "\xf0\x28\x8c\x28", + "imageRef": "10bab10c-1304-47d", + "max_count": 1}} """ + + deserializer = wsgi.JSONDeserializer() + self.assertRaises(exception.MalformedRequestBody, + deserializer.deserialize, data) + + +class XMLDeserializerTest(test.NoDBTestCase): + def test_xml(self): + xml = """ + <a a1="1" a2="2"> + <bs><b>1</b><b>2</b><b>3</b><b><c c1="1"/></b></bs> + <d><e>1</e></d> + <f>1</f> + </a> + """.strip() + as_dict = { + 'body': { + 'a': { + 'a1': '1', + 'a2': '2', + 'bs': ['1', '2', '3', {'c': {'c1': '1'}}], + 'd': {'e': '1'}, + 'f': '1', + }, + }, + } + metadata = {'plurals': {'bs': 'b', 'ts': 't'}} + deserializer = wsgi.XMLDeserializer(metadata=metadata) + self.assertEqual(deserializer.deserialize(xml), as_dict) + + def test_xml_empty(self): + xml = '<a></a>' + as_dict = {"body": {"a": {}}} + deserializer = wsgi.XMLDeserializer() + self.assertEqual(deserializer.deserialize(xml), as_dict) + + def test_xml_valid_utf8(self): + xml = """ <a><name>\xe6\xa6\x82\xe5\xbf\xb5</name></a> """ + deserializer = wsgi.XMLDeserializer() + as_dict = {'body': {u'a': {u'name': u'\u6982\u5ff5'}}} + self.assertEqual(deserializer.deserialize(xml), as_dict) + + def test_xml_invalid_utf8(self): + """Send invalid utf-8 to XMLDeserializer.""" + xml = """ <a><name>\xf0\x28\x8c\x28</name></a> """ + deserializer = wsgi.XMLDeserializer() + self.assertRaises(exception.MalformedRequestBody, + deserializer.deserialize, xml) + + +class ResourceTest(test.NoDBTestCase): + + def get_req_id_header_name(self, request): + header_name = 'x-openstack-request-id' + if utils.get_api_version(request) < 3: + header_name = 'x-compute-request-id' + + return header_name + + def test_resource_call_with_method_get(self): + class Controller(object): + def index(self, req): + return 'success' + + app = fakes.TestRouter(Controller()) + # the default method is GET + req = webob.Request.blank('/tests') + response = req.get_response(app) + self.assertEqual(response.body, 'success') + self.assertEqual(response.status_int, 200) + req.body = '{"body": {"key": "value"}}' + response = req.get_response(app) + self.assertEqual(response.body, 'success') + self.assertEqual(response.status_int, 200) + req.content_type = 'application/json' + response = req.get_response(app) + self.assertEqual(response.body, 'success') + self.assertEqual(response.status_int, 200) + + def test_resource_call_with_method_post(self): + class Controller(object): + @extensions.expected_errors(400) + def create(self, req, body): + if expected_body != body: + msg = "The request body invalid" + raise webob.exc.HTTPBadRequest(explanation=msg) + return "success" + # verify the method: POST + app = fakes.TestRouter(Controller()) + req = webob.Request.blank('/tests', method="POST", + content_type='application/json') + req.body = '{"body": {"key": "value"}}' + expected_body = {'body': { + "key": "value" + } + } + response = req.get_response(app) + self.assertEqual(response.status_int, 200) + self.assertEqual(response.body, 'success') + # verify without body + expected_body = None + req.body = None + response = req.get_response(app) + self.assertEqual(response.status_int, 200) + self.assertEqual(response.body, 'success') + # the body is validated in the controller + expected_body = {'body': None} + response = req.get_response(app) + expected_unsupported_type_body = ('{"badRequest": ' + '{"message": "The request body invalid", "code": 400}}') + self.assertEqual(response.status_int, 400) + self.assertEqual(expected_unsupported_type_body, response.body) + + def test_resource_call_with_method_put(self): + class Controller(object): + def update(self, req, id, body): + if expected_body != body: + msg = "The request body invalid" + raise webob.exc.HTTPBadRequest(explanation=msg) + return "success" + # verify the method: PUT + app = fakes.TestRouter(Controller()) + req = webob.Request.blank('/tests/test_id', method="PUT", + content_type='application/json') + req.body = '{"body": {"key": "value"}}' + expected_body = {'body': { + "key": "value" + } + } + response = req.get_response(app) + self.assertEqual(response.body, 'success') + self.assertEqual(response.status_int, 200) + req.body = None + expected_body = None + response = req.get_response(app) + self.assertEqual(response.status_int, 200) + # verify no content_type is contained in the request + req.content_type = None + req.body = '{"body": {"key": "value"}}' + response = req.get_response(app) + expected_unsupported_type_body = ('{"badRequest": ' + '{"message": "Unsupported Content-Type", "code": 400}}') + self.assertEqual(response.status_int, 400) + self.assertEqual(expected_unsupported_type_body, response.body) + + def test_resource_call_with_method_delete(self): + class Controller(object): + def delete(self, req, id): + return "success" + + # verify the method: DELETE + app = fakes.TestRouter(Controller()) + req = webob.Request.blank('/tests/test_id', method="DELETE") + response = req.get_response(app) + self.assertEqual(response.status_int, 200) + self.assertEqual(response.body, 'success') + # ignore the body + req.body = '{"body": {"key": "value"}}' + response = req.get_response(app) + self.assertEqual(response.status_int, 200) + self.assertEqual(response.body, 'success') + + def test_resource_not_authorized(self): + class Controller(object): + def index(self, req): + raise exception.Forbidden() + + req = webob.Request.blank('/tests') + app = fakes.TestRouter(Controller()) + response = req.get_response(app) + self.assertEqual(response.status_int, 403) + + def test_dispatch(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + method, extensions = resource.get_method(None, 'index', None, '') + actual = resource.dispatch(method, None, {'pants': 'off'}) + expected = 'off' + self.assertEqual(actual, expected) + + def test_get_method_unknown_controller_method(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + self.assertRaises(AttributeError, resource.get_method, + None, 'create', None, '') + + def test_get_method_action_json(self): + class Controller(wsgi.Controller): + @wsgi.action('fooAction') + def _action_foo(self, req, id, body): + return body + + controller = Controller() + resource = wsgi.Resource(controller) + method, extensions = resource.get_method(None, 'action', + 'application/json', + '{"fooAction": true}') + self.assertEqual(controller._action_foo, method) + + def test_get_method_action_xml(self): + class Controller(wsgi.Controller): + @wsgi.action('fooAction') + def _action_foo(self, req, id, body): + return body + + controller = Controller() + resource = wsgi.Resource(controller) + method, extensions = resource.get_method(None, 'action', + 'application/xml', + '<fooAction>true</fooAction>') + self.assertEqual(controller._action_foo, method) + + def test_get_method_action_corrupt_xml(self): + class Controller(wsgi.Controller): + @wsgi.action('fooAction') + def _action_foo(self, req, id, body): + return body + + controller = Controller() + resource = wsgi.Resource(controller) + self.assertRaises( + exception.MalformedRequestBody, + resource.get_method, + None, 'action', + 'application/xml', + utils.killer_xml_body()) + + def test_get_method_action_bad_body(self): + class Controller(wsgi.Controller): + @wsgi.action('fooAction') + def _action_foo(self, req, id, body): + return body + + controller = Controller() + resource = wsgi.Resource(controller) + self.assertRaises(exception.MalformedRequestBody, resource.get_method, + None, 'action', 'application/json', '{}') + + def test_get_method_unknown_controller_action(self): + class Controller(wsgi.Controller): + @wsgi.action('fooAction') + def _action_foo(self, req, id, body): + return body + + controller = Controller() + resource = wsgi.Resource(controller) + self.assertRaises(KeyError, resource.get_method, + None, 'action', 'application/json', + '{"barAction": true}') + + def test_get_method_action_method(self): + class Controller(): + def action(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + method, extensions = resource.get_method(None, 'action', + 'application/xml', + '<fooAction>true</fooAction') + self.assertEqual(controller.action, method) + + def test_get_action_args(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + + env = { + 'wsgiorg.routing_args': [None, { + 'controller': None, + 'format': None, + 'action': 'update', + 'id': 12, + }], + } + + expected = {'action': 'update', 'id': 12} + + self.assertEqual(resource.get_action_args(env), expected) + + def test_get_body_bad_content(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + + request = wsgi.Request.blank('/', method='POST') + request.headers['Content-Type'] = 'application/none' + request.body = 'foo' + + content_type, body = resource.get_body(request) + self.assertIsNone(content_type) + self.assertEqual(body, '') + + def test_get_body_no_content_type(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + + request = wsgi.Request.blank('/', method='POST') + request.body = 'foo' + + content_type, body = resource.get_body(request) + self.assertIsNone(content_type) + self.assertEqual(body, 'foo') + + def test_get_body_no_content_body(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + + request = wsgi.Request.blank('/', method='POST') + request.headers['Content-Type'] = 'application/json' + request.body = '' + + content_type, body = resource.get_body(request) + self.assertEqual('application/json', content_type) + self.assertEqual(body, '') + + def test_get_body(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + + request = wsgi.Request.blank('/', method='POST') + request.headers['Content-Type'] = 'application/json' + request.body = 'foo' + + content_type, body = resource.get_body(request) + self.assertEqual(content_type, 'application/json') + self.assertEqual(body, 'foo') + + def test_get_request_id_with_dict_response_body(self): + class Controller(wsgi.Controller): + def index(self, req): + return {'foo': 'bar'} + + req = fakes.HTTPRequest.blank('/tests') + app = fakes.TestRouter(Controller()) + response = req.get_response(app) + self.assertIn('nova.context', req.environ) + self.assertEqual(response.body, '{"foo": "bar"}') + self.assertEqual(response.status_int, 200) + + def test_no_request_id_with_str_response_body(self): + class Controller(wsgi.Controller): + def index(self, req): + return 'foo' + + req = fakes.HTTPRequest.blank('/tests') + app = fakes.TestRouter(Controller()) + response = req.get_response(app) + # NOTE(alaski): This test is really to ensure that a str response + # doesn't error. Not having a request_id header is a side effect of + # our wsgi setup, ideally it would be there. + expected_header = self.get_req_id_header_name(req) + self.assertFalse(hasattr(response.headers, expected_header)) + self.assertEqual(response.body, 'foo') + self.assertEqual(response.status_int, 200) + + def test_get_request_id_no_response_body(self): + class Controller(object): + def index(self, req): + pass + + req = fakes.HTTPRequest.blank('/tests') + app = fakes.TestRouter(Controller()) + response = req.get_response(app) + self.assertIn('nova.context', req.environ) + self.assertEqual(response.body, '') + self.assertEqual(response.status_int, 200) + + def test_deserialize_badtype(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + self.assertRaises(exception.InvalidContentType, + resource.deserialize, + controller.index, 'application/none', 'foo') + + def test_deserialize_default(self): + class JSONDeserializer(object): + def deserialize(self, body): + return 'json' + + class XMLDeserializer(object): + def deserialize(self, body): + return 'xml' + + class Controller(object): + @wsgi.deserializers(xml=XMLDeserializer) + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller, json=JSONDeserializer) + + obj = resource.deserialize(controller.index, 'application/json', 'foo') + self.assertEqual(obj, 'json') + + def test_deserialize_decorator(self): + class JSONDeserializer(object): + def deserialize(self, body): + return 'json' + + class XMLDeserializer(object): + def deserialize(self, body): + return 'xml' + + class Controller(object): + @wsgi.deserializers(xml=XMLDeserializer) + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller, json=JSONDeserializer) + + obj = resource.deserialize(controller.index, 'application/xml', 'foo') + self.assertEqual(obj, 'xml') + + def test_register_actions(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + class ControllerExtended(wsgi.Controller): + @wsgi.action('fooAction') + def _action_foo(self, req, id, body): + return body + + @wsgi.action('barAction') + def _action_bar(self, req, id, body): + return body + + controller = Controller() + resource = wsgi.Resource(controller) + self.assertEqual({}, resource.wsgi_actions) + + extended = ControllerExtended() + resource.register_actions(extended) + self.assertEqual({ + 'fooAction': extended._action_foo, + 'barAction': extended._action_bar, + }, resource.wsgi_actions) + + def test_register_extensions(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + class ControllerExtended(wsgi.Controller): + @wsgi.extends + def index(self, req, resp_obj, pants=None): + return None + + @wsgi.extends(action='fooAction') + def _action_foo(self, req, resp, id, body): + return None + + controller = Controller() + resource = wsgi.Resource(controller) + self.assertEqual({}, resource.wsgi_extensions) + self.assertEqual({}, resource.wsgi_action_extensions) + + extended = ControllerExtended() + resource.register_extensions(extended) + self.assertEqual({'index': [extended.index]}, resource.wsgi_extensions) + self.assertEqual({'fooAction': [extended._action_foo]}, + resource.wsgi_action_extensions) + + def test_get_method_extensions(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + class ControllerExtended(wsgi.Controller): + @wsgi.extends + def index(self, req, resp_obj, pants=None): + return None + + controller = Controller() + extended = ControllerExtended() + resource = wsgi.Resource(controller) + resource.register_extensions(extended) + method, extensions = resource.get_method(None, 'index', None, '') + self.assertEqual(method, controller.index) + self.assertEqual(extensions, [extended.index]) + + def test_get_method_action_extensions(self): + class Controller(wsgi.Controller): + def index(self, req, pants=None): + return pants + + @wsgi.action('fooAction') + def _action_foo(self, req, id, body): + return body + + class ControllerExtended(wsgi.Controller): + @wsgi.extends(action='fooAction') + def _action_foo(self, req, resp_obj, id, body): + return None + + controller = Controller() + extended = ControllerExtended() + resource = wsgi.Resource(controller) + resource.register_extensions(extended) + method, extensions = resource.get_method(None, 'action', + 'application/json', + '{"fooAction": true}') + self.assertEqual(method, controller._action_foo) + self.assertEqual(extensions, [extended._action_foo]) + + def test_get_method_action_whitelist_extensions(self): + class Controller(wsgi.Controller): + def index(self, req, pants=None): + return pants + + class ControllerExtended(wsgi.Controller): + @wsgi.action('create') + def _create(self, req, body): + pass + + @wsgi.action('delete') + def _delete(self, req, id): + pass + + controller = Controller() + extended = ControllerExtended() + resource = wsgi.Resource(controller) + resource.register_actions(extended) + + method, extensions = resource.get_method(None, 'create', + 'application/json', + '{"create": true}') + self.assertEqual(method, extended._create) + self.assertEqual(extensions, []) + + method, extensions = resource.get_method(None, 'delete', None, None) + self.assertEqual(method, extended._delete) + self.assertEqual(extensions, []) + + def test_pre_process_extensions_regular(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + + called = [] + + def extension1(req, resp_obj): + called.append(1) + return None + + def extension2(req, resp_obj): + called.append(2) + return None + + extensions = [extension1, extension2] + response, post = resource.pre_process_extensions(extensions, None, {}) + self.assertEqual(called, []) + self.assertIsNone(response) + self.assertEqual(list(post), [extension2, extension1]) + + def test_pre_process_extensions_generator(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + + called = [] + + def extension1(req): + called.append('pre1') + yield + called.append('post1') + + def extension2(req): + called.append('pre2') + yield + called.append('post2') + + extensions = [extension1, extension2] + response, post = resource.pre_process_extensions(extensions, None, {}) + post = list(post) + self.assertEqual(called, ['pre1', 'pre2']) + self.assertIsNone(response) + self.assertEqual(len(post), 2) + self.assertTrue(inspect.isgenerator(post[0])) + self.assertTrue(inspect.isgenerator(post[1])) + + for gen in post: + try: + gen.send(None) + except StopIteration: + continue + + self.assertEqual(called, ['pre1', 'pre2', 'post2', 'post1']) + + def test_pre_process_extensions_generator_response(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + + called = [] + + def extension1(req): + called.append('pre1') + yield 'foo' + + def extension2(req): + called.append('pre2') + + extensions = [extension1, extension2] + response, post = resource.pre_process_extensions(extensions, None, {}) + self.assertEqual(called, ['pre1']) + self.assertEqual(response, 'foo') + self.assertEqual(post, []) + + def test_post_process_extensions_regular(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + + called = [] + + def extension1(req, resp_obj): + called.append(1) + return None + + def extension2(req, resp_obj): + called.append(2) + return None + + response = resource.post_process_extensions([extension2, extension1], + None, None, {}) + self.assertEqual(called, [2, 1]) + self.assertIsNone(response) + + def test_post_process_extensions_regular_response(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + + called = [] + + def extension1(req, resp_obj): + called.append(1) + return None + + def extension2(req, resp_obj): + called.append(2) + return 'foo' + + response = resource.post_process_extensions([extension2, extension1], + None, None, {}) + self.assertEqual(called, [2]) + self.assertEqual(response, 'foo') + + def test_post_process_extensions_generator(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + + called = [] + + def extension1(req): + yield + called.append(1) + + def extension2(req): + yield + called.append(2) + + ext1 = extension1(None) + ext1.next() + ext2 = extension2(None) + ext2.next() + + response = resource.post_process_extensions([ext2, ext1], + None, None, {}) + + self.assertEqual(called, [2, 1]) + self.assertIsNone(response) + + def test_post_process_extensions_generator_response(self): + class Controller(object): + def index(self, req, pants=None): + return pants + + controller = Controller() + resource = wsgi.Resource(controller) + + called = [] + + def extension1(req): + yield + called.append(1) + + def extension2(req): + yield + called.append(2) + yield 'foo' + + ext1 = extension1(None) + ext1.next() + ext2 = extension2(None) + ext2.next() + + response = resource.post_process_extensions([ext2, ext1], + None, None, {}) + + self.assertEqual(called, [2]) + self.assertEqual(response, 'foo') + + def test_resource_exception_handler_type_error(self): + # A TypeError should be translated to a Fault/HTTP 400. + def foo(a,): + return a + + try: + with wsgi.ResourceExceptionHandler(): + foo() # generate a TypeError + self.fail("Should have raised a Fault (HTTP 400)") + except wsgi.Fault as fault: + self.assertEqual(400, fault.status_int) + + def test_resource_headers_are_utf8(self): + resp = webob.Response(status_int=202) + resp.headers['x-header1'] = 1 + resp.headers['x-header2'] = u'header2' + resp.headers['x-header3'] = u'header3' + + class Controller(object): + def index(self, req): + return resp + + req = webob.Request.blank('/tests') + app = fakes.TestRouter(Controller()) + response = req.get_response(app) + + for hdr, val in response.headers.iteritems(): + # All headers must be utf8 + self.assertIsInstance(hdr, str) + self.assertIsInstance(val, str) + self.assertEqual(response.headers['x-header1'], '1') + self.assertEqual(response.headers['x-header2'], 'header2') + self.assertEqual(response.headers['x-header3'], 'header3') + + def test_resource_valid_utf8_body(self): + class Controller(object): + def update(self, req, id, body): + return body + + req = webob.Request.blank('/tests/test_id', method="PUT") + body = """ {"name": "\xe6\xa6\x82\xe5\xbf\xb5" } """ + expected_body = '{"name": "\\u6982\\u5ff5"}' + req.body = body + req.headers['Content-Type'] = 'application/json' + app = fakes.TestRouter(Controller()) + response = req.get_response(app) + self.assertEqual(response.body, expected_body) + self.assertEqual(response.status_int, 200) + + def test_resource_invalid_utf8(self): + class Controller(object): + def update(self, req, id, body): + return body + + req = webob.Request.blank('/tests/test_id', method="PUT") + body = """ {"name": "\xf0\x28\x8c\x28" } """ + req.body = body + req.headers['Content-Type'] = 'application/json' + app = fakes.TestRouter(Controller()) + self.assertRaises(UnicodeDecodeError, req.get_response, app) + + +class ResponseObjectTest(test.NoDBTestCase): + def test_default_code(self): + robj = wsgi.ResponseObject({}) + self.assertEqual(robj.code, 200) + + def test_modified_code(self): + robj = wsgi.ResponseObject({}) + robj._default_code = 202 + self.assertEqual(robj.code, 202) + + def test_override_default_code(self): + robj = wsgi.ResponseObject({}, code=404) + self.assertEqual(robj.code, 404) + + def test_override_modified_code(self): + robj = wsgi.ResponseObject({}, code=404) + robj._default_code = 202 + self.assertEqual(robj.code, 404) + + def test_set_header(self): + robj = wsgi.ResponseObject({}) + robj['Header'] = 'foo' + self.assertEqual(robj.headers, {'header': 'foo'}) + + def test_get_header(self): + robj = wsgi.ResponseObject({}) + robj['Header'] = 'foo' + self.assertEqual(robj['hEADER'], 'foo') + + def test_del_header(self): + robj = wsgi.ResponseObject({}) + robj['Header'] = 'foo' + del robj['hEADER'] + self.assertNotIn('header', robj.headers) + + def test_header_isolation(self): + robj = wsgi.ResponseObject({}) + robj['Header'] = 'foo' + hdrs = robj.headers + hdrs['hEADER'] = 'bar' + self.assertEqual(robj['hEADER'], 'foo') + + def test_default_serializers(self): + robj = wsgi.ResponseObject({}) + self.assertEqual(robj.serializers, {}) + + def test_bind_serializers(self): + robj = wsgi.ResponseObject({}, json='foo') + robj._bind_method_serializers(dict(xml='bar', json='baz')) + self.assertEqual(robj.serializers, dict(xml='bar', json='foo')) + + def test_get_serializer(self): + robj = wsgi.ResponseObject({}, json='json', xml='xml', atom='atom') + for content_type, mtype in wsgi._MEDIA_TYPE_MAP.items(): + _mtype, serializer = robj.get_serializer(content_type) + self.assertEqual(serializer, mtype) + + def test_get_serializer_defaults(self): + robj = wsgi.ResponseObject({}) + default_serializers = dict(json='json', xml='xml', atom='atom') + for content_type, mtype in wsgi._MEDIA_TYPE_MAP.items(): + self.assertRaises(exception.InvalidContentType, + robj.get_serializer, content_type) + _mtype, serializer = robj.get_serializer(content_type, + default_serializers) + self.assertEqual(serializer, mtype) + + def test_serialize(self): + class JSONSerializer(object): + def serialize(self, obj): + return 'json' + + class XMLSerializer(object): + def serialize(self, obj): + return 'xml' + + class AtomSerializer(object): + def serialize(self, obj): + return 'atom' + + robj = wsgi.ResponseObject({}, code=202, + json=JSONSerializer, + xml=XMLSerializer, + atom=AtomSerializer) + robj['X-header1'] = 'header1' + robj['X-header2'] = 'header2' + robj['X-header3'] = 3 + robj['X-header-unicode'] = u'header-unicode' + + for content_type, mtype in wsgi._MEDIA_TYPE_MAP.items(): + request = wsgi.Request.blank('/tests/123') + response = robj.serialize(request, content_type) + + self.assertEqual(response.headers['Content-Type'], content_type) + for hdr, val in response.headers.iteritems(): + # All headers must be utf8 + self.assertIsInstance(hdr, str) + self.assertIsInstance(val, str) + self.assertEqual(response.headers['X-header1'], 'header1') + self.assertEqual(response.headers['X-header2'], 'header2') + self.assertEqual(response.headers['X-header3'], '3') + self.assertEqual(response.status_int, 202) + self.assertEqual(response.body, mtype) + + +class ValidBodyTest(test.NoDBTestCase): + + def setUp(self): + super(ValidBodyTest, self).setUp() + self.controller = wsgi.Controller() + + def test_is_valid_body(self): + body = {'foo': {}} + self.assertTrue(self.controller.is_valid_body(body, 'foo')) + + def test_is_valid_body_none(self): + wsgi.Resource(controller=None) + self.assertFalse(self.controller.is_valid_body(None, 'foo')) + + def test_is_valid_body_empty(self): + wsgi.Resource(controller=None) + self.assertFalse(self.controller.is_valid_body({}, 'foo')) + + def test_is_valid_body_no_entity(self): + wsgi.Resource(controller=None) + body = {'bar': {}} + self.assertFalse(self.controller.is_valid_body(body, 'foo')) + + def test_is_valid_body_malformed_entity(self): + wsgi.Resource(controller=None) + body = {'foo': 'bar'} + self.assertFalse(self.controller.is_valid_body(body, 'foo')) diff --git a/nova/tests/unit/api/openstack/test_xmlutil.py b/nova/tests/unit/api/openstack/test_xmlutil.py new file mode 100644 index 0000000000..19186889bb --- /dev/null +++ b/nova/tests/unit/api/openstack/test_xmlutil.py @@ -0,0 +1,948 @@ +# Copyright 2011 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. + +from xml.dom import minidom + +from lxml import etree + +from nova.api.openstack import xmlutil +from nova import exception +from nova import test +from nova.tests.unit import utils as tests_utils + + +class SelectorTest(test.NoDBTestCase): + obj_for_test = { + 'test': { + 'name': 'test', + 'values': [1, 2, 3], + 'attrs': { + 'foo': 1, + 'bar': 2, + 'baz': 3, + }, + }, + } + + def test_repr(self): + sel = xmlutil.Selector() + self.assertEqual(repr(sel), "Selector()") + + def test_empty_selector(self): + sel = xmlutil.EmptyStringSelector() + self.assertEqual(len(sel.chain), 0) + self.assertEqual(sel(self.obj_for_test), self.obj_for_test) + self.assertEqual( + repr(self.obj_for_test), + "{'test': {'values': [1, 2, 3], 'name': 'test', 'attrs': " + "{'baz': 3, 'foo': 1, 'bar': 2}}}") + + def test_dict_selector(self): + sel = xmlutil.Selector('test') + self.assertEqual(len(sel.chain), 1) + self.assertEqual(sel.chain[0], 'test') + self.assertEqual(sel(self.obj_for_test), + self.obj_for_test['test']) + + def test_datum_selector(self): + sel = xmlutil.Selector('test', 'name') + self.assertEqual(len(sel.chain), 2) + self.assertEqual(sel.chain[0], 'test') + self.assertEqual(sel.chain[1], 'name') + self.assertEqual(sel(self.obj_for_test), 'test') + + def test_list_selector(self): + sel = xmlutil.Selector('test', 'values', 0) + self.assertEqual(len(sel.chain), 3) + self.assertEqual(sel.chain[0], 'test') + self.assertEqual(sel.chain[1], 'values') + self.assertEqual(sel.chain[2], 0) + self.assertEqual(sel(self.obj_for_test), 1) + + def test_items_selector(self): + sel = xmlutil.Selector('test', 'attrs', xmlutil.get_items) + self.assertEqual(len(sel.chain), 3) + self.assertEqual(sel.chain[2], xmlutil.get_items) + for key, val in sel(self.obj_for_test): + self.assertEqual(self.obj_for_test['test']['attrs'][key], val) + + def test_missing_key_selector(self): + sel = xmlutil.Selector('test2', 'attrs') + self.assertIsNone(sel(self.obj_for_test)) + self.assertRaises(KeyError, sel, self.obj_for_test, True) + + def test_constant_selector(self): + sel = xmlutil.ConstantSelector('Foobar') + self.assertEqual(sel.value, 'Foobar') + self.assertEqual(sel(self.obj_for_test), 'Foobar') + self.assertEqual(repr(sel), "'Foobar'") + + +class TemplateElementTest(test.NoDBTestCase): + def test_element_initial_attributes(self): + # Create a template element with some attributes + elem = xmlutil.TemplateElement('test', attrib=dict(a=1, b=2, c=3), + c=4, d=5, e=6) + + # Verify all the attributes are as expected + expected = dict(a=1, b=2, c=4, d=5, e=6) + for k, v in expected.items(): + self.assertEqual(elem.attrib[k].chain[0], v) + self.assertTrue(repr(elem)) + + def test_element_get_attributes(self): + expected = dict(a=1, b=2, c=3) + + # Create a template element with some attributes + elem = xmlutil.TemplateElement('test', attrib=expected) + + # Verify that get() retrieves the attributes + for k, v in expected.items(): + self.assertEqual(elem.get(k).chain[0], v) + + def test_element_set_attributes(self): + attrs = dict(a=None, b='foo', c=xmlutil.Selector('foo', 'bar')) + + # Create a bare template element with no attributes + elem = xmlutil.TemplateElement('test') + + # Set the attribute values + for k, v in attrs.items(): + elem.set(k, v) + + # Now verify what got set + self.assertEqual(len(elem.attrib['a'].chain), 1) + self.assertEqual(elem.attrib['a'].chain[0], 'a') + self.assertEqual(len(elem.attrib['b'].chain), 1) + self.assertEqual(elem.attrib['b'].chain[0], 'foo') + self.assertEqual(elem.attrib['c'], attrs['c']) + + def test_element_attribute_keys(self): + attrs = dict(a=1, b=2, c=3, d=4) + expected = set(attrs.keys()) + + # Create a template element with some attributes + elem = xmlutil.TemplateElement('test', attrib=attrs) + + # Now verify keys + self.assertEqual(set(elem.keys()), expected) + + def test_element_attribute_items(self): + expected = dict(a=xmlutil.Selector(1), + b=xmlutil.Selector(2), + c=xmlutil.Selector(3)) + keys = set(expected.keys()) + + # Create a template element with some attributes + elem = xmlutil.TemplateElement('test', attrib=expected) + + # Now verify items + for k, v in elem.items(): + self.assertEqual(expected[k], v) + keys.remove(k) + + # Did we visit all keys? + self.assertEqual(len(keys), 0) + + def test_element_selector_none(self): + # Create a template element with no selector + elem = xmlutil.TemplateElement('test') + + self.assertEqual(len(elem.selector.chain), 0) + + def test_element_selector_string(self): + # Create a template element with a string selector + elem = xmlutil.TemplateElement('test', selector='test') + + self.assertEqual(len(elem.selector.chain), 1) + self.assertEqual(elem.selector.chain[0], 'test') + + def test_element_selector(self): + sel = xmlutil.Selector('a', 'b') + + # Create a template element with an explicit selector + elem = xmlutil.TemplateElement('test', selector=sel) + + self.assertEqual(elem.selector, sel) + + def test_element_subselector_none(self): + # Create a template element with no subselector + elem = xmlutil.TemplateElement('test') + + self.assertIsNone(elem.subselector) + + def test_element_subselector_string(self): + # Create a template element with a string subselector + elem = xmlutil.TemplateElement('test', subselector='test') + + self.assertEqual(len(elem.subselector.chain), 1) + self.assertEqual(elem.subselector.chain[0], 'test') + + def test_element_subselector(self): + sel = xmlutil.Selector('a', 'b') + + # Create a template element with an explicit subselector + elem = xmlutil.TemplateElement('test', subselector=sel) + + self.assertEqual(elem.subselector, sel) + + def test_element_append_child(self): + # Create an element + elem = xmlutil.TemplateElement('test') + + # Make sure the element starts off empty + self.assertEqual(len(elem), 0) + + # Create a child element + child = xmlutil.TemplateElement('child') + + # Append the child to the parent + elem.append(child) + + # Verify that the child was added + self.assertEqual(len(elem), 1) + self.assertEqual(elem[0], child) + self.assertIn('child', elem) + self.assertEqual(elem['child'], child) + + # Ensure that multiple children of the same name are rejected + child2 = xmlutil.TemplateElement('child') + self.assertRaises(KeyError, elem.append, child2) + + def test_element_extend_children(self): + # Create an element + elem = xmlutil.TemplateElement('test') + + # Make sure the element starts off empty + self.assertEqual(len(elem), 0) + + # Create a few children + children = [ + xmlutil.TemplateElement('child1'), + xmlutil.TemplateElement('child2'), + xmlutil.TemplateElement('child3'), + ] + + # Extend the parent by those children + elem.extend(children) + + # Verify that the children were added + self.assertEqual(len(elem), 3) + for idx in range(len(elem)): + self.assertEqual(children[idx], elem[idx]) + self.assertIn(children[idx].tag, elem) + self.assertEqual(elem[children[idx].tag], children[idx]) + + # Ensure that multiple children of the same name are rejected + children2 = [ + xmlutil.TemplateElement('child4'), + xmlutil.TemplateElement('child1'), + ] + self.assertRaises(KeyError, elem.extend, children2) + + # Also ensure that child4 was not added + self.assertEqual(len(elem), 3) + self.assertEqual(elem[-1].tag, 'child3') + + def test_element_insert_child(self): + # Create an element + elem = xmlutil.TemplateElement('test') + + # Make sure the element starts off empty + self.assertEqual(len(elem), 0) + + # Create a few children + children = [ + xmlutil.TemplateElement('child1'), + xmlutil.TemplateElement('child2'), + xmlutil.TemplateElement('child3'), + ] + + # Extend the parent by those children + elem.extend(children) + + # Create a child to insert + child = xmlutil.TemplateElement('child4') + + # Insert it + elem.insert(1, child) + + # Ensure the child was inserted in the right place + self.assertEqual(len(elem), 4) + children.insert(1, child) + for idx in range(len(elem)): + self.assertEqual(children[idx], elem[idx]) + self.assertIn(children[idx].tag, elem) + self.assertEqual(elem[children[idx].tag], children[idx]) + + # Ensure that multiple children of the same name are rejected + child2 = xmlutil.TemplateElement('child2') + self.assertRaises(KeyError, elem.insert, 2, child2) + + def test_element_remove_child(self): + # Create an element + elem = xmlutil.TemplateElement('test') + + # Make sure the element starts off empty + self.assertEqual(len(elem), 0) + + # Create a few children + children = [ + xmlutil.TemplateElement('child1'), + xmlutil.TemplateElement('child2'), + xmlutil.TemplateElement('child3'), + ] + + # Extend the parent by those children + elem.extend(children) + + # Create a test child to remove + child = xmlutil.TemplateElement('child2') + + # Try to remove it + self.assertRaises(ValueError, elem.remove, child) + + # Ensure that no child was removed + self.assertEqual(len(elem), 3) + + # Now remove a legitimate child + elem.remove(children[1]) + + # Ensure that the child was removed + self.assertEqual(len(elem), 2) + self.assertEqual(elem[0], children[0]) + self.assertEqual(elem[1], children[2]) + self.assertEqual('child2' in elem, False) + + # Ensure the child cannot be retrieved by name + def get_key(elem, key): + return elem[key] + self.assertRaises(KeyError, get_key, elem, 'child2') + + def test_element_text(self): + # Create an element + elem = xmlutil.TemplateElement('test') + + # Ensure that it has no text + self.assertIsNone(elem.text) + + # Try setting it to a string and ensure it becomes a selector + elem.text = 'test' + self.assertEqual(hasattr(elem.text, 'chain'), True) + self.assertEqual(len(elem.text.chain), 1) + self.assertEqual(elem.text.chain[0], 'test') + + # Try resetting the text to None + elem.text = None + self.assertIsNone(elem.text) + + # Now make up a selector and try setting the text to that + sel = xmlutil.Selector() + elem.text = sel + self.assertEqual(elem.text, sel) + + # Finally, try deleting the text and see what happens + del elem.text + self.assertIsNone(elem.text) + + def test_apply_attrs(self): + # Create a template element + attrs = dict(attr1=xmlutil.ConstantSelector(1), + attr2=xmlutil.ConstantSelector(2)) + tmpl_elem = xmlutil.TemplateElement('test', attrib=attrs) + + # Create an etree element + elem = etree.Element('test') + + # Apply the template to the element + tmpl_elem.apply(elem, None) + + # Now, verify the correct attributes were set + for k, v in elem.items(): + self.assertEqual(str(attrs[k].value), v) + + def test_apply_text(self): + # Create a template element + tmpl_elem = xmlutil.TemplateElement('test') + tmpl_elem.text = xmlutil.ConstantSelector(1) + + # Create an etree element + elem = etree.Element('test') + + # Apply the template to the element + tmpl_elem.apply(elem, None) + + # Now, verify the text was set + self.assertEqual(str(tmpl_elem.text.value), elem.text) + + def test__render(self): + attrs = dict(attr1=xmlutil.ConstantSelector(1), + attr2=xmlutil.ConstantSelector(2), + attr3=xmlutil.ConstantSelector(3)) + + # Create a master template element + master_elem = xmlutil.TemplateElement('test', attr1=attrs['attr1']) + + # Create a couple of slave template element + slave_elems = [ + xmlutil.TemplateElement('test', attr2=attrs['attr2']), + xmlutil.TemplateElement('test', attr3=attrs['attr3']), + ] + + # Try the render + elem = master_elem._render(None, None, slave_elems, None) + + # Verify the particulars of the render + self.assertEqual(elem.tag, 'test') + self.assertEqual(len(elem.nsmap), 0) + for k, v in elem.items(): + self.assertEqual(str(attrs[k].value), v) + + # Create a parent for the element to be rendered + parent = etree.Element('parent') + + # Try the render again... + elem = master_elem._render(parent, None, slave_elems, dict(a='foo')) + + # Verify the particulars of the render + self.assertEqual(len(parent), 1) + self.assertEqual(parent[0], elem) + self.assertEqual(len(elem.nsmap), 1) + self.assertEqual(elem.nsmap['a'], 'foo') + + def test_render(self): + # Create a template element + tmpl_elem = xmlutil.TemplateElement('test') + tmpl_elem.text = xmlutil.Selector() + + # Create the object we're going to render + obj = ['elem1', 'elem2', 'elem3', 'elem4'] + + # Try a render with no object + elems = tmpl_elem.render(None, None) + self.assertEqual(len(elems), 0) + + # Try a render with one object + elems = tmpl_elem.render(None, 'foo') + self.assertEqual(len(elems), 1) + self.assertEqual(elems[0][0].text, 'foo') + self.assertEqual(elems[0][1], 'foo') + + # Now, try rendering an object with multiple entries + parent = etree.Element('parent') + elems = tmpl_elem.render(parent, obj) + self.assertEqual(len(elems), 4) + + # Check the results + for idx in range(len(obj)): + self.assertEqual(elems[idx][0].text, obj[idx]) + self.assertEqual(elems[idx][1], obj[idx]) + + # Check with a subselector + tmpl_elem = xmlutil.TemplateElement( + 'test', + subselector=xmlutil.ConstantSelector('foo')) + parent = etree.Element('parent') + + # Try a render with no object + elems = tmpl_elem.render(parent, obj) + self.assertEqual(len(elems), 4) + + def test_subelement(self): + # Try the SubTemplateElement constructor + parent = xmlutil.SubTemplateElement(None, 'parent') + self.assertEqual(parent.tag, 'parent') + self.assertEqual(len(parent), 0) + + # Now try it with a parent element + child = xmlutil.SubTemplateElement(parent, 'child') + self.assertEqual(child.tag, 'child') + self.assertEqual(len(parent), 1) + self.assertEqual(parent[0], child) + + def test_wrap(self): + # These are strange methods, but they make things easier + elem = xmlutil.TemplateElement('test') + self.assertEqual(elem.unwrap(), elem) + self.assertEqual(elem.wrap().root, elem) + + def test_dyntag(self): + obj = ['a', 'b', 'c'] + + # Create a template element with a dynamic tag + tmpl_elem = xmlutil.TemplateElement(xmlutil.Selector()) + + # Try the render + parent = etree.Element('parent') + elems = tmpl_elem.render(parent, obj) + + # Verify the particulars of the render + self.assertEqual(len(elems), len(obj)) + for idx in range(len(obj)): + self.assertEqual(elems[idx][0].tag, obj[idx]) + + def test_tree(self): + # Create a template element + elem = xmlutil.TemplateElement('test', attr3='attr3') + elem.text = 'test' + self.assertEqual(elem.tree(), + "<test !selector=Selector() " + "!text=Selector('test',) " + "attr3=Selector('attr3',)" + "/>") + + # Create a template element + elem = xmlutil.TemplateElement('test2') + + # Create a child element + child = xmlutil.TemplateElement('child') + + # Append the child to the parent + elem.append(child) + + self.assertEqual(elem.tree(), + "<test2 !selector=Selector()>" + "<child !selector=Selector()/></test2>") + + +class TemplateTest(test.NoDBTestCase): + def test_tree(self): + elem = xmlutil.TemplateElement('test') + tmpl = xmlutil.Template(elem) + self.assertTrue(tmpl.tree()) + + def test_wrap(self): + # These are strange methods, but they make things easier + elem = xmlutil.TemplateElement('test') + tmpl = xmlutil.Template(elem) + self.assertEqual(tmpl.unwrap(), elem) + self.assertEqual(tmpl.wrap(), tmpl) + + def test__siblings(self): + # Set up a basic template + elem = xmlutil.TemplateElement('test') + tmpl = xmlutil.Template(elem) + + # Check that we get the right siblings + siblings = tmpl._siblings() + self.assertEqual(len(siblings), 1) + self.assertEqual(siblings[0], elem) + + def test__nsmap(self): + # Set up a basic template + elem = xmlutil.TemplateElement('test') + tmpl = xmlutil.Template(elem, nsmap=dict(a="foo")) + + # Check out that we get the right namespace dictionary + nsmap = tmpl._nsmap() + self.assertNotEqual(id(nsmap), id(tmpl.nsmap)) + self.assertEqual(len(nsmap), 1) + self.assertEqual(nsmap['a'], 'foo') + + def test_master_attach(self): + # Set up a master template + elem = xmlutil.TemplateElement('test') + tmpl = xmlutil.MasterTemplate(elem, 1) + + # Make sure it has a root but no slaves + self.assertEqual(tmpl.root, elem) + self.assertEqual(len(tmpl.slaves), 0) + self.assertTrue(repr(tmpl)) + + # Try to attach an invalid slave + bad_elem = xmlutil.TemplateElement('test2') + self.assertRaises(ValueError, tmpl.attach, bad_elem) + self.assertEqual(len(tmpl.slaves), 0) + + # Try to attach an invalid and a valid slave + good_elem = xmlutil.TemplateElement('test') + self.assertRaises(ValueError, tmpl.attach, good_elem, bad_elem) + self.assertEqual(len(tmpl.slaves), 0) + + # Try to attach an inapplicable template + class InapplicableTemplate(xmlutil.Template): + def apply(self, master): + return False + inapp_tmpl = InapplicableTemplate(good_elem) + tmpl.attach(inapp_tmpl) + self.assertEqual(len(tmpl.slaves), 0) + + # Now try attaching an applicable template + tmpl.attach(good_elem) + self.assertEqual(len(tmpl.slaves), 1) + self.assertEqual(tmpl.slaves[0].root, good_elem) + + def test_master_copy(self): + # Construct a master template + elem = xmlutil.TemplateElement('test') + tmpl = xmlutil.MasterTemplate(elem, 1, nsmap=dict(a='foo')) + + # Give it a slave + slave = xmlutil.TemplateElement('test') + tmpl.attach(slave) + + # Construct a copy + copy = tmpl.copy() + + # Check to see if we actually managed a copy + self.assertNotEqual(tmpl, copy) + self.assertEqual(tmpl.root, copy.root) + self.assertEqual(tmpl.version, copy.version) + self.assertEqual(id(tmpl.nsmap), id(copy.nsmap)) + self.assertNotEqual(id(tmpl.slaves), id(copy.slaves)) + self.assertEqual(len(tmpl.slaves), len(copy.slaves)) + self.assertEqual(tmpl.slaves[0], copy.slaves[0]) + + def test_slave_apply(self): + # Construct a master template + elem = xmlutil.TemplateElement('test') + master = xmlutil.MasterTemplate(elem, 3) + + # Construct a slave template with applicable minimum version + slave = xmlutil.SlaveTemplate(elem, 2) + self.assertEqual(slave.apply(master), True) + self.assertTrue(repr(slave)) + + # Construct a slave template with equal minimum version + slave = xmlutil.SlaveTemplate(elem, 3) + self.assertEqual(slave.apply(master), True) + + # Construct a slave template with inapplicable minimum version + slave = xmlutil.SlaveTemplate(elem, 4) + self.assertEqual(slave.apply(master), False) + + # Construct a slave template with applicable version range + slave = xmlutil.SlaveTemplate(elem, 2, 4) + self.assertEqual(slave.apply(master), True) + + # Construct a slave template with low version range + slave = xmlutil.SlaveTemplate(elem, 1, 2) + self.assertEqual(slave.apply(master), False) + + # Construct a slave template with high version range + slave = xmlutil.SlaveTemplate(elem, 4, 5) + self.assertEqual(slave.apply(master), False) + + # Construct a slave template with matching version range + slave = xmlutil.SlaveTemplate(elem, 3, 3) + self.assertEqual(slave.apply(master), True) + + def test__serialize(self): + # Our test object to serialize + obj = { + 'test': { + 'name': 'foobar', + 'values': [1, 2, 3, 4], + 'attrs': { + 'a': 1, + 'b': 2, + 'c': 3, + 'd': 4, + }, + 'image': { + 'name': 'image_foobar', + 'id': 42, + }, + }, + } + + # Set up our master template + root = xmlutil.TemplateElement('test', selector='test', + name='name') + value = xmlutil.SubTemplateElement(root, 'value', selector='values') + value.text = xmlutil.Selector() + attrs = xmlutil.SubTemplateElement(root, 'attrs', selector='attrs') + xmlutil.SubTemplateElement(attrs, 'attr', selector=xmlutil.get_items, + key=0, value=1) + master = xmlutil.MasterTemplate(root, 1, nsmap=dict(f='foo')) + + # Set up our slave template + root_slave = xmlutil.TemplateElement('test', selector='test') + image = xmlutil.SubTemplateElement(root_slave, 'image', + selector='image', id='id') + image.text = xmlutil.Selector('name') + slave = xmlutil.SlaveTemplate(root_slave, 1, nsmap=dict(b='bar')) + + # Attach the slave to the master... + master.attach(slave) + + # Try serializing our object + siblings = master._siblings() + nsmap = master._nsmap() + result = master._serialize(None, obj, siblings, nsmap) + + # Now we get to manually walk the element tree... + self.assertEqual(result.tag, 'test') + self.assertEqual(len(result.nsmap), 2) + self.assertEqual(result.nsmap['f'], 'foo') + self.assertEqual(result.nsmap['b'], 'bar') + self.assertEqual(result.get('name'), obj['test']['name']) + for idx, val in enumerate(obj['test']['values']): + self.assertEqual(result[idx].tag, 'value') + self.assertEqual(result[idx].text, str(val)) + idx += 1 + self.assertEqual(result[idx].tag, 'attrs') + for attr in result[idx]: + self.assertEqual(attr.tag, 'attr') + self.assertEqual(attr.get('value'), + str(obj['test']['attrs'][attr.get('key')])) + idx += 1 + self.assertEqual(result[idx].tag, 'image') + self.assertEqual(result[idx].get('id'), + str(obj['test']['image']['id'])) + self.assertEqual(result[idx].text, obj['test']['image']['name']) + + templ = xmlutil.Template(None) + self.assertEqual(templ.serialize(None), '') + + def test_serialize_with_colon_tagname_support(self): + # Our test object to serialize + obj = {'extra_specs': {'foo:bar': '999'}} + expected_xml = (("<?xml version='1.0' encoding='UTF-8'?>\n" + '<extra_specs><foo:bar xmlns:foo="foo">999</foo:bar>' + '</extra_specs>')) + # Set up our master template + root = xmlutil.TemplateElement('extra_specs', selector='extra_specs', + colon_ns=True) + value = xmlutil.SubTemplateElement(root, 'foo:bar', selector='foo:bar', + colon_ns=True) + value.text = xmlutil.Selector() + master = xmlutil.MasterTemplate(root, 1) + result = master.serialize(obj) + self.assertEqual(expected_xml, result) + + def test__serialize_with_empty_datum_selector(self): + # Our test object to serialize + obj = { + 'test': { + 'name': 'foobar', + 'image': '' + }, + } + + root = xmlutil.TemplateElement('test', selector='test', + name='name') + master = xmlutil.MasterTemplate(root, 1) + root_slave = xmlutil.TemplateElement('test', selector='test') + image = xmlutil.SubTemplateElement(root_slave, 'image', + selector='image') + image.set('id') + xmlutil.make_links(image, 'links') + slave = xmlutil.SlaveTemplate(root_slave, 1) + master.attach(slave) + + siblings = master._siblings() + result = master._serialize(None, obj, siblings) + self.assertEqual(result.tag, 'test') + self.assertEqual(result[0].tag, 'image') + self.assertEqual(result[0].get('id'), str(obj['test']['image'])) + + +class MasterTemplateBuilder(xmlutil.TemplateBuilder): + def construct(self): + elem = xmlutil.TemplateElement('test') + return xmlutil.MasterTemplate(elem, 1) + + +class SlaveTemplateBuilder(xmlutil.TemplateBuilder): + def construct(self): + elem = xmlutil.TemplateElement('test') + return xmlutil.SlaveTemplate(elem, 1) + + +class TemplateBuilderTest(test.NoDBTestCase): + def test_master_template_builder(self): + # Make sure the template hasn't been built yet + self.assertIsNone(MasterTemplateBuilder._tmpl) + + # Now, construct the template + tmpl1 = MasterTemplateBuilder() + + # Make sure that there is a template cached... + self.assertIsNotNone(MasterTemplateBuilder._tmpl) + + # Make sure it wasn't what was returned... + self.assertNotEqual(MasterTemplateBuilder._tmpl, tmpl1) + + # Make sure it doesn't get rebuilt + cached = MasterTemplateBuilder._tmpl + tmpl2 = MasterTemplateBuilder() + self.assertEqual(MasterTemplateBuilder._tmpl, cached) + + # Make sure we're always getting fresh copies + self.assertNotEqual(tmpl1, tmpl2) + + # Make sure we can override the copying behavior + tmpl3 = MasterTemplateBuilder(False) + self.assertEqual(MasterTemplateBuilder._tmpl, tmpl3) + + def test_slave_template_builder(self): + # Make sure the template hasn't been built yet + self.assertIsNone(SlaveTemplateBuilder._tmpl) + + # Now, construct the template + tmpl1 = SlaveTemplateBuilder() + + # Make sure there is a template cached... + self.assertIsNotNone(SlaveTemplateBuilder._tmpl) + + # Make sure it was what was returned... + self.assertEqual(SlaveTemplateBuilder._tmpl, tmpl1) + + # Make sure it doesn't get rebuilt + tmpl2 = SlaveTemplateBuilder() + self.assertEqual(SlaveTemplateBuilder._tmpl, tmpl1) + + # Make sure we're always getting the cached copy + self.assertEqual(tmpl1, tmpl2) + + +class MiscellaneousXMLUtilTests(test.NoDBTestCase): + def test_validate_schema(self): + xml = '''<?xml version='1.0' encoding='UTF-8'?> +<metadata xmlns="http://docs.openstack.org/compute/api/v1.1"> +<meta key="key6">value6</meta><meta key="key4">value4</meta> +</metadata> +''' + xmlutil.validate_schema(xml, 'metadata') + # No way to test the return value of validate_schema. + # It just raises an exception when something is wrong. + self.assertTrue(True) + + def test_make_links(self): + elem = xmlutil.TemplateElement('image', selector='image') + self.assertTrue(repr(xmlutil.make_links(elem, 'links'))) + + def test_make_flat_dict(self): + expected_xml = ("<?xml version='1.0' encoding='UTF-8'?>\n" + '<wrapper><a>foo</a><b>bar</b></wrapper>') + root = xmlutil.make_flat_dict('wrapper') + tmpl = xmlutil.MasterTemplate(root, 1) + result = tmpl.serialize(dict(wrapper=dict(a='foo', b='bar'))) + self.assertEqual(result, expected_xml) + + expected_xml = ("<?xml version='1.0' encoding='UTF-8'?>\n" +'<ns0:wrapper xmlns:ns0="ns"><ns0:a>foo</ns0:a><ns0:b>bar</ns0:b>' +"</ns0:wrapper>") + root = xmlutil.make_flat_dict('wrapper', ns='ns') + tmpl = xmlutil.MasterTemplate(root, 1) + result = tmpl.serialize(dict(wrapper=dict(a='foo', b='bar'))) + self.assertEqual(result, expected_xml) + + def test_make_flat_dict_with_colon_tagname_support(self): + # Our test object to serialize + obj = {'extra_specs': {'foo:bar': '999'}} + expected_xml = (("<?xml version='1.0' encoding='UTF-8'?>\n" + '<extra_specs><foo:bar xmlns:foo="foo">999</foo:bar>' + '</extra_specs>')) + # Set up our master template + root = xmlutil.make_flat_dict('extra_specs', colon_ns=True) + master = xmlutil.MasterTemplate(root, 1) + result = master.serialize(obj) + self.assertEqual(expected_xml, result) + + def test_make_flat_dict_with_parent(self): + # Our test object to serialize + obj = {"device": {"id": 1, + "extra_info": {"key1": "value1", + "key2": "value2"}}} + + expected_xml = (("<?xml version='1.0' encoding='UTF-8'?>\n" + '<device id="1"><extra_info><key2>value2</key2>' + '<key1>value1</key1></extra_info></device>')) + + root = xmlutil.TemplateElement('device', selector='device') + root.set('id') + extra = xmlutil.make_flat_dict('extra_info', root=root) + root.append(extra) + master = xmlutil.MasterTemplate(root, 1) + result = master.serialize(obj) + self.assertEqual(expected_xml, result) + + def test_make_flat_dict_with_dicts(self): + # Our test object to serialize + obj = {"device": {"id": 1, + "extra_info": {"key1": "value1", + "key2": "value2"}}} + + expected_xml = (("<?xml version='1.0' encoding='UTF-8'?>\n" + '<device><id>1</id><extra_info><key2>value2</key2>' + '<key1>value1</key1></extra_info></device>')) + + root = xmlutil.make_flat_dict('device', selector='device', + ignore_sub_dicts=True) + extra = xmlutil.make_flat_dict('extra_info', selector='extra_info') + root.append(extra) + master = xmlutil.MasterTemplate(root, 1) + result = master.serialize(obj) + self.assertEqual(expected_xml, result) + + def test_safe_parse_xml(self): + + normal_body = ('<?xml version="1.0" ?>' + '<foo><bar><v1>hey</v1><v2>there</v2></bar></foo>') + + dom = xmlutil.safe_minidom_parse_string(normal_body) + # Some versions of minidom inject extra newlines so we ignore them + result = str(dom.toxml()).replace('\n', '') + self.assertEqual(normal_body, result) + + self.assertRaises(exception.MalformedRequestBody, + xmlutil.safe_minidom_parse_string, + tests_utils.killer_xml_body()) + + +class SafeParserTestCase(test.NoDBTestCase): + def test_external_dtd(self): + xml_string = ("""<?xml version="1.0" encoding="utf-8"?> + <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + <html> + <head/> + <body>html with dtd</body> + </html>""") + + parser = xmlutil.ProtectedExpatParser(forbid_dtd=False, + forbid_entities=True) + self.assertRaises(ValueError, + minidom.parseString, + xml_string, parser) + + def test_external_file(self): + xml_string = """<!DOCTYPE external [ + <!ENTITY ee SYSTEM "file:///PATH/TO/root.xml"> + ]> + <root>ⅇ</root>""" + + parser = xmlutil.ProtectedExpatParser(forbid_dtd=False, + forbid_entities=True) + self.assertRaises(ValueError, + minidom.parseString, + xml_string, parser) + + def test_notation(self): + xml_string = """<?xml version="1.0" standalone="no"?> + <!-- comment data --> + <!DOCTYPE x [ + <!NOTATION notation SYSTEM "notation.jpeg"> + ]> + <root attr1="value1"> + </root>""" + + parser = xmlutil.ProtectedExpatParser(forbid_dtd=False, + forbid_entities=True) + self.assertRaises(ValueError, + minidom.parseString, + xml_string, parser) |