Skip to content
Open
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
1 change: 1 addition & 0 deletions components/nova/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ conf:
- ServerGroupAntiAffinityFilter
- ServerGroupAffinityFilter
- JsonFilter
- TraitRequiredFilter
nova_api_uwsgi:
uwsgi:
processes: 2
Expand Down
1 change: 1 addition & 0 deletions containers/nova/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ COPY containers/nova/patches /tmp/patches/
RUN cd /var/lib/openstack/lib/python3.12/site-packages && \
QUILT_PATCHES=/tmp/patches quilt push -a
COPY python/nova-understack/ironic_understack /var/lib/openstack/lib/python3.12/site-packages/nova/virt/ironic_understack
COPY python/nova-understack/nova_understack_filters /var/lib/openstack/lib/python3.12/site-packages/nova/scheduler/filters/understack
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from typing import ClassVar

import nova.conf
from nova import context as nova_context
from nova.scheduler import filters
from nova.scheduler.client import report as report_client
from oslo_log import log as logging

LOG = logging.getLogger(__name__)
CONF = nova.conf.CONF


class TraitRequiredFilter(filters.BaseHostFilter):
"""Filter hosts based on custom traits passed via scheduler hints.

Accepts scheduler hints in the form:
trait:CUSTOM_<NAME>=required

Only hosts whose resource provider reports the requested trait(s)
will pass the filter. If no trait hints are provided, all hosts pass.

Traits are fetched from the Placement API using the resource provider
UUID associated with each host.

Usage:
openstack server create ... --hint trait:CUSTOM_CAB_A1_1=required
"""

# Scheduler hints can differ per request, so this filter must
# run against every host for each request.
run_filter_once_per_request = False

# Cache of {rp_uuid: set(traits)} populated per scheduling pass.
# This avoids repeated Placement API calls for the same provider
# within a single filter run.
_traits_cache: ClassVar[dict[str, set[str]]] = {}

def host_passes(self, host_state, spec_obj):
requested_traits = set()

hints = spec_obj.scheduler_hints or {}
for key, values in hints.items():
if not key.startswith("trait:"):
continue
trait_name = key.split("trait:", 1)[1]
if not trait_name.startswith("CUSTOM_"):
continue
# values is a list; check if 'required' is among them
if "required" in values:
requested_traits.add(trait_name)

if not requested_traits:
return True

host_traits = self._get_traits_for_host(host_state)
missing = requested_traits - host_traits

if missing:
LOG.debug(
"%(host)s fails TraitRequiredFilter: missing %(missing)s",
{"host": host_state.host, "missing": missing},
)
return False

return True

def _get_traits_for_host(self, host_state):
"""Get traits for a host's resource provider from Placement.

Uses a per-instance cache to avoid repeated API calls within
the same scheduling pass.
"""
rp_uuid = host_state.uuid
if rp_uuid in self._traits_cache:
return self._traits_cache[rp_uuid]

try:
client = report_client.report_client_singleton()
context = nova_context.get_admin_context()
trait_info = client.get_provider_traits(context, rp_uuid)
traits = trait_info.traits
except Exception:
LOG.warning(
"Could not retrieve traits for host %(host)s "
"(rp_uuid=%(uuid)s) from Placement API.",
{"host": host_state.host, "uuid": rp_uuid},
)
traits = set()

self._traits_cache[rp_uuid] = traits
return traits

def filter_all(self, filter_obj_list, spec_obj):
"""Override to clear the traits cache before each filter pass."""
self._traits_cache = {}
return super().filter_all(filter_obj_list, spec_obj)


def all_filters():
"""Return all standard Nova filters plus Understack custom filters.

This function is used as the value for [filter_scheduler]available_filters
to work around OpenStack Helm's inability to render MultiStrOpt values
(it joins YAML lists into a single comma-separated line).

By pointing available_filters to this single function path, we avoid
needing multiple available_filters lines.
"""
from nova.scheduler.filters import all_filters as nova_all_filters

return [*nova_all_filters(), TraitRequiredFilter]
Loading