From 3c45f2fd1b8ca90ecc02ec32206eadab0333c7a1 Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Wed, 11 Jan 2017 16:59:13 +0000 Subject: [PATCH] Add hardware types to the hash ring This loads hardware types into the hash ring, along with the drivers that are currently there. This allows us to make RPC calls for nodes that are using a dynamic driver. Adds a dbapi method `get_active_hardware_type_dict`, similar to `get_active_driver_dict`, to get a mapping of hardware types to active conductors. Change-Id: I50c1428568bd8cbe4ef252d56a6278f1a54dfcdb Partial-Bug: #1524745 --- ironic/common/hash_ring.py | 1 + ironic/db/api.py | 13 ++ ironic/db/sqlalchemy/api.py | 28 +++- ironic/tests/unit/common/test_hash_ring.py | 12 +- ironic/tests/unit/db/test_conductor.py | 121 ++++++++++++++++-- ...e-types-in-hash-ring-d3f097e332c4d395.yaml | 4 + 6 files changed, 164 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/hardware-types-in-hash-ring-d3f097e332c4d395.yaml 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.