Skip to content

Commit cd13b27

Browse files
Implement external scheduler call
1 parent 7b4ec99 commit cd13b27

File tree

5 files changed

+457
-0
lines changed

5 files changed

+457
-0
lines changed

cinder/opts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from cinder.message import api as cinder_message_api
5858
from cinder import quota as cinder_quota
5959
from cinder.scheduler import driver as cinder_scheduler_driver
60+
from cinder.scheduler import external as cinder_scheduler_external
6061
from cinder.scheduler.filters import shard_filter as \
6162
cinder_scheduler_filters_shardfilter
6263
from cinder.scheduler import host_manager as cinder_scheduler_hostmanager
@@ -274,6 +275,7 @@ def list_opts():
274275
cinder_message_api.messages_opts,
275276
cinder_quota.quota_opts,
276277
cinder_scheduler_driver.scheduler_driver_opts,
278+
cinder_scheduler_external.scheduler_external_opts,
277279
cinder_scheduler_hostmanager.host_manager_opts,
278280
cinder_scheduler_manager.scheduler_manager_opts,
279281
[cinder_scheduler_scheduleroptions.

cinder/scheduler/external.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Copyright (c) 2025 SAP SE
2+
# All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
# not use this file except in compliance with the License. You may obtain
6+
# a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
"""
16+
This module provides functionality to interact with an external scheduler API
17+
to reorder and filter hosts based on additional criteria.
18+
19+
The external scheduler API is expected to take a list of weighed hosts and
20+
their weights, along with the request specification, and return a reordered
21+
and filtered list of host names.
22+
"""
23+
import jsonschema
24+
from oslo_config import cfg
25+
from oslo_log import log as logging
26+
import requests
27+
28+
29+
scheduler_external_opts = [
30+
cfg.StrOpt(
31+
"external_scheduler_api_url",
32+
default="",
33+
help="""
34+
The API URL of the external scheduler.
35+
36+
If this URL is provided, Cinder will call an external service after filters
37+
and weighers have been applied. This service can reorder and filter the
38+
list of hosts before Cinder attempts to place the volume.
39+
40+
If not provided, this step will be skipped.
41+
"""),
42+
cfg.IntOpt(
43+
"external_scheduler_timeout",
44+
default=5,
45+
min=1,
46+
help="""
47+
The timeout in seconds for the external scheduler.
48+
49+
If external_scheduler_api_url is configured, Cinder will call and wait for the
50+
external scheduler to respond for this long. If the external scheduler does not
51+
respond within this time, the request will be aborted. In this case, the
52+
scheduler will continue with the original host selection and weights.
53+
"""),
54+
]
55+
56+
CONF = cfg.CONF
57+
CONF.register_opts(scheduler_external_opts)
58+
59+
LOG = logging.getLogger(__name__)
60+
61+
# The expected response schema from the external scheduler api.
62+
# The response should contain a list of ordered host names.
63+
RESPONSE_SCHEMA = {
64+
"type": "object",
65+
"properties": {
66+
"hosts": {
67+
"type": "array",
68+
"items": {
69+
"type": "string"
70+
}
71+
}
72+
},
73+
"required": ["hosts"],
74+
"additionalProperties": False,
75+
}
76+
77+
78+
def call_external_scheduler_api(context, weighed_hosts, spec_dict):
79+
"""Reorder and filter hosts using an external scheduler service.
80+
81+
:param context: The RequestContext object containing request id, user, etc.
82+
:param weighed_hosts: List of w. hosts to send to the external scheduler.
83+
:param spec_dict: The RequestSpec object with the share specification.
84+
"""
85+
if not weighed_hosts:
86+
return weighed_hosts
87+
if not (url := CONF.external_scheduler_api_url):
88+
LOG.debug("External scheduler API is not enabled.")
89+
return weighed_hosts
90+
timeout = CONF.external_scheduler_timeout
91+
# We shouldn't pass and log the auth token. Thus, we delete it here.
92+
ctx_dict = context.to_dict()
93+
if "auth_token" in ctx_dict:
94+
del ctx_dict["auth_token"]
95+
json_data = {
96+
"spec": spec_dict,
97+
# Also serialize the request context, which contains the global request
98+
# id and other information helpful for logging and request tracing.
99+
"context": ctx_dict,
100+
# Only provide basic information for the hosts for now.
101+
# The external scheduler is expected to fetch statistics
102+
# about the hosts separately, so we don't need to pass
103+
# them here.
104+
"hosts": [
105+
{
106+
"host": h.obj.host,
107+
} for h in weighed_hosts
108+
],
109+
# Also pass previous weights from the Cinder weigher pipeline.
110+
# The external scheduler api is expected to take these weights
111+
# into account if provided.
112+
"weights": {h.obj.host: h.weight for h in weighed_hosts},
113+
}
114+
LOG.debug("Calling external scheduler API with %s", json_data)
115+
try:
116+
response = requests.post(url, json=json_data, timeout=timeout)
117+
response.raise_for_status()
118+
# If the JSON parsing fails, this will also raise a RequestException.
119+
response_json = response.json()
120+
except requests.RequestException as e:
121+
LOG.error("Failed to call external scheduler API: %s", e)
122+
return weighed_hosts
123+
124+
# The external scheduler api is expected to return a json with
125+
# a sorted list of host names. Note that no weights are returned.
126+
try:
127+
jsonschema.validate(response_json, RESPONSE_SCHEMA)
128+
except jsonschema.ValidationError as e:
129+
LOG.error("External scheduler response is invalid: %s", e)
130+
return weighed_hosts
131+
132+
# The list of host names can also be empty. In this case, we trust
133+
# the external scheduler decision and return an empty list.
134+
if not (host_names := response_json["hosts"]):
135+
# If this case happens often, it may indicate an issue.
136+
LOG.warning("External scheduler filtered out all hosts.")
137+
138+
# Reorder the weighed hosts based on the list of host names returned
139+
# by the external scheduler api.
140+
weighed_hosts_dict = {h.obj.host: h for h in weighed_hosts}
141+
return [weighed_hosts_dict[h] for h in host_names]

cinder/scheduler/filter_scheduler.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from cinder.i18n import _
3434
from cinder import objects
3535
from cinder.scheduler import driver
36+
from cinder.scheduler.external import call_external_scheduler_api
3637
from cinder.scheduler.host_manager import BackendState
3738
from cinder.scheduler import scheduler_options
3839
from cinder.scheduler.weights import WeighedHost
@@ -405,6 +406,13 @@ def _get_weighted_candidates(
405406
# backend for the job.
406407
weighed_backends = self.host_manager.get_weighed_backends(
407408
backends, filter_properties)
409+
410+
# Call an external service that can modify `weighed_hosts` once more.
411+
# This service may filter out some hosts, or it may re-order them.
412+
# Note: the result can also be empty.
413+
weighed_backends = call_external_scheduler_api(
414+
context, weighed_backends, request_spec)
415+
408416
return weighed_backends
409417

410418
def _get_weighted_candidates_generic_group(

0 commit comments

Comments
 (0)