From 424f32f04d9c6c97f684782b35e1c25fbf83ce05 Mon Sep 17 00:00:00 2001 From: Armando Migliaccio Date: Wed, 1 Feb 2012 15:01:26 +0000 Subject: blueprint host-aggregates: xenapi implementation This commit introduces some clean-up/improvements on the current model and api for host aggregates. It also introduces a first version of the xenapi implementation. More precisely: - it lays out the structure of the virt driver, - it introduces compute and xenapi unit tests coverage, - it deals with join/eject of pool master and slaves, - it fixes xenapi_conn, when used in resource pool configurations More commits to follow (to ensure that VM placement, networking setup, performance metrics work just as well in cases where resource pools are present). However, these may be outside the scope of this blueprint and considered as ad-hoc bug fixes. Change-Id: Ib3cff71160264c5547e1c060d3fd566ad87337cb --- nova/tests/test_compute.py | 108 +++++++++++++++++++++++++---- nova/tests/test_db_api.py | 56 ++++++++------- nova/tests/test_virt_drivers.py | 8 +++ nova/tests/test_xenapi.py | 147 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 283 insertions(+), 36 deletions(-) (limited to 'nova/tests') 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) -- cgit