Fix AvailabilityZone check for hosts in multiple aggregates

Check for multiple AZ was distinct when adding an host to an aggregate
and when updating an aggregate (or its metadata), which was leading to
possible corner-cases where logics were different.

This patch proposes a single way for checking for both API methods and
also adds an extra control on the possibility to have an aggregate with
its metadata AZ set to the configuration default value (eg. 'nova') and
where it was possible to subsequently have one host in two disctinct AZs
Fixed that scenerio as it is a gap, Nova as of now preventing to have
one host in two distinct AZs.

Change-Id: Ida6aa507f34f36a436adaafe1f7609963c59a9bc
Partial-Bug: #1277230
This commit is contained in:
Sylvain Bauza 2014-04-08 11:00:40 +02:00
parent a5d18f2520
commit 0ad5a64dc9
2 changed files with 89 additions and 32 deletions

View File

@ -3300,32 +3300,14 @@ class AggregateAPI(base.Base):
aggregates = aggregate_obj.AggregateList.get_all(context)
return [self._reformat_aggregate_info(agg) for agg in aggregates]
def is_safe_to_update_az(self, context, aggregate, metadata,
action_name):
"""Determine if updates alter an aggregate's availability zone."""
if 'availability_zone' in metadata:
aggregate_az = aggregate.metadata.get("availability_zone")
for host in aggregate.hosts:
host_az = availability_zones.get_host_availability_zone(
context, host)
if (host_az and host_az != metadata["availability_zone"]
and host_az != CONF.default_availability_zone and
host_az != aggregate_az):
msg = _("This aggregate contains hosts in"
" an existing availability zone")
raise exception.InvalidAggregateAction(
action=action_name,
aggregate_id=aggregate.id,
reason=msg)
@wrap_exception()
def update_aggregate(self, context, aggregate_id, values):
"""Update the properties of an aggregate."""
aggregate = aggregate_obj.Aggregate.get_by_id(context, aggregate_id)
if 'name' in values:
aggregate.name = values.pop('name')
self.is_safe_to_update_az(context, aggregate,
values, "update aggregate")
self.is_safe_to_update_az(context, values, aggregate=aggregate,
action_name="update_aggregate")
if values:
aggregate.metadata = values
aggregate.save()
@ -3339,8 +3321,8 @@ class AggregateAPI(base.Base):
def update_aggregate_metadata(self, context, aggregate_id, metadata):
"""Updates the aggregate metadata."""
aggregate = aggregate_obj.Aggregate.get_by_id(context, aggregate_id)
self.is_safe_to_update_az(context, aggregate,
metadata, "update aggregate metadata")
self.is_safe_to_update_az(context, metadata, aggregate=aggregate,
action_name="update_aggregate_metadata")
aggregate.update_metadata(metadata)
# If updated metadata include availability_zones, then the cache
# which stored availability_zones and host need to be reset
@ -3366,19 +3348,59 @@ class AggregateAPI(base.Base):
"delete.end",
aggregate_payload)
def _check_az_for_host(self, aggregate_meta, host_az, aggregate_id):
def is_safe_to_update_az(self, context, metadata, aggregate,
hosts=None, action_name="add_host_to_aggregate"):
"""Determine if updates alter an aggregate's availability zone.
:param context: local context
:param metadata: Target metadata for updating aggregate
:param aggregate: Aggregate to update
:param hosts: Hosts to check. If None, aggregate.hosts is used
:type hosts: list
:action_name: Calling method for logging purposes
"""
if 'availability_zone' in metadata:
_hosts = hosts or aggregate.hosts
for host in _hosts:
host_az = availability_zones.get_host_availability_zone(
context, host)
if host_az == CONF.default_availability_zone:
# NOTE(sbauza): Aggregate with AZ set to default AZ can
# exist, we need to check
host_aggs = aggregate_obj.AggregateList.get_by_host(
context, host, key='availability_zone')
default_aggs = [agg for agg in host_aggs
if agg['metadata'].get(
'availability_zone'
) == CONF.default_availability_zone]
else:
default_aggs = []
if (host_az != aggregate.metadata.get('availability_zone') and
(host_az != CONF.default_availability_zone or
len(default_aggs) != 0)):
self._check_az_for_host(
metadata, host_az, aggregate.id,
action_name=action_name)
def _check_az_for_host(self, aggregate_meta, host_az, aggregate_id,
action_name="add_host_to_aggregate"):
# NOTE(mtreinish) The availability_zone key returns a set of
# zones so loop over each zone. However there should only
# ever be one zone in the set because an aggregate can only
# have a single availability zone set at one time.
for aggregate_az in aggregate_meta["availability_zone"]:
if isinstance(aggregate_meta["availability_zone"], six.string_types):
azs = set([aggregate_meta["availability_zone"]])
else:
azs = aggregate_meta["availability_zone"]
for aggregate_az in azs:
# NOTE(mtreinish) Ensure that the aggregate_az is not none
# if it is none then that is just a regular aggregate and
# it is valid to have a host in multiple aggregates.
if aggregate_az and aggregate_az != host_az:
msg = _("Host already in availability zone "
"%s") % host_az
action_name = "add_host_to_aggregate"
raise exception.InvalidAggregateAction(
action=action_name, aggregate_id=aggregate_id,
reason=msg)
@ -3401,14 +3423,13 @@ class AggregateAPI(base.Base):
aggregate_payload)
# validates the host; ComputeHostNotFound is raised if invalid
service_obj.Service.get_by_compute_host(context, host_name)
host_az = availability_zones.get_host_availability_zone(context,
host_name)
if host_az and host_az != CONF.default_availability_zone:
aggregate_meta = self.db.aggregate_metadata_get_by_metadata_key(
context, aggregate_id, 'availability_zone')
if aggregate_meta.get("availability_zone"):
self._check_az_for_host(aggregate_meta, host_az, aggregate_id)
metadata = self.db.aggregate_metadata_get_by_metadata_key(
context, aggregate_id, 'availability_zone')
aggregate = aggregate_obj.Aggregate.get_by_id(context, aggregate_id)
self.is_safe_to_update_az(context, metadata, hosts=[host_name],
aggregate=aggregate)
aggregate.add_host(context, host_name)
self._update_az_cache_for_host(context, host_name, aggregate.metadata)
#NOTE(jogo): Send message to host to support resource pools

View File

@ -9321,6 +9321,23 @@ class ComputeAPIAggrTestCase(BaseTestCase):
self.assertEqual(msg.event_type,
'aggregate.updateprop.end')
def test_update_aggregate_az_fails_with_nova_az(self):
# Ensure aggregate's availability zone can't be updated,
# when aggregate has hosts in other availability zone
fake_notifier.NOTIFICATIONS = []
values = _create_service_entries(self.context)
fake_zone = values.keys()[0]
fake_host = values[fake_zone][0]
aggr1 = self._init_aggregate_with_host(None, 'fake_aggregate1',
CONF.default_availability_zone,
fake_host)
aggr2 = self._init_aggregate_with_host(None, 'fake_aggregate2', None,
fake_host)
metadata = {'availability_zone': 'another_zone'}
self.assertRaises(exception.InvalidAggregateAction,
self.api.update_aggregate,
self.context, aggr2['id'], metadata)
def test_update_aggregate_metadata(self):
# Ensure metadata can be updated.
aggr = self.api.create_aggregate(self.context, 'fake_aggregate',
@ -9547,6 +9564,25 @@ class ComputeAPIAggrTestCase(BaseTestCase):
self.api.add_host_to_aggregate,
self.context, aggr2['id'], fake_host)
def test_add_host_to_multi_az_with_nova_agg(self):
# Ensure we can't add a host if already existing in an agg with AZ set
# to default
values = _create_service_entries(self.context)
fake_zone = values.keys()[0]
fake_host = values[fake_zone][0]
aggr = self.api.create_aggregate(self.context,
'fake_aggregate',
CONF.default_availability_zone)
aggr = self.api.add_host_to_aggregate(self.context,
aggr['id'], fake_host)
self.assertEqual(len(aggr['hosts']), 1)
fake_zone2 = "another_zone"
aggr2 = self.api.create_aggregate(self.context,
'fake_aggregate2', fake_zone2)
self.assertRaises(exception.InvalidAggregateAction,
self.api.add_host_to_aggregate,
self.context, aggr2['id'], fake_host)
def test_add_host_to_aggregate_multiple(self):
# Ensure we can add multiple hosts to an aggregate.
values = _create_service_entries(self.context)