diff --git a/ironic/common/hash_ring.py b/ironic/common/hash_ring.py index 95cfbdef30..61e1b385f0 100644 --- a/ironic/common/hash_ring.py +++ b/ironic/common/hash_ring.py @@ -50,6 +50,7 @@ class HashRingManager(object): def _load_hash_rings(self): rings = {} d2c = self.dbapi.get_active_driver_dict() + d2c.update(self.dbapi.get_active_hardware_type_dict()) for driver_name, hosts in d2c.items(): rings[driver_name] = hashring.HashRing( diff --git a/ironic/db/api.py b/ironic/db/api.py index d9f16e95ae..92357f6cd8 100644 --- a/ironic/db/api.py +++ b/ironic/db/api.py @@ -530,6 +530,19 @@ class Connection(object): driverB: set([host2, host3])} """ + @abc.abstractmethod + def get_active_hardware_type_dict(self): + """Retrieve hardware types for the registered and active conductors. + + :returns: A dict which maps hardware type names to the set of hosts + which support them. For example: + + :: + + {hardware-type-a: set([host1, host2]), + hardware-type-b: set([host2, host3])} + """ + @abc.abstractmethod def get_offline_conductors(self): """Get a list conductor hostnames that are offline (dead). diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py index da7c6f5252..69ddbca942 100644 --- a/ironic/db/sqlalchemy/api.py +++ b/ironic/db/sqlalchemy/api.py @@ -177,6 +177,16 @@ def _paginate_query(model, limit=None, marker=None, sort_key=None, return query.all() +def _filter_active_conductors(query, interval=None): + if interval is None: + interval = CONF.conductor.heartbeat_timeout + limit = timeutils.utcnow() - datetime.timedelta(seconds=interval) + + query = (query.filter(models.Conductor.online.is_(True)) + .filter(models.Conductor.updated_at >= limit)) + return query + + class Connection(api.Connection): """SqlAlchemy connection.""" @@ -782,11 +792,8 @@ class Connection(api.Connection): if interval is None: interval = CONF.conductor.heartbeat_timeout - limit = timeutils.utcnow() - datetime.timedelta(seconds=interval) - result = (model_query(models.Conductor) - .filter_by(online=True) - .filter(models.Conductor.updated_at >= limit) - .all()) + query = model_query(models.Conductor) + result = _filter_active_conductors(query, interval=interval) # build mapping of drivers to the set of hosts which support them d2c = collections.defaultdict(set) @@ -795,6 +802,17 @@ class Connection(api.Connection): d2c[driver].add(row['hostname']) return d2c + def get_active_hardware_type_dict(self): + query = (model_query(models.ConductorHardwareInterfaces, + models.Conductor) + .join(models.Conductor)) + result = _filter_active_conductors(query) + + d2c = collections.defaultdict(set) + for iface_row, cdr_row in result: + d2c[iface_row['hardware_type']].add(cdr_row['hostname']) + return d2c + def get_offline_conductors(self): interval = CONF.conductor.heartbeat_timeout limit = timeutils.utcnow() - datetime.timedelta(seconds=interval) diff --git a/ironic/tests/unit/common/test_hash_ring.py b/ironic/tests/unit/common/test_hash_ring.py index 798980fd79..94cfbb56a6 100644 --- a/ironic/tests/unit/common/test_hash_ring.py +++ b/ironic/tests/unit/common/test_hash_ring.py @@ -31,20 +31,28 @@ class HashRingManagerTestCase(db_base.DbTestCase): self.ring_manager = hash_ring.HashRingManager() def register_conductors(self): - self.dbapi.register_conductor({ + c1 = self.dbapi.register_conductor({ 'hostname': 'host1', 'drivers': ['driver1', 'driver2'], }) - self.dbapi.register_conductor({ + c2 = self.dbapi.register_conductor({ 'hostname': 'host2', 'drivers': ['driver1'], }) + for c in (c1, c2): + self.dbapi.register_conductor_hardware_interfaces( + c.id, 'hardware-type', 'deploy', ['iscsi', 'direct'], 'iscsi') def test_hash_ring_manager_get_ring_success(self): self.register_conductors() ring = self.ring_manager['driver1'] self.assertEqual(sorted(['host1', 'host2']), sorted(ring.nodes)) + def test_hash_ring_manager_hardware_type_success(self): + self.register_conductors() + ring = self.ring_manager['hardware-type'] + self.assertEqual(sorted(['host1', 'host2']), sorted(ring.nodes)) + def test_hash_ring_manager_driver_not_found(self): self.register_conductors() self.assertRaises(exception.DriverNotFound, diff --git a/ironic/tests/unit/db/test_conductor.py b/ironic/tests/unit/db/test_conductor.py index 32e055e826..76b065da51 100644 --- a/ironic/tests/unit/db/test_conductor.py +++ b/ironic/tests/unit/db/test_conductor.py @@ -40,9 +40,16 @@ class DbConductorTestCase(base.DbTestCase): self.dbapi.register_conductor(c) self.dbapi.register_conductor(c, update_existing=True) - def _create_test_cdr(self, **kwargs): + def _create_test_cdr(self, hardware_types=None, **kwargs): + hardware_types = hardware_types or [] c = utils.get_test_conductor(**kwargs) - return self.dbapi.register_conductor(c) + cdr = self.dbapi.register_conductor(c) + for ht in hardware_types: + self.dbapi.register_conductor_hardware_interfaces(cdr.id, ht, + 'power', + ['ipmi', 'fake'], + 'ipmi') + return cdr def test_register_conductor_hardware_interfaces(self): c = self._create_test_cdr() @@ -187,7 +194,7 @@ class DbConductorTestCase(base.DbTestCase): def test_get_active_driver_dict_one_host_one_driver(self, mock_utcnow): h = 'fake-host' d = 'fake-driver' - expected = {d: set([h])} + expected = {d: {h}} mock_utcnow.return_value = datetime.datetime.utcnow() self._create_test_cdr(hostname=h, drivers=[d]) @@ -199,7 +206,7 @@ class DbConductorTestCase(base.DbTestCase): h = 'fake-host' d1 = 'driver-one' d2 = 'driver-two' - expected = {d1: set([h]), d2: set([h])} + expected = {d1: {h}, d2: {h}} mock_utcnow.return_value = datetime.datetime.utcnow() self._create_test_cdr(hostname=h, drivers=[d1, d2]) @@ -211,7 +218,7 @@ class DbConductorTestCase(base.DbTestCase): h1 = 'host-one' h2 = 'host-two' d = 'fake-driver' - expected = {d: set([h1, h2])} + expected = {d: {h1, h2}} mock_utcnow.return_value = datetime.datetime.utcnow() self._create_test_cdr(id=1, hostname=h1, drivers=[d]) @@ -226,7 +233,7 @@ class DbConductorTestCase(base.DbTestCase): h3 = 'host-three' d1 = 'driver-one' d2 = 'driver-two' - expected = {d1: set([h1, h2]), d2: set([h2, h3])} + expected = {d1: {h1, h2}, d2: {h2, h3}} mock_utcnow.return_value = datetime.datetime.utcnow() self._create_test_cdr(id=1, hostname=h1, drivers=[d1]) @@ -254,16 +261,114 @@ class DbConductorTestCase(base.DbTestCase): # verify that old-host does not show up in current list one_minute = 60 - expected = {d: set([h2]), d2: set([h2])} + expected = {d: {h2}, d2: {h2}} result = self.dbapi.get_active_driver_dict(interval=one_minute) self.assertEqual(expected, result) # change the interval, and verify that old-host appears two_minute = one_minute * 2 - expected = {d: set([h1, h2]), d1: set([h1]), d2: set([h2])} + expected = {d: {h1, h2}, d1: {h1}, d2: {h2}} result = self.dbapi.get_active_driver_dict(interval=two_minute) self.assertEqual(expected, result) + @mock.patch.object(timeutils, 'utcnow', autospec=True) + def test_get_active_hardware_type_dict_one_host_no_ht(self, mock_utcnow): + h = 'fake-host' + expected = {} + + mock_utcnow.return_value = datetime.datetime.utcnow() + self._create_test_cdr(hostname=h, drivers=[], hardware_types=[]) + result = self.dbapi.get_active_hardware_type_dict() + self.assertEqual(expected, result) + + @mock.patch.object(timeutils, 'utcnow', autospec=True) + def test_get_active_hardware_type_dict_one_host_one_ht(self, mock_utcnow): + h = 'fake-host' + ht = 'hardware-type' + expected = {ht: {h}} + + mock_utcnow.return_value = datetime.datetime.utcnow() + self._create_test_cdr(hostname=h, drivers=[], hardware_types=[ht]) + result = self.dbapi.get_active_hardware_type_dict() + self.assertEqual(expected, result) + + @mock.patch.object(timeutils, 'utcnow', autospec=True) + def test_get_active_hardware_type_dict_one_host_many_ht(self, mock_utcnow): + h = 'fake-host' + ht1 = 'hardware-type' + ht2 = 'another-hardware-type' + expected = {ht1: {h}, ht2: {h}} + + mock_utcnow.return_value = datetime.datetime.utcnow() + self._create_test_cdr(hostname=h, drivers=[], + hardware_types=[ht1, ht2]) + result = self.dbapi.get_active_hardware_type_dict() + self.assertEqual(expected, result) + + @mock.patch.object(timeutils, 'utcnow', autospec=True) + def test_get_active_hardware_type_dict_many_host_one_ht(self, mock_utcnow): + h1 = 'host-one' + h2 = 'host-two' + ht = 'hardware-type' + expected = {ht: {h1, h2}} + + mock_utcnow.return_value = datetime.datetime.utcnow() + self._create_test_cdr(id=1, hostname=h1, drivers=[], + hardware_types=[ht]) + self._create_test_cdr(id=2, hostname=h2, drivers=[], + hardware_types=[ht]) + result = self.dbapi.get_active_hardware_type_dict() + self.assertEqual(expected, result) + + @mock.patch.object(timeutils, 'utcnow', autospec=True) + def test_get_active_hardware_type_dict_many_host_many_ht(self, + mock_utcnow): + h1 = 'host-one' + h2 = 'host-two' + ht1 = 'hardware-type' + ht2 = 'another-hardware-type' + expected = {ht1: {h1, h2}, ht2: {h1, h2}} + + mock_utcnow.return_value = datetime.datetime.utcnow() + self._create_test_cdr(id=1, hostname=h1, drivers=[], + hardware_types=[ht1, ht2]) + self._create_test_cdr(id=2, hostname=h2, drivers=[], + hardware_types=[ht1, ht2]) + result = self.dbapi.get_active_hardware_type_dict() + self.assertEqual(expected, result) + + @mock.patch.object(timeutils, 'utcnow', autospec=True) + def test_get_active_hardware_type_dict_with_old_conductor(self, + mock_utcnow): + past = datetime.datetime(2000, 1, 1, 0, 0) + present = past + datetime.timedelta(minutes=2) + + ht = 'hardware-type' + + h1 = 'old-host' + ht1 = 'old-hardware-type' + mock_utcnow.return_value = past + self._create_test_cdr(id=1, hostname=h1, drivers=[], + hardware_types=[ht, ht1]) + + h2 = 'new-host' + ht2 = 'new-hardware-type' + mock_utcnow.return_value = present + self._create_test_cdr(id=2, hostname=h2, drivers=[], + hardware_types=[ht, ht2]) + + # verify that old-host does not show up in current list + self.config(heartbeat_timeout=60, group='conductor') + expected = {ht: {h2}, ht2: {h2}} + result = self.dbapi.get_active_hardware_type_dict() + self.assertEqual(expected, result) + + # change the heartbeat timeout, and verify that old-host appears + self.config(heartbeat_timeout=120, group='conductor') + expected = {ht: {h1, h2}, ht1: {h1}, ht2: {h2}} + result = self.dbapi.get_active_hardware_type_dict() + self.assertEqual(expected, result) + @mock.patch.object(timeutils, 'utcnow', autospec=True) def test_get_offline_conductors(self, mock_utcnow): self.config(heartbeat_timeout=60, group='conductor') diff --git a/releasenotes/notes/hardware-types-in-hash-ring-d3f097e332c4d395.yaml b/releasenotes/notes/hardware-types-in-hash-ring-d3f097e332c4d395.yaml new file mode 100644 index 0000000000..23624507de --- /dev/null +++ b/releasenotes/notes/hardware-types-in-hash-ring-d3f097e332c4d395.yaml @@ -0,0 +1,4 @@ +--- +features: + - Using a dynamic driver in a node's driver field is now possible, though + customizing the interfaces is not yet exposed in the REST API.