Skip to content

Commit a11d44d

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

File tree

4 files changed

+452
-0
lines changed

4 files changed

+452
-0
lines changed

cinder/scheduler/external.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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+
LOG = logging.getLogger(__name__)
29+
30+
CONF = cfg.CONF
31+
CONF.register_opts([
32+
cfg.StrOpt(
33+
"external_scheduler_api_url",
34+
default="",
35+
help="""
36+
The API URL of the external scheduler.
37+
38+
If this URL is provided, Cinder will call an external service after filters
39+
and weighers have been applied. This service can reorder and filter the
40+
list of hosts before Cinder attempts to place the volume.
41+
42+
If not provided, this step will be skipped.
43+
"""),
44+
cfg.IntOpt(
45+
"external_scheduler_timeout",
46+
default=5,
47+
min=1,
48+
help="""
49+
The timeout in seconds for the external scheduler.
50+
51+
If external_scheduler_api_url is configured, Cinder will call and wait for the
52+
external scheduler to respond for this long. If the external scheduler does not
53+
respond within this time, the request will be aborted. In this case, the
54+
scheduler will continue with the original host selection and weights.
55+
""")
56+
])
57+
58+
# The expected response schema from the external scheduler api.
59+
# The response should contain a list of ordered host names.
60+
RESPONSE_SCHEMA = {
61+
"type": "object",
62+
"properties": {
63+
"hosts": {
64+
"type": "array",
65+
"items": {
66+
"type": "string"
67+
}
68+
}
69+
},
70+
"required": ["hosts"],
71+
"additionalProperties": False,
72+
}
73+
74+
75+
def call_external_scheduler_api(context, weighed_hosts, spec_dict):
76+
"""Reorder and filter hosts using an external scheduler service.
77+
78+
:param context: The RequestContext object containing request id, user, etc.
79+
:param weighed_hosts: List of w. hosts to send to the external scheduler.
80+
:param spec_dict: The RequestSpec object with the share specification.
81+
"""
82+
if not weighed_hosts:
83+
return weighed_hosts
84+
if not (url := CONF.external_scheduler_api_url):
85+
LOG.debug("External scheduler API is not enabled.")
86+
return weighed_hosts
87+
timeout = CONF.external_scheduler_timeout
88+
# We shouldn't pass and log the auth token. Thus, we delete it here.
89+
ctx_dict = context.to_dict()
90+
if "auth_token" in ctx_dict:
91+
del ctx_dict["auth_token"]
92+
json_data = {
93+
"spec": spec_dict,
94+
# Also serialize the request context, which contains the global request
95+
# id and other information helpful for logging and request tracing.
96+
"context": ctx_dict,
97+
# Only provide basic information for the hosts for now.
98+
# The external scheduler is expected to fetch statistics
99+
# about the hosts separately, so we don't need to pass
100+
# them here.
101+
"hosts": [
102+
{
103+
"host": h.obj.host,
104+
} for h in weighed_hosts
105+
],
106+
# Also pass previous weights from the Cinder weigher pipeline.
107+
# The external scheduler api is expected to take these weights
108+
# into account if provided.
109+
"weights": {h.obj.host: h.weight for h in weighed_hosts},
110+
}
111+
LOG.debug("Calling external scheduler API with %s", json_data)
112+
try:
113+
response = requests.post(url, json=json_data, timeout=timeout)
114+
response.raise_for_status()
115+
# If the JSON parsing fails, this will also raise a RequestException.
116+
response_json = response.json()
117+
except requests.RequestException as e:
118+
LOG.error("Failed to call external scheduler API: %s", e)
119+
return weighed_hosts
120+
121+
# The external scheduler api is expected to return a json with
122+
# a sorted list of host names. Note that no weights are returned.
123+
try:
124+
jsonschema.validate(response_json, RESPONSE_SCHEMA)
125+
except jsonschema.ValidationError as e:
126+
LOG.error("External scheduler response is invalid: %s", e)
127+
return weighed_hosts
128+
129+
# The list of host names can also be empty. In this case, we trust
130+
# the external scheduler decision and return an empty list.
131+
if not (host_names := response_json["hosts"]):
132+
# If this case happens often, it may indicate an issue.
133+
LOG.warning("External scheduler filtered out all hosts.")
134+
135+
# Reorder the weighed hosts based on the list of host names returned
136+
# by the external scheduler api.
137+
weighed_hosts_dict = {h.obj.host: h for h in weighed_hosts}
138+
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)