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
This commit is contained in:
Jim Rollenhagen 2017-01-11 16:59:13 +00:00
parent da6b0f2aad
commit 3c45f2fd1b
6 changed files with 164 additions and 15 deletions

View File

@ -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(

View File

@ -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).

View File

@ -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)

View File

@ -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,

View File

@ -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')

View File

@ -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.