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:
EdLeafe
2017-06-12 23:26:40 +00:00
committed by Matt Riedemann
parent a222f03c84
commit 1dc93d00b0
3 changed files with 185 additions and 0 deletions

View File

@@ -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.")

View File

@@ -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

View File

@@ -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)