Extract custom resource classes from flavors
This patch adds code to look in a flavor's extra_specs for keys beginning with "resources:", and if found, use them to update the resources dict sent to placement. The entry for a custom resource class will look like "resources:CUSTOM_FOO=1". Additionally, standard classes in a flavor can be overridden with an entry that looks like: "resources:VCPU=0". If a standard class is found in extra_specs, it will be used instead of the amount in the flavor. This is useful for things like Ironic, where an operator may want to list the amount of RAM, disk, etc. in the flavor, but use a custom Ironic resource class for doing the actual selection. An amount for a standard class that is zero will result in that class being removed from the requested resources dict sent to placement. DocImpact We should document the capability and rules in https://docs.openstack.org/admin-guide/compute-flavors.html#extra-specs. Blueprint: custom-resource-classes-in-flavors Change-Id: I84f403fe78e04dd1d099d7d0d1d2925df59e80e7
This commit is contained in:
@@ -2090,6 +2090,10 @@ class ResourceClassCannotUpdateStandard(Invalid):
|
||||
msg_fmt = _("Cannot update standard resource class %(resource_class)s.")
|
||||
|
||||
|
||||
class InvalidResourceAmount(Invalid):
|
||||
msg_fmt = _("Resource amounts must be integers. Received '%(amount)s'.")
|
||||
|
||||
|
||||
class InvalidInventory(Invalid):
|
||||
msg_fmt = _("Inventory for '%(resource_class)s' on "
|
||||
"resource provider '%(resource_provider)s' invalid.")
|
||||
|
||||
@@ -31,6 +31,7 @@ from nova import objects
|
||||
from nova.objects import base as obj_base
|
||||
from nova.objects import fields
|
||||
from nova.objects import instance as obj_instance
|
||||
from nova.objects.resource_provider import ResourceClass
|
||||
from nova import rpc
|
||||
|
||||
|
||||
@@ -83,6 +84,65 @@ def build_request_spec(ctxt, image, instances, instance_type=None):
|
||||
return jsonutils.to_primitive(request_spec)
|
||||
|
||||
|
||||
def _process_extra_specs(extra_specs, resources):
|
||||
"""Check the flavor's extra_specs for custom resource information.
|
||||
These will be a dict that is generated from the flavor; and in the
|
||||
flavor, the extra_specs entries will be in the format of either:
|
||||
|
||||
resources:$CUSTOM_RESOURCE_CLASS=$N
|
||||
...to add a custom resource class to the request, with a positive
|
||||
integer amount of $N
|
||||
|
||||
resources:$STANDARD_RESOURCE_CLASS=0
|
||||
...to remove that resource class from the request.
|
||||
|
||||
resources:$STANDARD_RESOURCE_CLASS=$N
|
||||
...to override the flavor's value for that resource class with $N
|
||||
"""
|
||||
resource_specs = {key.split("resources:", 1)[-1]: val
|
||||
for key, val in extra_specs.items()
|
||||
if key.startswith("resources:")}
|
||||
resource_keys = set(resource_specs)
|
||||
custom_keys = set([key for key in resource_keys
|
||||
if key.startswith(ResourceClass.CUSTOM_NAMESPACE)])
|
||||
std_keys = resource_keys - custom_keys
|
||||
|
||||
def validate_int(key):
|
||||
val = resource_specs.get(key)
|
||||
try:
|
||||
# Amounts must be integers
|
||||
return int(val)
|
||||
except ValueError:
|
||||
# Not a valid integer
|
||||
LOG.warning("Resource amounts must be integers. Received "
|
||||
"'%(val)s' for key %(key)s.", {"key": key, "val": val})
|
||||
return None
|
||||
|
||||
for custom_key in custom_keys:
|
||||
custom_val = validate_int(custom_key)
|
||||
if custom_val is not None:
|
||||
if custom_val == 0:
|
||||
# Custom resource values must be positive integers
|
||||
LOG.warning("Resource amounts must be positive integers. "
|
||||
"Received '%(val)s' for key %(key)s.",
|
||||
{"key": custom_key, "val": custom_val})
|
||||
continue
|
||||
resources[custom_key] = custom_val
|
||||
for std_key in std_keys:
|
||||
if std_key not in resources:
|
||||
LOG.warning("Received an invalid ResourceClass '%(key)s' in "
|
||||
"extra_specs.", {"key": std_key})
|
||||
continue
|
||||
val = validate_int(std_key)
|
||||
if val is None:
|
||||
# Received an invalid amount. It's already logged, so move on.
|
||||
continue
|
||||
elif val == 0:
|
||||
resources.pop(std_key, None)
|
||||
else:
|
||||
resources[std_key] = val
|
||||
|
||||
|
||||
def resources_from_request_spec(spec_obj):
|
||||
"""Given a RequestSpec object, returns a dict, keyed by resource class
|
||||
name, of requested amounts of those resources.
|
||||
@@ -111,6 +171,8 @@ def resources_from_request_spec(spec_obj):
|
||||
# to avoid asking for disk usage.
|
||||
if requested_disk_gb != 0:
|
||||
resources[fields.ResourceClass.DISK_GB] = requested_disk_gb
|
||||
if "extra_specs" in spec_obj.flavor:
|
||||
_process_extra_specs(spec_obj.flavor.extra_specs, resources)
|
||||
|
||||
return resources
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
|
||||
from nova import objects
|
||||
from nova.scheduler import utils
|
||||
from nova import test
|
||||
@@ -42,3 +44,120 @@ class TestUtils(test.NoDBTestCase):
|
||||
expected_resources = {'VCPU': 1,
|
||||
'MEMORY_MB': 1024}
|
||||
self._test_resources_from_request_spec(flavor, expected_resources)
|
||||
|
||||
def test_get_resources_from_request_spec_custom_resource_class(self):
|
||||
flavor = objects.Flavor(vcpus=1,
|
||||
memory_mb=1024,
|
||||
root_gb=10,
|
||||
ephemeral_gb=5,
|
||||
swap=0,
|
||||
extra_specs={"resources:CUSTOM_TEST_CLASS": 1})
|
||||
expected_resources = {"VCPU": 1,
|
||||
"MEMORY_MB": 1024,
|
||||
"DISK_GB": 15,
|
||||
"CUSTOM_TEST_CLASS": 1}
|
||||
self._test_resources_from_request_spec(flavor, expected_resources)
|
||||
|
||||
def test_get_resources_from_request_spec_override_flavor_amounts(self):
|
||||
flavor = objects.Flavor(vcpus=1,
|
||||
memory_mb=1024,
|
||||
root_gb=10,
|
||||
ephemeral_gb=5,
|
||||
swap=0,
|
||||
extra_specs={
|
||||
"resources:VCPU": 99,
|
||||
"resources:MEMORY_MB": 99,
|
||||
"resources:DISK_GB": 99})
|
||||
expected_resources = {"VCPU": 99,
|
||||
"MEMORY_MB": 99,
|
||||
"DISK_GB": 99}
|
||||
self._test_resources_from_request_spec(flavor, expected_resources)
|
||||
|
||||
def test_get_resources_from_request_spec_remove_flavor_amounts(self):
|
||||
flavor = objects.Flavor(vcpus=1,
|
||||
memory_mb=1024,
|
||||
root_gb=10,
|
||||
ephemeral_gb=5,
|
||||
swap=0,
|
||||
extra_specs={
|
||||
"resources:VCPU": 0,
|
||||
"resources:DISK_GB": 0})
|
||||
expected_resources = {"MEMORY_MB": 1024}
|
||||
self._test_resources_from_request_spec(flavor, expected_resources)
|
||||
|
||||
def test_get_resources_from_request_spec_bad_std_resource_class(self):
|
||||
flavor = objects.Flavor(vcpus=1,
|
||||
memory_mb=1024,
|
||||
root_gb=10,
|
||||
ephemeral_gb=5,
|
||||
swap=0,
|
||||
extra_specs={
|
||||
"resources:DOESNT_EXIST": 0})
|
||||
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||
with mock.patch("nova.scheduler.utils.LOG.warning") as mock_log:
|
||||
utils.resources_from_request_spec(fake_spec)
|
||||
mock_log.assert_called_once()
|
||||
args = mock_log.call_args[0]
|
||||
self.assertEqual(args[0], "Received an invalid ResourceClass "
|
||||
"'%(key)s' in extra_specs.")
|
||||
self.assertEqual(args[1], {"key": "DOESNT_EXIST"})
|
||||
|
||||
def test_get_resources_from_request_spec_bad_value(self):
|
||||
flavor = objects.Flavor(vcpus=1,
|
||||
memory_mb=1024,
|
||||
root_gb=10,
|
||||
ephemeral_gb=5,
|
||||
swap=0,
|
||||
extra_specs={
|
||||
"resources:MEMORY_MB": "bogus"})
|
||||
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||
with mock.patch("nova.scheduler.utils.LOG.warning") as mock_log:
|
||||
utils.resources_from_request_spec(fake_spec)
|
||||
mock_log.assert_called_once()
|
||||
|
||||
def test_get_resources_from_request_spec_zero_cust_amt(self):
|
||||
flavor = objects.Flavor(vcpus=1,
|
||||
memory_mb=1024,
|
||||
root_gb=10,
|
||||
ephemeral_gb=5,
|
||||
swap=0,
|
||||
extra_specs={
|
||||
"resources:CUSTOM_TEST_CLASS": 0})
|
||||
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||
with mock.patch("nova.scheduler.utils.LOG.warning") as mock_log:
|
||||
utils.resources_from_request_spec(fake_spec)
|
||||
mock_log.assert_called_once()
|
||||
|
||||
@mock.patch("nova.scheduler.utils._process_extra_specs")
|
||||
def test_process_extra_specs_called(self, mock_proc):
|
||||
flavor = objects.Flavor(vcpus=1,
|
||||
memory_mb=1024,
|
||||
root_gb=10,
|
||||
ephemeral_gb=5,
|
||||
swap=0,
|
||||
extra_specs={"resources:CUSTOM_TEST_CLASS": 1})
|
||||
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||
utils.resources_from_request_spec(fake_spec)
|
||||
mock_proc.assert_called_once()
|
||||
|
||||
@mock.patch("nova.scheduler.utils._process_extra_specs")
|
||||
def test_process_extra_specs_not_called(self, mock_proc):
|
||||
flavor = objects.Flavor(vcpus=1,
|
||||
memory_mb=1024,
|
||||
root_gb=10,
|
||||
ephemeral_gb=5,
|
||||
swap=0)
|
||||
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||
utils.resources_from_request_spec(fake_spec)
|
||||
mock_proc.assert_not_called()
|
||||
|
||||
def test_process_missing_extra_specs_value(self):
|
||||
flavor = objects.Flavor(
|
||||
vcpus=1,
|
||||
memory_mb=1024,
|
||||
root_gb=10,
|
||||
ephemeral_gb=5,
|
||||
swap=0,
|
||||
extra_specs={"resources:CUSTOM_TEST_CLASS": ""})
|
||||
fake_spec = objects.RequestSpec(flavor=flavor)
|
||||
utils.resources_from_request_spec(fake_spec)
|
||||
|
||||
Reference in New Issue
Block a user