Introduce storage adapter mapping conf object

This patch introduces a oslo config object that can be used by the
physical_storage_adapter_mappings configuration option.

The config object validates the config option along a regex and
returns a tuple (adapter_id, port).

It accepts adapter-ids in upper and lower case. But for users
it returns the adapter-id always in lower case (like desired
by the HMC web services API).

Some parts of this code can be removed once they are available
in a new os-dpm release (e.g. the MultiStringWithKwargsType).

Change-Id: I03319412ad15c608ecc43b0421f0cae42bd6b5dc
Partial-Bug: #1663369
This commit is contained in:
Andreas Scheuring 2017-03-08 16:45:49 +01:00
parent 4e9526f42b
commit 6f3261499c
5 changed files with 248 additions and 0 deletions

30
nova_dpm/conf/cfg.py Normal file
View File

@ -0,0 +1,30 @@
# Copyright 2017 IBM Corp. All Rights Reserved.
#
# 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.
from oslo_config import cfg
from nova_dpm.conf import types
class MultiStorageAdapterMappingOpt(cfg.MultiOpt):
def __init__(self, name, **kwargs):
"""Option with DPM AdapterPortMapping type
Option with ``type`` :class:`StorageAdapterMappingType`
:param name: the option's name
"""
super(MultiStorageAdapterMappingOpt, self).__init__(
name, item_type=types.StorageAdapterMappingType(), **kwargs)

45
nova_dpm/conf/types.py Normal file
View File

@ -0,0 +1,45 @@
# Copyright 2017 IBM Corp. All Rights Reserved.
#
# 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.
from oslo_config import cfg
OBJECT_ID_REGEX = "[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}"
# A FCP adapter can have up to 4 ports
PORT_REGEX = "[0-3]"
MAPPING_REGEX = "^" + OBJECT_ID_REGEX + ":" + PORT_REGEX + "$"
class StorageAdapterMappingType(cfg.types.String):
"""Storage adapter mapping type.
Values are returned as tuple (adapter-id, port)
"""
def __init__(self, type_name='multi valued'):
super(StorageAdapterMappingType, self).__init__(
type_name=type_name,
regex=MAPPING_REGEX,
ignore_case=True
)
def format_defaults(self, default, sample_default=None):
multi = cfg.types.MultiString()
return multi.format_defaults(default, sample_default)
def __call__(self, value):
val = super(StorageAdapterMappingType, self).__call__(value)
# No extra checking for None required here.
# The regex ensures the format of the value in the super class.
split_result = val.split(':')
return split_result[0].lower(), split_result[1]

View File

View File

@ -0,0 +1,97 @@
# Copyright 2016 IBM Corp. All Rights Reserved.
#
# 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 os
from oslo_config import cfg
from oslo_config.fixture import Config
import tempfile
from nova.test import TestCase
from nova_dpm.conf.cfg import MultiStorageAdapterMappingOpt
from nova_dpm.conf.types import StorageAdapterMappingType
class TestStorageAdapterMappingOpt(TestCase):
def create_tempfiles(self, files, ext='.conf'):
"""Create temp files for testing
:param files: A list of files of tuples in the format
[('filename1', 'line1\nline2\n'), ('filename2', 'line1\nline2\n')]
:param ext: The file extension to be used
:return: List of file paths
paths[0] = path of filename1
paths[1] = path of filename2
"""
# TODO(andreas_s): Make a mixin in os-dpm and move this there
# (also required in nova-dpm)
tempfiles = []
for (basename, contents) in files:
if not os.path.isabs(basename):
# create all the tempfiles in a tempdir
tmpdir = tempfile.mkdtemp()
path = os.path.join(tmpdir, basename + ext)
# the path can start with a subdirectory so create
# it if it doesn't exist yet
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
else:
path = basename + ext
fd = os.open(path, os.O_CREAT | os.O_WRONLY)
tempfiles.append(path)
try:
os.write(fd, contents.encode('utf-8'))
finally:
os.close(fd)
return tempfiles
def test_object(self):
opt = MultiStorageAdapterMappingOpt("mapping", help="foo-help")
self.assertEqual("foo-help", opt.help)
self.assertEqual("mapping", opt.name)
self.assertEqual(StorageAdapterMappingType, type(opt.type))
def test_config_single(self):
mapping = ["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:0"]
opt = MultiStorageAdapterMappingOpt("mapping")
cfg.CONF.register_opt(opt)
self.flags(mapping=mapping)
self.assertEqual([("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "0")],
cfg.CONF.mapping)
def test_config_multiple_set_override(self):
mapping = ["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:0",
"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb:1"]
opt = MultiStorageAdapterMappingOpt("mapping")
cfg.CONF.register_opt(opt)
self.flags(mapping=mapping)
expected_mapping = [
("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "0"),
("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "1")]
self.assertEqual(expected_mapping, cfg.CONF.mapping)
def test_config_multiple_config_file(self):
mapping = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:0"
other_mapping = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb:1"
paths = self.create_tempfiles([
('test', '[DEFAULT]\nmapping = ' + mapping + '\n'
'mapping = ' + other_mapping + '\n')])
conf = self.useFixture(Config())
conf.register_opt(MultiStorageAdapterMappingOpt("mapping"))
conf.conf(args=['--config-file', paths[0]])
self.assertEqual([("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "0"),
("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "1")],
cfg.CONF.mapping)

View File

@ -0,0 +1,76 @@
# Copyright 2017 IBM Corp. All Rights Reserved.
#
# 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.
from nova.test import TestCase
from nova_dpm.conf.types import MAPPING_REGEX
from nova_dpm.conf.types import StorageAdapterMappingType
VALID_DPM_OBJECT_ID = "fa1f2466-12df-311a-804c-4ed2cc1d656b"
VALID_DPM_OBJECT_ID_UC = "FA1F2466-12DF-311A-804C-4ED2CC1D656B"
VALID_STORAGE_MAPPING = VALID_DPM_OBJECT_ID + ":0"
class TestStorageAdapterMappingType(TestCase):
def setUp(self):
super(TestStorageAdapterMappingType, self).setUp()
self.conf_type = StorageAdapterMappingType()
def test_valid_mapping(self):
adapter_id, port = self.conf_type(VALID_STORAGE_MAPPING)
self.assertEqual("0", port)
self.assertEqual(VALID_DPM_OBJECT_ID, adapter_id)
def test_valid_port_values_ok(self):
for p in ["0", "1", "2", "3"]:
adapter_id, port = self.conf_type(VALID_DPM_OBJECT_ID + ":" + p)
self.assertEqual(p, port)
def test_upper_case_to_lower_case_adapter_id(self):
adapter_id, port = self.conf_type(VALID_DPM_OBJECT_ID_UC + ":0")
self.assertEqual("0", port)
self.assertEqual(VALID_DPM_OBJECT_ID, adapter_id)
def test_empty_value_fail(self):
self.assertRaises(ValueError, self.conf_type, '')
def test_invalid_value_fail(self):
self.assertRaises(ValueError, self.conf_type, 'foobar')
def test_missing_port_fail(self):
self.assertRaises(ValueError, self.conf_type, VALID_DPM_OBJECT_ID)
def test_missing_port_fail_2(self):
self.assertRaises(ValueError, self.conf_type,
VALID_DPM_OBJECT_ID + ":")
def test_invalid_port_fail(self):
self.assertRaises(ValueError, self.conf_type,
VALID_DPM_OBJECT_ID + ":4")
def test_invalid_port_type_fail(self):
self.assertRaises(ValueError, self.conf_type,
VALID_DPM_OBJECT_ID + ":a")
def test_invalid_adapter_id_fail(self):
self.assertRaises(ValueError, self.conf_type, "foo:1")
def test_repr(self):
self.assertEqual(
"String(regex='" + MAPPING_REGEX + "')",
repr(self.conf_type))
def test_equal(self):
self.assertTrue(
StorageAdapterMappingType() == StorageAdapterMappingType())