Skip to content

Commit 9d444b0

Browse files
feat(nova): Adds custom Nova scheduler filter for host traits
1 parent 43cdf0a commit 9d444b0

4 files changed

Lines changed: 113 additions & 0 deletions

File tree

components/nova/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ conf:
9191
- ServerGroupAntiAffinityFilter
9292
- ServerGroupAffinityFilter
9393
- JsonFilter
94+
- TraitRequiredFilter
9495
nova_api_uwsgi:
9596
uwsgi:
9697
processes: 2

containers/nova/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ COPY containers/nova/patches /tmp/patches/
1313
RUN cd /var/lib/openstack/lib/python3.12/site-packages && \
1414
QUILT_PATCHES=/tmp/patches quilt push -a
1515
COPY python/nova-understack/ironic_understack /var/lib/openstack/lib/python3.12/site-packages/nova/virt/ironic_understack
16+
COPY python/nova-understack/nova_understack_filters /var/lib/openstack/lib/python3.12/site-packages/nova/scheduler/filters/understack

python/nova-understack/nova_understack_filters/__init__.py

Whitespace-only changes.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from typing import ClassVar
2+
3+
import nova.conf
4+
from nova import context as nova_context
5+
from nova.scheduler import filters
6+
from nova.scheduler.client import report as report_client
7+
from oslo_log import log as logging
8+
9+
LOG = logging.getLogger(__name__)
10+
CONF = nova.conf.CONF
11+
12+
13+
class TraitRequiredFilter(filters.BaseHostFilter):
14+
"""Filter hosts based on custom traits passed via scheduler hints.
15+
16+
Accepts scheduler hints in the form:
17+
trait:CUSTOM_<NAME>=required
18+
19+
Only hosts whose resource provider reports the requested trait(s)
20+
will pass the filter. If no trait hints are provided, all hosts pass.
21+
22+
Traits are fetched from the Placement API using the resource provider
23+
UUID associated with each host.
24+
25+
Usage:
26+
openstack server create ... --hint trait:CUSTOM_CAB_A1_1=required
27+
"""
28+
29+
# Scheduler hints can differ per request, so this filter must
30+
# run against every host for each request.
31+
run_filter_once_per_request = False
32+
33+
# Cache of {rp_uuid: set(traits)} populated per scheduling pass.
34+
# This avoids repeated Placement API calls for the same provider
35+
# within a single filter run.
36+
_traits_cache: ClassVar[dict[str, set[str]]] = {}
37+
38+
def host_passes(self, host_state, spec_obj):
39+
requested_traits = set()
40+
41+
hints = spec_obj.scheduler_hints or {}
42+
for key, values in hints.items():
43+
if not key.startswith("trait:"):
44+
continue
45+
trait_name = key.split("trait:", 1)[1]
46+
if not trait_name.startswith("CUSTOM_"):
47+
continue
48+
# values is a list; check if 'required' is among them
49+
if "required" in values:
50+
requested_traits.add(trait_name)
51+
52+
if not requested_traits:
53+
return True
54+
55+
host_traits = self._get_traits_for_host(host_state)
56+
missing = requested_traits - host_traits
57+
58+
if missing:
59+
LOG.debug(
60+
"%(host)s fails TraitRequiredFilter: missing %(missing)s",
61+
{"host": host_state.host, "missing": missing},
62+
)
63+
return False
64+
65+
return True
66+
67+
def _get_traits_for_host(self, host_state):
68+
"""Get traits for a host's resource provider from Placement.
69+
70+
Uses a per-instance cache to avoid repeated API calls within
71+
the same scheduling pass.
72+
"""
73+
rp_uuid = host_state.uuid
74+
if rp_uuid in self._traits_cache:
75+
return self._traits_cache[rp_uuid]
76+
77+
try:
78+
client = report_client.report_client_singleton()
79+
context = nova_context.get_admin_context()
80+
trait_info = client.get_provider_traits(context, rp_uuid)
81+
traits = trait_info.traits
82+
except Exception:
83+
LOG.warning(
84+
"Could not retrieve traits for host %(host)s "
85+
"(rp_uuid=%(uuid)s) from Placement API.",
86+
{"host": host_state.host, "uuid": rp_uuid},
87+
)
88+
traits = set()
89+
90+
self._traits_cache[rp_uuid] = traits
91+
return traits
92+
93+
def filter_all(self, filter_obj_list, spec_obj):
94+
"""Override to clear the traits cache before each filter pass."""
95+
self._traits_cache = {}
96+
return super().filter_all(filter_obj_list, spec_obj)
97+
98+
99+
def all_filters():
100+
"""Return all standard Nova filters plus Understack custom filters.
101+
102+
This function is used as the value for [filter_scheduler]available_filters
103+
to work around OpenStack Helm's inability to render MultiStrOpt values
104+
(it joins YAML lists into a single comma-separated line).
105+
106+
By pointing available_filters to this single function path, we avoid
107+
needing multiple available_filters lines.
108+
"""
109+
from nova.scheduler.filters import all_filters as nova_all_filters
110+
111+
return [*nova_all_filters(), TraitRequiredFilter]

0 commit comments

Comments
 (0)