summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJim Rollenhagen <jim@jimrollenhagen.com>2017-01-11 16:59:13 +0000
committerJim Rollenhagen <jim@jimrollenhagen.com>2017-01-19 20:35:05 +0000
commit3c45f2fd1b8ca90ecc02ec32206eadab0333c7a1 (patch)
tree4d07d0e1c674c0b6d8e89ca9eacb924f7b88d618
parentda6b0f2aad7b38b18c1886e30d9517ded66297d1 (diff)
downloadironic-3c45f2fd1b8ca90ecc02ec32206eadab0333c7a1.tar.gz
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
-rw-r--r--ironic/common/hash_ring.py1
-rw-r--r--ironic/db/api.py13
-rw-r--r--ironic/db/sqlalchemy/api.py28
-rw-r--r--ironic/tests/unit/common/test_hash_ring.py12
-rw-r--r--ironic/tests/unit/db/test_conductor.py121
-rw-r--r--releasenotes/notes/hardware-types-in-hash-ring-d3f097e332c4d395.yaml4
6 files changed, 164 insertions, 15 deletions
diff --git a/ironic/common/hash_ring.py b/ironic/common/hash_ring.py
index 95cfbdef3..61e1b385f 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 d9f16e95a..92357f6cd 100644
--- a/ironic/db/api.py
+++ b/ironic/db/api.py
@@ -531,6 +531,19 @@ class Connection(object):
"""
@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 da7c6f525..69ddbca94 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 798980fd7..94cfbb56a 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 32e055e82..76b065da5 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,17 +261,115 @@ 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')
time_ = datetime.datetime(2000, 1, 1, 0, 0)
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 000000000..23624507d
--- /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.