Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion nova/api/openstack/compute/schemas/server_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,20 @@
create_v264['properties']['server_group']['properties']['policy'] = {
'type': 'string',
'enum': ['anti-affinity', 'affinity',
'soft-anti-affinity', 'soft-affinity'],
'soft-anti-affinity', 'soft-affinity',
'network-group-affinity', 'network-group-anti-affinity'],
}

create_v264['properties']['server_group']['properties']['rules'] = {
'type': 'object',
'properties': {
'max_server_per_host':
parameter_types.positive_integer,
'network_group': {
'type': 'string',
'minLength': 1,
'maxLength': 255,
},
},
'additionalProperties': False,
}
Expand Down Expand Up @@ -160,12 +166,15 @@
'anti-affinity',
'soft-affinity',
'soft-anti-affinity',
'network-group-affinity',
'network-group-anti-affinity',
],
},
'rules': {
'type': 'object',
'properties': {
'max_server_per_host': {'type': 'integer'},
'network_group': {'type': 'string'},
},
'required': [],
'additionalProperties': False,
Expand Down
28 changes: 21 additions & 7 deletions nova/api/openstack/compute/server_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,28 @@ def create(self, req, body):
if api_version_request.is_supported(req, "2.64"):
policy = vals['policy']
rules = vals.get('rules', {})
if policy != 'anti-affinity' and rules:
msg = _("Only anti-affinity policy supports rules.")
if policy == 'anti-affinity':
# NOTE(yikun): This should be removed in Stein version.
if not _should_enable_custom_max_server_rules(context, rules):
msg = _("Creating an anti-affinity group with rule "
"max_server_per_host > 1 is not yet supported.")
raise exc.HTTPConflict(explanation=msg)
elif policy in ('network-group-affinity',
'network-group-anti-affinity'):
if 'max_server_per_host' in rules:
msg = _("network-group-affinity and "
"network-group-anti-affinity policies do not "
"support the max_server_per_host rule.")
raise exc.HTTPBadRequest(explanation=msg)
if 'network_group' not in rules:
msg = _("network-group-affinity and "
"network-group-anti-affinity policies require "
"a network_group rule.")
raise exc.HTTPBadRequest(explanation=msg)
elif rules:
msg = _("Only anti-affinity, network-group-affinity, and "
"network-group-anti-affinity policies support rules.")
raise exc.HTTPBadRequest(explanation=msg)
# NOTE(yikun): This should be removed in Stein version.
if not _should_enable_custom_max_server_rules(context, rules):
msg = _("Creating an anti-affinity group with rule "
"max_server_per_host > 1 is not yet supported.")
raise exc.HTTPConflict(explanation=msg)
sg = objects.InstanceGroup(context, policy=policy,
rules=rules)
else:
Expand Down
2 changes: 2 additions & 0 deletions nova/conf/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@
"ImagePropertiesFilter",
"ServerGroupAntiAffinityFilter",
"ServerGroupAffinityFilter",
"NetworkGroupAffinityFilter",
"NetworkGroupAntiAffinityFilter",
],
help="""
Filters that the scheduler will use.
Expand Down
2 changes: 2 additions & 0 deletions nova/objects/instance_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ def rules(self):
if 'max_server_per_host' in self._rules:
rules['max_server_per_host'] = \
int(self._rules['max_server_per_host'])
if 'network_group' in self._rules:
rules['network_group'] = self._rules['network_group']
return rules

def obj_make_compatible(self, primitive, target_version):
Expand Down
135 changes: 135 additions & 0 deletions nova/scheduler/filters/network_group_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Copyright 2025 Rackspace Technology, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

"""Scheduler filters for network group affinity and anti-affinity.

These filters constrain instance placement based on the physical network
group (VLAN group / cabinet switch pair) that an Ironic node belongs to.

The network group is specified in a server group's ``rules`` field at
creation time and is matched against ``CUSTOM_NETGROUP_*`` traits reported
by Ironic nodes via the Placement service.
"""

from oslo_log import log as logging

from nova.scheduler import filters

LOG = logging.getLogger(__name__)

# Prefix used when converting a network group name to a trait.
# Example: "a1-1-network" -> "CUSTOM_NETGROUP_A1_1_NETWORK"
_TRAIT_PREFIX = "CUSTOM_NETGROUP_"


def _network_group_to_trait(network_group):
"""Convert a network group name to its corresponding Placement trait.

:param network_group: The network group name (e.g. "a1-1-network")
:returns: The trait string (e.g. "CUSTOM_NETGROUP_A1_1_NETWORK")
"""
normalised = network_group.upper().replace("-", "_").replace("/", "_")
return _TRAIT_PREFIX + normalised


class NetworkGroupAffinityFilter(filters.BaseHostFilter):
"""Schedule instances onto hosts within a specific network group.

When a server group has the ``network-group-affinity`` policy and a
``network_group`` rule, this filter only passes hosts whose reported
traits include the matching ``CUSTOM_NETGROUP_*`` trait.

Hosts without the required trait are rejected.
"""

# The trait set of a host does not change within a single scheduling
# request.
run_filter_once_per_request = True

RUN_ON_REBUILD = False

def host_passes(self, host_state, spec_obj):
instance_group = spec_obj.instance_group
if not instance_group:
return True

policy = instance_group.policy if instance_group else None
if policy != 'network-group-affinity':
return True

rules = instance_group.rules
network_group = rules.get('network_group') if rules else None
if not network_group:
return True

required_trait = _network_group_to_trait(network_group)

host_traits = set()
if hasattr(host_state, 'traits'):
host_traits = host_state.traits

passes = required_trait in host_traits
if not passes:
LOG.debug(
"NetworkGroupAffinityFilter: host %(host)s rejected. "
"Required trait %(trait)s not found in host traits.",
{'host': host_state.host, 'trait': required_trait})
return passes


class NetworkGroupAntiAffinityFilter(filters.BaseHostFilter):
"""Schedule instances onto hosts NOT within a specific network group.

When a server group has the ``network-group-anti-affinity`` policy and
a ``network_group`` rule, this filter rejects hosts whose reported
traits include the matching ``CUSTOM_NETGROUP_*`` trait.

This is useful for spreading workloads across cabinets or ensuring
instances avoid a particular switch pair.
"""

# The trait set of a host does not change within a single scheduling
# request.
run_filter_once_per_request = True

RUN_ON_REBUILD = False

def host_passes(self, host_state, spec_obj):
instance_group = spec_obj.instance_group
if not instance_group:
return True

policy = instance_group.policy if instance_group else None
if policy != 'network-group-anti-affinity':
return True

rules = instance_group.rules
network_group = rules.get('network_group') if rules else None
if not network_group:
return True

excluded_trait = _network_group_to_trait(network_group)

host_traits = set()
if hasattr(host_state, 'traits'):
host_traits = host_state.traits

passes = excluded_trait not in host_traits
if not passes:
LOG.debug(
"NetworkGroupAntiAffinityFilter: host %(host)s rejected. "
"Excluded trait %(trait)s found in host traits.",
{'host': host_state.host, 'trait': excluded_trait})
return passes
3 changes: 2 additions & 1 deletion nova/scheduler/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1210,7 +1210,8 @@ def _get_group_details(context, instance_uuid, user_group_hosts=None):
return

policies = set(('anti-affinity', 'affinity', 'soft-affinity',
'soft-anti-affinity'))
'soft-anti-affinity', 'network-group-affinity',
'network-group-anti-affinity'))
if group.policy in policies:
if not _SUPPORTS_AFFINITY and 'affinity' == group.policy:
msg = _("ServerGroupAffinityFilter not configured")
Expand Down
Loading