@@ -1646,7 +1646,11 @@ def _decode(f):
1646
1646
return [_decode(f) for f in injected_files]
1647
1647
1648
1648
def _validate_instance_group_policy(self, context, instance,
1649
- scheduler_hints):
1649
+ scheduler_hints=None):
1650
+
1651
+ if CONF.workarounds.disable_group_policy_check_upcall:
1652
+ return
1653
+
1650
1654
# NOTE(russellb) Instance group policy is enforced by the scheduler.
1651
1655
# However, there is a race condition with the enforcement of
1652
1656
# the policy. Since more than one instance may be scheduled at the
@@ -1655,29 +1659,63 @@ def _validate_instance_group_policy(self, context, instance,
1655
1659
# multiple instances with an affinity policy could end up on different
1656
1660
# hosts. This is a validation step to make sure that starting the
1657
1661
# instance here doesn't violate the policy.
1658
- group_hint = scheduler_hints.get('group')
1659
- if not group_hint:
1660
- return
1661
-
1662
- # The RequestSpec stores scheduler_hints as key=list pairs so we need
1663
- # to check the type on the value and pull the single entry out. The
1664
- # API request schema validates that the 'group' hint is a single value.
1665
- if isinstance(group_hint, list):
1666
- group_hint = group_hint[0]
1662
+ if scheduler_hints is not None:
1663
+ # only go through here if scheduler_hints is provided, even if it
1664
+ # is empty.
1665
+ group_hint = scheduler_hints.get('group')
1666
+ if not group_hint:
1667
+ return
1668
+ else:
1669
+ # The RequestSpec stores scheduler_hints as key=list pairs so
1670
+ # we need to check the type on the value and pull the single
1671
+ # entry out. The API request schema validates that
1672
+ # the 'group' hint is a single value.
1673
+ if isinstance(group_hint, list):
1674
+ group_hint = group_hint[0]
1675
+
1676
+ group = objects.InstanceGroup.get_by_hint(context, group_hint)
1677
+ else:
1678
+ # TODO(ganso): a call to DB can be saved by adding request_spec
1679
+ # to rpcapi payload of live_migration, pre_live_migration and
1680
+ # check_can_live_migrate_destination
1681
+ try:
1682
+ group = objects.InstanceGroup.get_by_instance_uuid(
1683
+ context, instance.uuid)
1684
+ except exception.InstanceGroupNotFound:
1685
+ return
1667
1686
1668
- @utils.synchronized(group_hint)
1669
- def _do_validation(context, instance, group_hint):
1670
- group = objects.InstanceGroup.get_by_hint(context, group_hint)
1687
+ @utils.synchronized(group['uuid'])
1688
+ def _do_validation(context, instance, group):
1671
1689
if group.policy and 'anti-affinity' == group.policy:
1690
+
1691
+ # instances on host
1672
1692
instances_uuids = objects.InstanceList.get_uuids_by_host(
1673
1693
context, self.host)
1674
1694
ins_on_host = set(instances_uuids)
1695
+
1696
+ # instance param is just for logging, the nodename obtained is
1697
+ # not actually related to the instance at all
1698
+ nodename = self._get_nodename(instance)
1699
+
1700
+ # instances being migrated to host
1701
+ migrations = (
1702
+ objects.MigrationList.get_in_progress_by_host_and_node(
1703
+ context, self.host, nodename))
1704
+ migration_vm_uuids = set([mig['instance_uuid']
1705
+ for mig in migrations])
1706
+
1707
+ total_instances = migration_vm_uuids | ins_on_host
1708
+
1709
+ # refresh group to get updated members within locked block
1710
+ group = objects.InstanceGroup.get_by_uuid(context,
1711
+ group['uuid'])
1675
1712
members = set(group.members)
1676
1713
# Determine the set of instance group members on this host
1677
1714
# which are not the instance in question. This is used to
1678
1715
# determine how many other members from the same anti-affinity
1679
1716
# group can be on this host.
1680
- members_on_host = ins_on_host & members - set([instance.uuid])
1717
+ members_on_host = (total_instances & members -
1718
+ set([instance.uuid]))
1681
1719
rules = group.rules
1682
1720
if rules and 'max_server_per_host' in rules:
1683
1721
max_server = rules['max_server_per_host']
@@ -1689,6 +1727,12 @@ def _do_validation(context, instance, group_hint):
1689
1727
raise exception.RescheduledException(
1690
1728
instance_uuid=instance.uuid,
1691
1729
reason=msg)
1730
+
1731
+ # NOTE(ganso): The check for affinity below does not work and it
1732
+ # can easily be violated because the lock happens in different
1733
+ # compute hosts.
1734
+ # The only fix seems to be a DB lock to perform the check whenever
1735
+ # setting the host field to an instance.
1692
1736
elif group.policy and 'affinity' == group.policy:
1693
1737
group_hosts = group.get_hosts(exclude=[instance.uuid])
1694
1738
if group_hosts and self.host not in group_hosts:
@@ -1697,8 +1741,7 @@ def _do_validation(context, instance, group_hint):
1697
1741
instance_uuid=instance.uuid,
1698
1742
reason=msg)
1699
1743
1700
- if not CONF.workarounds.disable_group_policy_check_upcall:
1701
- _do_validation(context, instance, group_hint)
1744
+ _do_validation(context, instance, group)
1702
1745
1703
1746
def _log_original_error(self, exc_info, instance_uuid):
1704
1747
LOG.error('Error: %s', exc_info[1], instance_uuid=instance_uuid,
@@ -5217,10 +5260,24 @@ def prep_resize(self, context, image, instance, instance_type,
5217
5260
with self._error_out_instance_on_exception(
5218
5261
context, instance, instance_state=instance_state),\
5219
5262
errors_out_migration_ctxt(migration):
5263
+
5220
5264
self._send_prep_resize_notifications(
5221
5265
context, instance, fields.NotificationPhase.START,
5222
5266
instance_type)
5223
5267
try:
5268
+ scheduler_hints = self._get_scheduler_hints(filter_properties,
5269
+ request_spec)
5270
+ # Error out if this host cannot accept the new instance due
5271
+ # to anti-affinity. At this point the migration is already
5272
+ # in-progress, so this is the definitive moment to abort due to
5273
+ # the policy violation. Also, exploding here is covered by the
5274
+ # cleanup methods in except block.
5275
+ try:
5276
+ self._validate_instance_group_policy(context, instance,
5277
+ scheduler_hints)
5278
+ except exception.RescheduledException as e:
5279
+ raise exception.InstanceFaultRollback(inner_exception=e)
5280
+
5224
5281
self._prep_resize(context, image, instance,
5225
5282
instance_type, filter_properties,
5226
5283
node, migration, request_spec,
@@ -7823,6 +7880,20 @@ def check_can_live_migrate_destination(self, ctxt, instance,
7823
7880
:param limits: objects.SchedulerLimits object for this live migration.
7824
7881
:returns: a LiveMigrateData object (hypervisor-dependent)
7825
7882
"""
7883
+
7884
+ # Error out if this host cannot accept the new instance due
7885
+ # to anti-affinity. This check at this moment is not very accurate, as
7886
+ # multiple requests may be happening concurrently and miss the lock,
7887
+ # but when it works it provides a better user experience by failing
7888
+ # earlier. Also, it should be safe to explode here, error becomes
7889
+ # NoValidHost and instance status remains ACTIVE.
7890
+ try:
7891
+ self._validate_instance_group_policy(ctxt, instance)
7892
+ except exception.RescheduledException as e:
7893
+ msg = ("Failed to validate instance group policy "
7894
+ "due to: {}".format(e))
7895
+ raise exception.MigrationPreCheckError(reason=msg)
7896
+
7826
7897
src_compute_info = obj_base.obj_to_primitive(
7827
7898
self._get_compute_info(ctxt, instance.host))
7828
7899
dst_compute_info = obj_base.obj_to_primitive(
@@ -7965,6 +8036,13 @@ def pre_live_migration(self, context, instance, block_migration, disk,
7965
8036
"""
7966
8037
LOG.debug('pre_live_migration data is %s', migrate_data)
7967
8038
8039
+ # Error out if this host cannot accept the new instance due
8040
+ # to anti-affinity. At this point the migration is already in-progress,
8041
+ # so this is the definitive moment to abort due to the policy
8042
+ # violation. Also, it should be safe to explode here. The instance
8043
+ # status remains ACTIVE, migration status failed.
8044
+ self._validate_instance_group_policy(context, instance)
8045
+
7968
8046
migrate_data.old_vol_attachment_ids = {}
7969
8047
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
7970
8048
context, instance.uuid)
0 commit comments