diff --git a/nova/exception.py b/nova/exception.py index 518a95a62..91a377c1e 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -929,6 +929,11 @@ class QuotaError(NovaException): message = _("Quota exceeded") + ": code=%(code)s" +class AggregateError(NovaException): + message = _("Aggregate %(aggregate_id)s: action '%(action)s' " + "caused an error: %(reason)s.") + + class AggregateNotFound(NotFound): message = _("Aggregate %(aggregate_id)s could not be found.") diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index d6d54227f..5ee74df8e 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -3153,6 +3153,8 @@ def _create_service_entries(context, values={'avail_zone1': ['fake_host1', class ComputeAPIAggrTestCase(test.TestCase): + """This is for unit coverage of aggregate-related methods + defined in nova.compute.api.""" def setUp(self): super(ComputeAPIAggrTestCase, self).setUp() @@ -3164,9 +3166,16 @@ class ComputeAPIAggrTestCase(test.TestCase): def tearDown(self): super(ComputeAPIAggrTestCase, self).tearDown() + def test_create_invalid_availability_zone(self): + """Ensure InvalidAggregateAction is raised with wrong avail_zone.""" + self.assertRaises(exception.InvalidAggregateAction, + self.api.create_aggregate, + self.context, 'fake_aggr', 'fake_avail_zone') + def test_update_aggregate_metadata(self): + _create_service_entries(self.context, {'fake_zone': ['fake_host']}) aggr = self.api.create_aggregate(self.context, 'fake_aggregate', - 'fake_availability_zone') + 'fake_zone') metadata = {'foo_key1': 'foo_value1', 'foo_key2': 'foo_value2', } aggr = self.api.update_aggregate_metadata(self.context, aggr['id'], @@ -3178,8 +3187,9 @@ class ComputeAPIAggrTestCase(test.TestCase): def test_delete_aggregate(self): """Ensure we can delete an aggregate.""" + _create_service_entries(self.context, {'fake_zone': ['fake_host']}) aggr = self.api.create_aggregate(self.context, 'fake_aggregate', - 'fake_availability_zone') + 'fake_zone') self.api.delete_aggregate(self.context, aggr['id']) expected = db.aggregate_get(self.context, aggr['id'], read_deleted='yes') @@ -3188,10 +3198,10 @@ class ComputeAPIAggrTestCase(test.TestCase): def test_delete_non_empty_aggregate(self): """Ensure InvalidAggregateAction is raised when non empty aggregate.""" - aggr = self.api.create_aggregate(self.context, 'fake_aggregate', - 'fake_availability_zone') _create_service_entries(self.context, {'fake_availability_zone': ['fake_host']}) + aggr = self.api.create_aggregate(self.context, 'fake_aggregate', + 'fake_availability_zone') self.api.add_host_to_aggregate(self.context, aggr['id'], 'fake_host') self.assertRaises(exception.InvalidAggregateAction, self.api.delete_aggregate, self.context, aggr['id']) @@ -3242,9 +3252,9 @@ class ComputeAPIAggrTestCase(test.TestCase): def test_add_host_to_aggregate_invalid_dismissed_status(self): """Ensure InvalidAggregateAction is raised when aggregate is deleted.""" + _create_service_entries(self.context, {'fake_zone': ['fake_host']}) aggr = self.api.create_aggregate(self.context, 'fake_aggregate', 'fake_zone') - _create_service_entries(self.context, {'fake_zone': ['fake_host']}) # let's mock the fact that the aggregate is dismissed! status = {'operational_state': aggregate_states.DISMISSED} db.aggregate_update(self.context, aggr['id'], status) @@ -3255,9 +3265,9 @@ class ComputeAPIAggrTestCase(test.TestCase): def test_add_host_to_aggregate_invalid_error_status(self): """Ensure InvalidAggregateAction is raised when aggregate is in error.""" + _create_service_entries(self.context, {'fake_zone': ['fake_host']}) aggr = self.api.create_aggregate(self.context, 'fake_aggregate', 'fake_zone') - _create_service_entries(self.context, {'fake_zone': ['fake_host']}) # let's mock the fact that the aggregate is in error! status = {'operational_state': aggregate_states.ERROR} db.aggregate_update(self.context, aggr['id'], status) @@ -3267,17 +3277,19 @@ class ComputeAPIAggrTestCase(test.TestCase): def test_add_host_to_aggregate_zones_mismatch(self): """Ensure InvalidAggregateAction is raised when zones don't match.""" - _create_service_entries(self.context, {'fake_zoneX': ['fake_host']}) + _create_service_entries(self.context, {'fake_zoneX': ['fake_host1'], + 'fake_zoneY': ['fake_host2']}) aggr = self.api.create_aggregate(self.context, 'fake_aggregate', 'fake_zoneY') self.assertRaises(exception.InvalidAggregateAction, self.api.add_host_to_aggregate, - self.context, aggr['id'], 'fake_host') + self.context, aggr['id'], 'fake_host1') def test_add_host_to_aggregate_raise_not_found(self): """Ensure ComputeHostNotFound is raised when adding invalid host.""" + _create_service_entries(self.context, {'fake_zone': ['fake_host']}) aggr = self.api.create_aggregate(self.context, 'fake_aggregate', - 'fake_availability_zone') + 'fake_zone') self.assertRaises(exception.ComputeHostNotFound, self.api.add_host_to_aggregate, self.context, aggr['id'], 'invalid_host') @@ -3325,9 +3337,9 @@ class ComputeAPIAggrTestCase(test.TestCase): def test_remove_host_from_aggregate_invalid_dismissed_status(self): """Ensure InvalidAggregateAction is raised when aggregate is deleted.""" + _create_service_entries(self.context, {'fake_zone': ['fake_host']}) aggr = self.api.create_aggregate(self.context, 'fake_aggregate', 'fake_zone') - _create_service_entries(self.context, {'fake_zone': ['fake_host']}) # let's mock the fact that the aggregate is dismissed! status = {'operational_state': aggregate_states.DISMISSED} db.aggregate_update(self.context, aggr['id'], status) @@ -3338,9 +3350,9 @@ class ComputeAPIAggrTestCase(test.TestCase): def test_remove_host_from_aggregate_invalid_changing_status(self): """Ensure InvalidAggregateAction is raised when aggregate is changing.""" + _create_service_entries(self.context, {'fake_zone': ['fake_host']}) aggr = self.api.create_aggregate(self.context, 'fake_aggregate', 'fake_zone') - _create_service_entries(self.context, {'fake_zone': ['fake_host']}) # let's mock the fact that the aggregate is changing! status = {'operational_state': aggregate_states.CHANGING} db.aggregate_update(self.context, aggr['id'], status) @@ -3350,13 +3362,85 @@ class ComputeAPIAggrTestCase(test.TestCase): def test_remove_host_from_aggregate_raise_not_found(self): """Ensure ComputeHostNotFound is raised when removing invalid host.""" + _create_service_entries(self.context, {'fake_zone': ['fake_host']}) aggr = self.api.create_aggregate(self.context, 'fake_aggregate', - 'fake_availability_zone') + 'fake_zone') self.assertRaises(exception.ComputeHostNotFound, self.api.remove_host_from_aggregate, self.context, aggr['id'], 'invalid_host') +class ComputeAggrTestCase(BaseTestCase): + """This is for unit coverage of aggregate-related methods + defined in nova.compute.manager.""" + + def setUp(self): + super(ComputeAggrTestCase, self).setUp() + self.context = context.get_admin_context() + values = {'name': 'test_aggr', + 'availability_zone': 'test_zone', } + self.aggr = db.aggregate_create(self.context, values) + + def tearDown(self): + super(ComputeAggrTestCase, self).tearDown() + + def test_add_aggregate_host(self): + def fake_driver_add_to_aggregate(context, aggregate, host): + fake_driver_add_to_aggregate.called = True + return {"foo": "bar"} + self.stubs.Set(self.compute.driver, "add_to_aggregate", + fake_driver_add_to_aggregate) + + self.compute.add_aggregate_host(self.context, self.aggr.id, "host") + self.assertTrue(fake_driver_add_to_aggregate.called) + + def test_add_aggregate_host_raise_err(self): + """Ensure the undo operation works correctly on add.""" + def fake_driver_add_to_aggregate(context, aggregate, host): + raise exception.AggregateError + self.stubs.Set(self.compute.driver, "add_to_aggregate", + fake_driver_add_to_aggregate) + + state = {'operational_state': aggregate_states.ACTIVE} + db.aggregate_update(self.context, self.aggr.id, state) + db.aggregate_host_add(self.context, self.aggr.id, 'fake_host') + + self.assertRaises(exception.AggregateError, + self.compute.add_aggregate_host, + self.context, self.aggr.id, "fake_host") + excepted = db.aggregate_get(self.context, self.aggr.id) + self.assertEqual(excepted.operational_state, aggregate_states.ERROR) + self.assertEqual(excepted.hosts, []) + + def test_remove_aggregate_host(self): + def fake_driver_remove_from_aggregate(context, aggregate, host): + fake_driver_remove_from_aggregate.called = True + self.assertEqual("host", host, "host") + return {"foo": "bar"} + self.stubs.Set(self.compute.driver, "remove_from_aggregate", + fake_driver_remove_from_aggregate) + + self.compute.remove_aggregate_host(self.context, self.aggr.id, "host") + self.assertTrue(fake_driver_remove_from_aggregate.called) + + def test_remove_aggregate_host_raise_err(self): + """Ensure the undo operation works correctly on remove.""" + def fake_driver_remove_from_aggregate(context, aggregate, host): + raise exception.AggregateError + self.stubs.Set(self.compute.driver, "remove_from_aggregate", + fake_driver_remove_from_aggregate) + + state = {'operational_state': aggregate_states.ACTIVE} + db.aggregate_update(self.context, self.aggr.id, state) + + self.assertRaises(exception.AggregateError, + self.compute.remove_aggregate_host, + self.context, self.aggr.id, "fake_host") + excepted = db.aggregate_get(self.context, self.aggr.id) + self.assertEqual(excepted.operational_state, aggregate_states.ERROR) + self.assertEqual(excepted.hosts, ['fake_host']) + + class ComputePolicyTestCase(BaseTestCase): def setUp(self): diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py index 3353ea737..4cb17d958 100644 --- a/nova/tests/test_db_api.py +++ b/nova/tests/test_db_api.py @@ -267,6 +267,30 @@ class DbApiTestCase(test.TestCase): expected = {uuids[0]: [], uuids[1]: []} self.assertEqual(expected, instance_faults) + def test_dns_registration(self): + domain1 = 'test.domain.one' + domain2 = 'test.domain.two' + testzone = 'testzone' + ctxt = context.get_admin_context() + + db.dnsdomain_register_for_zone(ctxt, domain1, testzone) + domain_ref = db.dnsdomain_get(ctxt, domain1) + zone = domain_ref.availability_zone + scope = domain_ref.scope + self.assertEqual(scope, 'private') + self.assertEqual(zone, testzone) + + db.dnsdomain_register_for_project(ctxt, domain2, + self.project_id) + domain_ref = db.dnsdomain_get(ctxt, domain2) + project = domain_ref.project_id + scope = domain_ref.scope + self.assertEqual(project, self.project_id) + self.assertEqual(scope, 'public') + + db.dnsdomain_unregister(ctxt, domain1) + db.dnsdomain_unregister(ctxt, domain2) + def _get_fake_aggr_values(): return {'name': 'fake_aggregate', @@ -351,6 +375,14 @@ class AggregateDBApiTestCase(test.TestCase): db.aggregate_create, self.context, _get_fake_aggr_values()) + def test_aggregate_get(self): + """Ensure we can get aggregate with all its relations.""" + ctxt = context.get_admin_context() + result = _create_aggregate_with_hosts(context=ctxt) + expected = db.aggregate_get(ctxt, result.id) + self.assertEqual(_get_fake_aggr_hosts(), expected.hosts) + self.assertEqual(_get_fake_aggr_metadata(), expected.metadetails) + def test_aggregate_delete_raise_not_found(self): """Ensure AggregateNotFound is raised when deleting an aggregate.""" ctxt = context.get_admin_context() @@ -541,30 +573,6 @@ class AggregateDBApiTestCase(test.TestCase): db.aggregate_host_delete, ctxt, result.id, _get_fake_aggr_hosts()[0]) - def test_dns_registration(self): - domain1 = 'test.domain.one' - domain2 = 'test.domain.two' - testzone = 'testzone' - ctxt = context.get_admin_context() - - db.dnsdomain_register_for_zone(ctxt, domain1, testzone) - domain_ref = db.dnsdomain_get(ctxt, domain1) - zone = domain_ref.availability_zone - scope = domain_ref.scope - self.assertEqual(scope, 'private') - self.assertEqual(zone, testzone) - - db.dnsdomain_register_for_project(ctxt, domain2, - self.project_id) - domain_ref = db.dnsdomain_get(ctxt, domain2) - project = domain_ref.project_id - scope = domain_ref.scope - self.assertEqual(project, self.project_id) - self.assertEqual(scope, 'public') - - db.dnsdomain_unregister(ctxt, domain1) - db.dnsdomain_unregister(ctxt, domain2) - class CapacityTestCase(test.TestCase): def setUp(self): diff --git a/nova/tests/test_virt_drivers.py b/nova/tests/test_virt_drivers.py index 41c5d118e..70d54db9a 100644 --- a/nova/tests/test_virt_drivers.py +++ b/nova/tests/test_virt_drivers.py @@ -401,6 +401,14 @@ class _VirtDriverTestCase(test.TestCase): def test_host_power_action_startup(self): self.connection.host_power_action('a useless argument?', 'startup') + @catch_notimplementederror + def test_add_to_aggregate(self): + self.connection.add_to_aggregate(self.ctxt, 'aggregate', 'host') + + @catch_notimplementederror + def test_remove_from_aggregate(self): + self.connection.remove_from_aggregate(self.ctxt, 'aggregate', 'host') + class AbstractDriverTestCase(_VirtDriverTestCase): def setUp(self): diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index d72365e5c..b08bbd69c 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -31,6 +31,7 @@ from nova import flags from nova import log as logging from nova import test from nova import utils +from nova.compute import aggregate_states from nova.compute import instance_types from nova.compute import power_state from nova import exception @@ -1741,3 +1742,149 @@ class XenAPISRSelectionTestCase(test.TestCase): expected = helper.safe_find_sr(session) self.assertEqual(session.call_xenapi('pool.get_default_SR', pool_ref), expected) + + +class XenAPIAggregateTestCase(test.TestCase): + """Unit tests for aggregate operations.""" + def setUp(self): + super(XenAPIAggregateTestCase, self).setUp() + self.stubs = stubout.StubOutForTesting() + self.flags(xenapi_connection_url='http://test_url', + xenapi_connection_username='test_user', + xenapi_connection_password='test_pass', + instance_name_template='%d', + firewall_driver='nova.virt.xenapi.firewall.' + 'Dom0IptablesFirewallDriver', + host='host') + xenapi_fake.reset() + stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests) + self.context = context.get_admin_context() + self.conn = xenapi_conn.get_connection(False) + self.fake_metadata = {'master_compute': 'host'} + + def tearDown(self): + super(XenAPIAggregateTestCase, self).tearDown() + self.stubs.UnsetAll() + + def test_add_to_aggregate_called(self): + def fake_add_to_aggregate(context, aggregate, host): + fake_add_to_aggregate.called = True + self.stubs.Set(self.conn._pool, + "add_to_aggregate", + fake_add_to_aggregate) + + self.conn.add_to_aggregate(None, None, None) + self.assertTrue(fake_add_to_aggregate.called) + + def test_add_to_aggregate_for_first_host_sets_metadata(self): + def fake_init_pool(id, name): + fake_init_pool.called = True + self.stubs.Set(self.conn._pool, "_init_pool", fake_init_pool) + + aggregate = self._aggregate_setup() + self.conn._pool.add_to_aggregate(self.context, aggregate, "host") + result = db.aggregate_get(self.context, aggregate.id) + self.assertTrue(fake_init_pool.called) + self.assertDictMatch(self.fake_metadata, result.metadetails) + self.assertEqual(aggregate_states.ACTIVE, result.operational_state) + + def test_join_slave(self): + """Ensure join_slave gets called when the request gets to master.""" + def fake_join_slave(id, compute_uuid, host, url, user, password): + fake_join_slave.called = True + self.stubs.Set(self.conn._pool, "_join_slave", fake_join_slave) + + aggregate = self._aggregate_setup(hosts=['host', 'host2'], + metadata=self.fake_metadata) + self.conn._pool.add_to_aggregate(self.context, aggregate, "host2", + compute_uuid='fake_uuid', + url='fake_url', + user='fake_user', + passwd='fake_pass', + xenhost_uuid='fake_uuid') + self.assertTrue(fake_join_slave.called) + + def test_add_to_aggregate_first_host(self): + def fake_pool_set_name_label(self, session, pool_ref, name): + fake_pool_set_name_label.called = True + self.stubs.Set(xenapi_fake.SessionBase, "pool_set_name_label", + fake_pool_set_name_label) + self.conn._session.call_xenapi("pool.create", {"name": "asdf"}) + + values = {"name": 'fake_aggregate', + "availability_zone": 'fake_zone'} + result = db.aggregate_create(self.context, values) + db.aggregate_host_add(self.context, result.id, "host") + aggregate = db.aggregate_get(self.context, result.id) + self.assertEqual(["host"], aggregate.hosts) + self.assertEqual({}, aggregate.metadetails) + + self.conn._pool.add_to_aggregate(self.context, aggregate, "host") + self.assertTrue(fake_pool_set_name_label.called) + + def test_remove_from_aggregate_called(self): + def fake_remove_from_aggregate(context, aggregate, host): + fake_remove_from_aggregate.called = True + self.stubs.Set(self.conn._pool, + "remove_from_aggregate", + fake_remove_from_aggregate) + + self.conn.remove_from_aggregate(None, None, None) + self.assertTrue(fake_remove_from_aggregate.called) + + def test_remove_from_empty_aggregate(self): + values = {"name": 'fake_aggregate', + "availability_zone": 'fake_zone'} + result = db.aggregate_create(self.context, values) + self.assertRaises(exception.AggregateError, + self.conn._pool.remove_from_aggregate, + None, result, "test_host") + + def test_remove_slave(self): + """Ensure eject slave gets called.""" + def fake_eject_slave(id, compute_uuid, host_uuid): + fake_eject_slave.called = True + self.stubs.Set(self.conn._pool, "_eject_slave", fake_eject_slave) + + self.fake_metadata['host2'] = 'fake_host2_uuid' + aggregate = self._aggregate_setup(hosts=['host', 'host2'], + metadata=self.fake_metadata) + self.conn._pool.remove_from_aggregate(self.context, aggregate, "host2") + self.assertTrue(fake_eject_slave.called) + + def test_remove_master_solo(self): + """Ensure metadata are cleared after removal.""" + def fake_clear_pool(id): + fake_clear_pool.called = True + self.stubs.Set(self.conn._pool, "_clear_pool", fake_clear_pool) + + aggregate = self._aggregate_setup(aggr_state=aggregate_states.ACTIVE, + metadata=self.fake_metadata) + self.conn._pool.remove_from_aggregate(self.context, aggregate, "host") + result = db.aggregate_get(self.context, aggregate.id) + self.assertTrue(fake_clear_pool.called) + self.assertDictMatch({}, result.metadetails) + self.assertEqual(aggregate_states.ACTIVE, result.operational_state) + + def test_remote_master_non_empty_pool(self): + """Ensure AggregateError is raised if removing the master.""" + aggregate = self._aggregate_setup(aggr_state=aggregate_states.ACTIVE, + hosts=['host', 'host2'], + metadata=self.fake_metadata) + self.assertRaises(exception.AggregateError, + self.conn._pool.remove_from_aggregate, + self.context, aggregate, "host") + + def _aggregate_setup(self, aggr_name='fake_aggregate', + aggr_zone='fake_zone', + aggr_state=aggregate_states.CREATED, + hosts=['host'], metadata=None): + values = {"name": aggr_name, + "availability_zone": aggr_zone, + "operational_state": aggr_state, } + result = db.aggregate_create(self.context, values) + for host in hosts: + db.aggregate_host_add(self.context, result.id, host) + if metadata: + db.aggregate_metadata_add(self.context, result.id, metadata) + return db.aggregate_get(self.context, result.id)