nova/nova/tests/unit/compute/test_provider_config.py

444 lines
17 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import ddt
import fixtures
import microversion_parse
import os
from unittest import mock
from oslo_utils.fixture import uuidsentinel
from oslotest import base
from nova.compute import provider_config
from nova import exception as nova_exc
class SchemaValidationMixin(base.BaseTestCase):
"""This class provides the basic methods for running schema validation test
cases. It can be used along with ddt.file_data to test a specific schema
version using tests defined in yaml files. See SchemaValidationTestCasesV1
for an example of how this was done for schema version 1.
Because decorators can only access class properties of the class they are
defined in (even when overriding values in the subclass), the decorators
need to be placed in the subclass. This is why there are test_ functions in
the subclass that call the run_test_ methods in this class. This should
keep things simple as more schema versions are added.
"""
def setUp(self):
super(SchemaValidationMixin, self).setUp()
self.mock_load_yaml = self.useFixture(
fixtures.MockPatchObject(
provider_config, '_load_yaml_file')).mock
self.mock_LOG = self.useFixture(
fixtures.MockPatchObject(
provider_config, 'LOG')).mock
def set_config(self, config=None):
data = config or {}
self.mock_load_yaml.return_value = data
return data
def run_test_validation_errors(self, config, expected_messages):
self.set_config(config=config)
actual_msg = self.assertRaises(
nova_exc.ProviderConfigException,
provider_config._parse_provider_yaml, 'test_path').message
for msg in expected_messages:
self.assertIn(msg, actual_msg)
def run_test_validation_success(self, config):
reference = self.set_config(config=config)
actual = provider_config._parse_provider_yaml('test_path')
self.assertEqual(reference, actual)
def run_schema_version_matching(
self, min_schema_version, max_schema_version):
# note _load_yaml_file is mocked so the value is not important
# however it may appear in logs messages so changing it could
# result in tests failing unless the expected_messages field
# is updated in the test data.
path = 'test_path'
# test exactly min and max versions are supported
self.set_config(config={
'meta': {'schema_version': str(min_schema_version)}})
provider_config._parse_provider_yaml(path)
self.set_config(config={
'meta': {'schema_version': str(max_schema_version)}})
provider_config._parse_provider_yaml(path)
self.mock_LOG.warning.assert_not_called()
# test max major+1 raises
higher_major = microversion_parse.Version(
major=max_schema_version.major + 1, minor=max_schema_version.minor)
self.set_config(config={'meta': {'schema_version': str(higher_major)}})
self.assertRaises(nova_exc.ProviderConfigException,
provider_config._parse_provider_yaml, path)
# test max major with max minor+1 is logged
higher_minor = microversion_parse.Version(
major=max_schema_version.major, minor=max_schema_version.minor + 1)
expected_log_call = (
"Provider config file [%(path)s] is at schema version "
"%(schema_version)s. Nova supports the major version, but "
"not the minor. Some fields may be ignored." % {
"path": path, "schema_version": higher_minor})
self.set_config(config={'meta': {'schema_version': str(higher_minor)}})
provider_config._parse_provider_yaml(path)
self.mock_LOG.warning.assert_called_once_with(expected_log_call)
@ddt.ddt
class SchemaValidationTestCasesV1(SchemaValidationMixin):
MIN_SCHEMA_VERSION = microversion_parse.Version(1, 0)
MAX_SCHEMA_VERSION = microversion_parse.Version(1, 0)
@ddt.unpack
@ddt.file_data('provider_config_data/v1/validation_error_test_data.yaml')
def test_validation_errors(self, config, expected_messages):
self.run_test_validation_errors(config, expected_messages)
@ddt.unpack
@ddt.file_data('provider_config_data/v1/validation_success_test_data.yaml')
def test_validation_success(self, config):
self.run_test_validation_success(config)
def test_schema_version_matching(self):
self.run_schema_version_matching(self.MIN_SCHEMA_VERSION,
self.MAX_SCHEMA_VERSION)
@ddt.ddt
class ValidateProviderConfigTestCases(base.BaseTestCase):
@ddt.unpack
@ddt.file_data('provider_config_data/validate_provider_good_config.yaml')
def test__validate_provider_good_config(self, sample):
provider_config._validate_provider_config(sample, "fake_path")
@ddt.unpack
@ddt.file_data('provider_config_data/validate_provider_bad_config.yaml')
def test__validate_provider_bad_config(self, sample, expected_messages):
actual_msg = self.assertRaises(
nova_exc.ProviderConfigException,
provider_config._validate_provider_config,
sample, 'fake_path').message
self.assertIn(actual_msg, expected_messages)
@mock.patch.object(provider_config, 'LOG')
def test__validate_provider_config_one_noop_provider(self, mock_log):
expected = {
"providers": [
{
"identification": {"name": "NAME1"},
"inventories": {
"additional": [
{"CUSTOM_RESOURCE_CLASS": {}}
]
}
},
{
"identification": {"name": "NAME_453764"},
"inventories": {
"additional": []
},
"traits": {
"additional": []
}
}
]
}
data = copy.deepcopy(expected)
valid = provider_config._validate_provider_config(data, "fake_path")
mock_log.warning.assert_called_once_with(
"Provider NAME_453764 defined in "
"fake_path has no additional "
"inventories or traits and will be ignored."
)
# assert that _validate_provider_config does not mutate inputs
self.assertEqual(expected, data)
# assert that the first entry in the returned tuple is the full set
# of providers not a copy and is equal to the expected providers.
self.assertIs(data['providers'][0], valid[0])
self.assertEqual(expected['providers'][0], valid[0])
class GetProviderConfigsTestCases(base.BaseTestCase):
@mock.patch.object(provider_config, 'glob')
def test_get_provider_configs_one_file(self, mock_glob):
expected = {
"$COMPUTE_NODE": {
"__source_file": "example_provider.yaml",
"identification": {
"name": "$COMPUTE_NODE"
},
"inventories": {
"additional": [
{
"CUSTOM_EXAMPLE_RESOURCE_CLASS": {
"total": 100,
"reserved": 0,
"min_unit": 1,
"max_unit": 10,
"step_size": 1,
"allocation_ratio": 1.0
}
}
]
},
"traits": {
"additional": [
"CUSTOM_TRAIT_ONE",
"CUSTOM_TRAIT2"
]
}
}
}
example_file = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'provider_config_data/v1/example_provider.yaml')
mock_glob.glob.return_value = [example_file]
actual = provider_config.get_provider_configs('path')
self.assertEqual(expected, actual)
mock_glob.glob.assert_called_with('path/*.yaml')
@mock.patch.object(provider_config, 'glob')
@mock.patch.object(provider_config, '_parse_provider_yaml')
def test_get_provider_configs_one_file_uuid_conflict(
self, mock_parser, mock_glob):
# one config file with conflicting identification
providers = [
{"__source_file": "file1.yaml",
"identification": {
"uuid": uuidsentinel.uuid1
},
"inventories": {
"additional": [
{
"CUSTOM_EXAMPLE_RESOURCE_CLASS1": {
"total": 100,
"reserved": 0,
"min_unit": 1,
"max_unit": 10,
"step_size": 1,
"allocation_ratio": 1
}
}
]
},
"traits": {
"additional": [
"CUSTOM_TRAIT1"
]
}
},
{"__source_file": "file1.yaml",
"identification": {
"uuid": uuidsentinel.uuid1
},
"inventories": {
"additional": [
{
"CUSTOM_EXAMPLE_RESOURCE_CLASS2": {
"total": 100,
"reserved": 0,
"min_unit": 1,
"max_unit": 10,
"step_size": 1,
"allocation_ratio": 1
}
}
]
},
"traits": {
"additional": [
"CUSTOM_TRAIT2"
]
}
}
]
mock_parser.side_effect = [{"providers": providers}]
mock_glob.glob.return_value = ['file1.yaml']
# test that correct error is raised and message matches
error = self.assertRaises(nova_exc.ProviderConfigException,
provider_config.get_provider_configs,
'dummy_path').kwargs['error']
self.assertEqual("Provider %s has multiple definitions in source "
"file(s): ['file1.yaml']." % uuidsentinel.uuid1,
error)
@mock.patch.object(provider_config, 'glob')
@mock.patch.object(provider_config, '_parse_provider_yaml')
def test_get_provider_configs_two_files(self, mock_parser, mock_glob):
expected = {
"EXAMPLE_RESOURCE_PROVIDER1": {
"__source_file": "file1.yaml",
"identification": {
"name": "EXAMPLE_RESOURCE_PROVIDER1"
},
"inventories": {
"additional": [
{
"CUSTOM_EXAMPLE_RESOURCE_CLASS1": {
"total": 100,
"reserved": 0,
"min_unit": 1,
"max_unit": 10,
"step_size": 1,
"allocation_ratio": 1
}
}
]
},
"traits": {
"additional": [
"CUSTOM_TRAIT1"
]
}
},
"EXAMPLE_RESOURCE_PROVIDER2": {
"__source_file": "file2.yaml",
"identification": {
"name": "EXAMPLE_RESOURCE_PROVIDER2"
},
"inventories": {
"additional": [
{
"CUSTOM_EXAMPLE_RESOURCE_CLASS2": {
"total": 100,
"reserved": 0,
"min_unit": 1,
"max_unit": 10,
"step_size": 1,
"allocation_ratio": 1
}
}
]
},
"traits": {
"additional": [
"CUSTOM_TRAIT2"
]
}
}
}
mock_parser.side_effect = [
{"providers": [provider]} for provider in expected.values()]
mock_glob_return = ['file1.yaml', 'file2.yaml']
mock_glob.glob.return_value = mock_glob_return
dummy_path = 'dummy_path'
actual = provider_config.get_provider_configs(dummy_path)
mock_glob.glob.assert_called_once_with(os.path.join(dummy_path,
'*.yaml'))
mock_parser.assert_has_calls([mock.call(param)
for param in mock_glob_return])
self.assertEqual(expected, actual)
@mock.patch.object(provider_config, 'glob')
@mock.patch.object(provider_config, '_parse_provider_yaml')
def test_get_provider_configs_two_files_name_conflict(self, mock_parser,
mock_glob):
# two config files with conflicting identification
configs = {
"EXAMPLE_RESOURCE_PROVIDER1": {
"__source_file": "file1.yaml",
"identification": {
"name": "EXAMPLE_RESOURCE_PROVIDER1"
},
"inventories": {
"additional": [
{
"CUSTOM_EXAMPLE_RESOURCE_CLASS1": {
"total": 100,
"reserved": 0,
"min_unit": 1,
"max_unit": 10,
"step_size": 1,
"allocation_ratio": 1
}
}
]
},
"traits": {
"additional": [
"CUSTOM_TRAIT1"
]
}
},
"EXAMPLE_RESOURCE_PROVIDER2": {
"__source_file": "file2.yaml",
"identification": {
"name": "EXAMPLE_RESOURCE_PROVIDER1"
},
"inventories": {
"additional": [
{
"CUSTOM_EXAMPLE_RESOURCE_CLASS1": {
"total": 100,
"reserved": 0,
"min_unit": 1,
"max_unit": 10,
"step_size": 1,
"allocation_ratio": 1
}
}
]
},
"traits": {
"additional": [
"CUSTOM_TRAIT1"
]
}
}
}
mock_parser.side_effect = [{"providers": [configs[provider]]}
for provider in configs]
mock_glob.glob.return_value = ['file1.yaml', 'file2.yaml']
# test that correct error is raised and message matches
error = self.assertRaises(nova_exc.ProviderConfigException,
provider_config.get_provider_configs,
'dummy_path').kwargs['error']
self.assertEqual("Provider EXAMPLE_RESOURCE_PROVIDER1 has multiple "
"definitions in source file(s): "
"['file1.yaml', 'file2.yaml'].", error)
@mock.patch.object(provider_config, 'LOG')
def test_get_provider_configs_no_configs(self, mock_log):
path = "invalid_path!@#"
actual = provider_config.get_provider_configs(path)
self.assertEqual({}, actual)
mock_log.info.assert_called_once_with(
"No provider configs found in %s. If files are present, "
"ensure the Nova process has access.", path)