@@ -1610,7 +1610,11 @@ def _decode(f):
1610
1610
return [_decode(f) for f in injected_files]
1611
1611
1612
1612
def _validate_instance_group_policy(self, context, instance,
1613
- scheduler_hints):
1613
+ scheduler_hints=None):
1614
+
1615
+ if CONF.workarounds.disable_group_policy_check_upcall:
1616
+ return
1617
+
1614
1618
# NOTE(russellb) Instance group policy is enforced by the scheduler.
1615
1619
# However, there is a race condition with the enforcement of
1616
1620
# the policy. Since more than one instance may be scheduled at the
@@ -1619,29 +1623,63 @@ def _validate_instance_group_policy(self, context, instance,
1619
1623
# multiple instances with an affinity policy could end up on different
1620
1624
# hosts. This is a validation step to make sure that starting the
1621
1625
# instance here doesn't violate the policy.
1622
- group_hint = scheduler_hints.get('group')
1623
- if not group_hint:
1624
- return
1625
-
1626
- # The RequestSpec stores scheduler_hints as key=list pairs so we need
1627
- # to check the type on the value and pull the single entry out. The
1628
- # API request schema validates that the 'group' hint is a single value.
1629
- if isinstance(group_hint, list):
1630
- group_hint = group_hint[0]
1626
+ if scheduler_hints is not None:
1627
+ # only go through here if scheduler_hints is provided, even if it
1628
+ # is empty.
1629
+ group_hint = scheduler_hints.get('group')
1630
+ if not group_hint:
1631
+ return
1632
+ else:
1633
+ # The RequestSpec stores scheduler_hints as key=list pairs so
1634
+ # we need to check the type on the value and pull the single
1635
+ # entry out. The API request schema validates that
1636
+ # the 'group' hint is a single value.
1637
+ if isinstance(group_hint, list):
1638
+ group_hint = group_hint[0]
1639
+
1640
+ group = objects.InstanceGroup.get_by_hint(context, group_hint)
1641
+ else:
1642
+ # TODO(ganso): a call to DB can be saved by adding request_spec
1643
+ # to rpcapi payload of live_migration, pre_live_migration and
1644
+ # check_can_live_migrate_destination
1645
+ try:
1646
+ group = objects.InstanceGroup.get_by_instance_uuid(
1647
+ context, instance.uuid)
1648
+ except exception.InstanceGroupNotFound:
1649
+ return
1631
1650
1632
- @utils.synchronized(group_hint)
1633
- def _do_validation(context, instance, group_hint):
1634
- group = objects.InstanceGroup.get_by_hint(context, group_hint)
1651
+ @utils.synchronized(group['uuid'])
1652
+ def _do_validation(context, instance, group):
1635
1653
if group.policy and 'anti-affinity' == group.policy:
1654
+
1655
+ # instances on host
1636
1656
instances_uuids = objects.InstanceList.get_uuids_by_host(
1637
1657
context, self.host)
1638
1658
ins_on_host = set(instances_uuids)
1659
+
1660
+ # instance param is just for logging, the nodename obtained is
1661
+ # not actually related to the instance at all
1662
+ nodename = self._get_nodename(instance)
1663
+
1664
+ # instances being migrated to host
1665
+ migrations = (
1666
+ objects.MigrationList.get_in_progress_by_host_and_node(
1667
+ context, self.host, nodename))
1668
+ migration_vm_uuids = set([mig['instance_uuid']
1669
+ for mig in migrations])
1670
+
1671
+ total_instances = migration_vm_uuids | ins_on_host
1672
+
1673
+ # refresh group to get updated members within locked block
1674
+ group = objects.InstanceGroup.get_by_uuid(context,
1675
+ group['uuid'])
1639
1676
members = set(group.members)
1640
1677
# Determine the set of instance group members on this host
1641
1678
# which are not the instance in question. This is used to
1642
1679
# determine how many other members from the same anti-affinity
1643
1680
# group can be on this host.
1644
- members_on_host = ins_on_host & members - set([instance.uuid])
1681
+ members_on_host = (total_instances & members -
1682
+ set([instance.uuid]))
1645
1683
rules = group.rules
1646
1684
if rules and 'max_server_per_host' in rules:
1647
1685
max_server = rules['max_server_per_host']
@@ -1653,6 +1691,12 @@ def _do_validation(context, instance, group_hint):
1653
1691
raise exception.RescheduledException(
1654
1692
instance_uuid=instance.uuid,
1655
1693
reason=msg)
1694
+
1695
+ # NOTE(ganso): The check for affinity below does not work and it
1696
+ # can easily be violated because the lock happens in different
1697
+ # compute hosts.
1698
+ # The only fix seems to be a DB lock to perform the check whenever
1699
+ # setting the host field to an instance.
1656
1700
elif group.policy and 'affinity' == group.policy:
1657
1701
group_hosts = group.get_hosts(exclude=[instance.uuid])
1658
1702
if group_hosts and self.host not in group_hosts:
@@ -1661,8 +1705,7 @@ def _do_validation(context, instance, group_hint):
1661
1705
instance_uuid=instance.uuid,
1662
1706
reason=msg)
1663
1707
1664
- if not CONF.workarounds.disable_group_policy_check_upcall:
1665
- _do_validation(context, instance, group_hint)
1708
+ _do_validation(context, instance, group)
1666
1709
1667
1710
def _log_original_error(self, exc_info, instance_uuid):
1668
1711
LOG.error('Error: %s', exc_info[1], instance_uuid=instance_uuid,
@@ -5174,10 +5217,24 @@ def prep_resize(self, context, image, instance, flavor,
5174
5217
with self._error_out_instance_on_exception(
5175
5218
context, instance, instance_state=instance_state),\
5176
5219
errors_out_migration_ctxt(migration):
5220
+
5177
5221
self._send_prep_resize_notifications(
5178
5222
context, instance, fields.NotificationPhase.START,
5179
5223
flavor)
5180
5224
try:
5225
+ scheduler_hints = self._get_scheduler_hints(filter_properties,
5226
+ request_spec)
5227
+ # Error out if this host cannot accept the new instance due
5228
+ # to anti-affinity. At this point the migration is already
5229
+ # in-progress, so this is the definitive moment to abort due to
5230
+ # the policy violation. Also, exploding here is covered by the
5231
+ # cleanup methods in except block.
5232
+ try:
5233
+ self._validate_instance_group_policy(context, instance,
5234
+ scheduler_hints)
5235
+ except exception.RescheduledException as e:
5236
+ raise exception.InstanceFaultRollback(inner_exception=e)
5237
+
5181
5238
self._prep_resize(context, image, instance,
5182
5239
flavor, filter_properties,
5183
5240
node, migration, request_spec,
@@ -7909,6 +7966,20 @@ def check_can_live_migrate_destination(self, ctxt, instance,
7909
7966
:param limits: objects.SchedulerLimits object for this live migration.
7910
7967
:returns: a LiveMigrateData object (hypervisor-dependent)
7911
7968
"""
7969
+
7970
+ # Error out if this host cannot accept the new instance due
7971
+ # to anti-affinity. This check at this moment is not very accurate, as
7972
+ # multiple requests may be happening concurrently and miss the lock,
7973
+ # but when it works it provides a better user experience by failing
7974
+ # earlier. Also, it should be safe to explode here, error becomes
7975
+ # NoValidHost and instance status remains ACTIVE.
7976
+ try:
7977
+ self._validate_instance_group_policy(ctxt, instance)
7978
+ except exception.RescheduledException as e:
7979
+ msg = ("Failed to validate instance group policy "
7980
+ "due to: {}".format(e))
7981
+ raise exception.MigrationPreCheckError(reason=msg)
7982
+
7912
7983
src_compute_info = obj_base.obj_to_primitive(
7913
7984
self._get_compute_info(ctxt, instance.host))
7914
7985
dst_compute_info = obj_base.obj_to_primitive(
@@ -8048,6 +8119,13 @@ def pre_live_migration(self, context, instance, disk, migrate_data):
8048
8119
"""
8049
8120
LOG.debug('pre_live_migration data is %s', migrate_data)
8050
8121
8122
+ # Error out if this host cannot accept the new instance due
8123
+ # to anti-affinity. At this point the migration is already in-progress,
8124
+ # so this is the definitive moment to abort due to the policy
8125
+ # violation. Also, it should be safe to explode here. The instance
8126
+ # status remains ACTIVE, migration status failed.
8127
+ self._validate_instance_group_policy(context, instance)
8128
+
8051
8129
migrate_data.old_vol_attachment_ids = {}
8052
8130
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
8053
8131
context, instance.uuid)
0 commit comments