Enable cinder storage interface for generic hardware
This patch enables cinder storage interface for generic hardware. It also adds storage_interface field to node resource and driver resource in API and bumps API version to 1.33 so that storage interface can be set and shown via API. Change-Id: I2c74f386291e588a25612f73de08e8367795acff Partial-Bug: #1559691
This commit is contained in:
parent
54d5335edd
commit
b90f7a15fb
@ -2,6 +2,11 @@
|
|||||||
REST API Version History
|
REST API Version History
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
**1.33** (Pike)
|
||||||
|
|
||||||
|
Added ``storage_interface`` field to the node object to allow getting and
|
||||||
|
setting the interface.
|
||||||
|
|
||||||
**1.32** (Pike)
|
**1.32** (Pike)
|
||||||
|
|
||||||
Added new endpoints for remote volume configuration:
|
Added new endpoints for remote volume configuration:
|
||||||
|
@ -64,6 +64,18 @@ _VENDOR_METHODS = {}
|
|||||||
_RAID_PROPERTIES = {}
|
_RAID_PROPERTIES = {}
|
||||||
|
|
||||||
|
|
||||||
|
def hide_fields_in_newer_versions(obj):
|
||||||
|
"""This method hides fields that were added in newer API versions.
|
||||||
|
|
||||||
|
Certain fields were introduced at certain API versions.
|
||||||
|
These fields are only made available when the request's API version
|
||||||
|
matches or exceeds the versions when these fields were introduced.
|
||||||
|
"""
|
||||||
|
if not api_utils.allow_storage_interface():
|
||||||
|
obj.default_storage_interface = wsme.Unset
|
||||||
|
obj.enabled_storage_interfaces = wsme.Unset
|
||||||
|
|
||||||
|
|
||||||
class Driver(base.APIBase):
|
class Driver(base.APIBase):
|
||||||
"""API representation of a driver."""
|
"""API representation of a driver."""
|
||||||
|
|
||||||
@ -91,6 +103,7 @@ class Driver(base.APIBase):
|
|||||||
default_network_interface = wtypes.text
|
default_network_interface = wtypes.text
|
||||||
default_power_interface = wtypes.text
|
default_power_interface = wtypes.text
|
||||||
default_raid_interface = wtypes.text
|
default_raid_interface = wtypes.text
|
||||||
|
default_storage_interface = wtypes.text
|
||||||
default_vendor_interface = wtypes.text
|
default_vendor_interface = wtypes.text
|
||||||
|
|
||||||
"""A list of enabled interfaces for a hardware type"""
|
"""A list of enabled interfaces for a hardware type"""
|
||||||
@ -102,6 +115,7 @@ class Driver(base.APIBase):
|
|||||||
enabled_network_interfaces = [wtypes.text]
|
enabled_network_interfaces = [wtypes.text]
|
||||||
enabled_power_interfaces = [wtypes.text]
|
enabled_power_interfaces = [wtypes.text]
|
||||||
enabled_raid_interfaces = [wtypes.text]
|
enabled_raid_interfaces = [wtypes.text]
|
||||||
|
enabled_storage_interfaces = [wtypes.text]
|
||||||
enabled_vendor_interfaces = [wtypes.text]
|
enabled_vendor_interfaces = [wtypes.text]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -172,6 +186,7 @@ class Driver(base.APIBase):
|
|||||||
setattr(driver, 'default_%s_interface' % iface_type, None)
|
setattr(driver, 'default_%s_interface' % iface_type, None)
|
||||||
setattr(driver, 'enabled_%s_interfaces' % iface_type, None)
|
setattr(driver, 'enabled_%s_interfaces' % iface_type, None)
|
||||||
|
|
||||||
|
hide_fields_in_newer_versions(driver)
|
||||||
return driver
|
return driver
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -153,6 +153,9 @@ def hide_fields_in_newer_versions(obj):
|
|||||||
for field in api_utils.V31_FIELDS:
|
for field in api_utils.V31_FIELDS:
|
||||||
setattr(obj, field, wsme.Unset)
|
setattr(obj, field, wsme.Unset)
|
||||||
|
|
||||||
|
if not api_utils.allow_storage_interface():
|
||||||
|
obj.storage_interface = wsme.Unset
|
||||||
|
|
||||||
|
|
||||||
def update_state_in_older_versions(obj):
|
def update_state_in_older_versions(obj):
|
||||||
"""Change provision state names for API backwards compatibility.
|
"""Change provision state names for API backwards compatibility.
|
||||||
@ -844,6 +847,9 @@ class Node(base.APIBase):
|
|||||||
raid_interface = wsme.wsattr(wtypes.text)
|
raid_interface = wsme.wsattr(wtypes.text)
|
||||||
"""The raid interface to be used for this node"""
|
"""The raid interface to be used for this node"""
|
||||||
|
|
||||||
|
storage_interface = wsme.wsattr(wtypes.text)
|
||||||
|
"""The storage interface to be used for this node"""
|
||||||
|
|
||||||
vendor_interface = wsme.wsattr(wtypes.text)
|
vendor_interface = wsme.wsattr(wtypes.text)
|
||||||
"""The vendor interface to be used for this node"""
|
"""The vendor interface to be used for this node"""
|
||||||
|
|
||||||
@ -995,7 +1001,8 @@ class Node(base.APIBase):
|
|||||||
boot_interface=None, console_interface=None,
|
boot_interface=None, console_interface=None,
|
||||||
deploy_interface=None, inspect_interface=None,
|
deploy_interface=None, inspect_interface=None,
|
||||||
management_interface=None, power_interface=None,
|
management_interface=None, power_interface=None,
|
||||||
raid_interface=None, vendor_interface=None)
|
raid_interface=None, vendor_interface=None,
|
||||||
|
storage_interface=None)
|
||||||
# NOTE(matty_dubs): The chassis_uuid getter() is based on the
|
# NOTE(matty_dubs): The chassis_uuid getter() is based on the
|
||||||
# _chassis_uuid variable:
|
# _chassis_uuid variable:
|
||||||
sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
|
sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
|
||||||
@ -1602,6 +1609,10 @@ class NodesController(rest.RestController):
|
|||||||
if getattr(node, field) is not wsme.Unset:
|
if getattr(node, field) is not wsme.Unset:
|
||||||
raise exception.NotAcceptable()
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
|
if (not api_utils.allow_storage_interface() and
|
||||||
|
node.storage_interface is not wtypes.Unset):
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
# NOTE(deva): get_topic_for checks if node.driver is in the hash ring
|
# NOTE(deva): get_topic_for checks if node.driver is in the hash ring
|
||||||
# and raises NoValidHost if it is not.
|
# and raises NoValidHost if it is not.
|
||||||
# We need to ensure that node has a UUID before it can
|
# We need to ensure that node has a UUID before it can
|
||||||
@ -1666,6 +1677,10 @@ class NodesController(rest.RestController):
|
|||||||
if api_utils.get_patch_values(patch, '/%s' % field):
|
if api_utils.get_patch_values(patch, '/%s' % field):
|
||||||
raise exception.NotAcceptable()
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
|
s_interface = api_utils.get_patch_values(patch, '/storage_interface')
|
||||||
|
if s_interface and not api_utils.allow_storage_interface():
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
rpc_node = api_utils.get_rpc_node(node_ident)
|
||||||
|
|
||||||
remove_inst_uuid_patch = [{'op': 'remove', 'path': '/instance_uuid'}]
|
remove_inst_uuid_patch = [{'op': 'remove', 'path': '/instance_uuid'}]
|
||||||
|
@ -300,6 +300,8 @@ def check_allowed_fields(fields):
|
|||||||
if not allow_dynamic_interfaces():
|
if not allow_dynamic_interfaces():
|
||||||
if set(V31_FIELDS).intersection(set(fields)):
|
if set(V31_FIELDS).intersection(set(fields)):
|
||||||
raise exception.NotAcceptable()
|
raise exception.NotAcceptable()
|
||||||
|
if 'storage_interface' in fields and not allow_storage_interface():
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
|
|
||||||
def check_allowed_portgroup_fields(fields):
|
def check_allowed_portgroup_fields(fields):
|
||||||
@ -557,6 +559,15 @@ def allow_volume():
|
|||||||
return pecan.request.version.minor >= versions.MINOR_32_VOLUME
|
return pecan.request.version.minor >= versions.MINOR_32_VOLUME
|
||||||
|
|
||||||
|
|
||||||
|
def allow_storage_interface():
|
||||||
|
"""Check if we should support storage_interface node field.
|
||||||
|
|
||||||
|
Version 1.33 of the API added support for storage interfaces.
|
||||||
|
"""
|
||||||
|
return (pecan.request.version.minor >=
|
||||||
|
versions.MINOR_33_STORAGE_INTERFACE)
|
||||||
|
|
||||||
|
|
||||||
def get_controller_reserved_names(cls):
|
def get_controller_reserved_names(cls):
|
||||||
"""Get reserved names for a given controller.
|
"""Get reserved names for a given controller.
|
||||||
|
|
||||||
|
@ -63,6 +63,7 @@ BASE_VERSION = 1
|
|||||||
# v1.30: Add dynamic driver interactions.
|
# v1.30: Add dynamic driver interactions.
|
||||||
# v1.31: Add dynamic interfaces fields to node.
|
# v1.31: Add dynamic interfaces fields to node.
|
||||||
# v1.32: Add volume support.
|
# v1.32: Add volume support.
|
||||||
|
# v1.33: Add node storage interface
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -97,11 +98,12 @@ MINOR_29_INJECT_NMI = 29
|
|||||||
MINOR_30_DYNAMIC_DRIVERS = 30
|
MINOR_30_DYNAMIC_DRIVERS = 30
|
||||||
MINOR_31_DYNAMIC_INTERFACES = 31
|
MINOR_31_DYNAMIC_INTERFACES = 31
|
||||||
MINOR_32_VOLUME = 32
|
MINOR_32_VOLUME = 32
|
||||||
|
MINOR_33_STORAGE_INTERFACE = 33
|
||||||
|
|
||||||
# When adding another version, update MINOR_MAX_VERSION and also update
|
# When adding another version, update MINOR_MAX_VERSION and also update
|
||||||
# doc/source/dev/webapi-version-history.rst with a detailed explanation of
|
# doc/source/dev/webapi-version-history.rst with a detailed explanation of
|
||||||
# what the version has changed.
|
# what the version has changed.
|
||||||
MINOR_MAX_VERSION = MINOR_32_VOLUME
|
MINOR_MAX_VERSION = MINOR_33_STORAGE_INTERFACE
|
||||||
|
|
||||||
# String representations of the minor and maximum versions
|
# String representations of the minor and maximum versions
|
||||||
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||||
|
@ -26,6 +26,8 @@ from ironic.drivers.modules.network import neutron
|
|||||||
from ironic.drivers.modules.network import noop as noop_net
|
from ironic.drivers.modules.network import noop as noop_net
|
||||||
from ironic.drivers.modules import noop
|
from ironic.drivers.modules import noop
|
||||||
from ironic.drivers.modules import pxe
|
from ironic.drivers.modules import pxe
|
||||||
|
from ironic.drivers.modules.storage import cinder
|
||||||
|
from ironic.drivers.modules.storage import noop as noop_storage
|
||||||
|
|
||||||
|
|
||||||
class GenericHardware(hardware_type.AbstractHardwareType):
|
class GenericHardware(hardware_type.AbstractHardwareType):
|
||||||
@ -65,6 +67,11 @@ class GenericHardware(hardware_type.AbstractHardwareType):
|
|||||||
# default. Hence, even if AgentRAID is enabled, NoRAID is the default.
|
# default. Hence, even if AgentRAID is enabled, NoRAID is the default.
|
||||||
return [noop.NoRAID, agent.AgentRAID]
|
return [noop.NoRAID, agent.AgentRAID]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_storage_interfaces(self):
|
||||||
|
"""List of supported storage interfaces."""
|
||||||
|
return [noop_storage.NoopStorage, cinder.CinderStorage]
|
||||||
|
|
||||||
|
|
||||||
class ManualManagementHardware(GenericHardware):
|
class ManualManagementHardware(GenericHardware):
|
||||||
"""Hardware type that uses manual power and boot management.
|
"""Hardware type that uses manual power and boot management.
|
||||||
|
@ -48,7 +48,7 @@ class TestListDrivers(base.BaseApiTest):
|
|||||||
self.dbapi.register_conductor_hardware_interfaces(
|
self.dbapi.register_conductor_hardware_interfaces(
|
||||||
c.id, self.d3, 'deploy', ['iscsi', 'direct'], 'direct')
|
c.id, self.d3, 'deploy', ['iscsi', 'direct'], 'direct')
|
||||||
|
|
||||||
def _test_drivers(self, use_dynamic, detail=False):
|
def _test_drivers(self, use_dynamic, detail=False, storage_if=False):
|
||||||
self.register_fake_conductors()
|
self.register_fake_conductors()
|
||||||
headers = {}
|
headers = {}
|
||||||
expected = [
|
expected = [
|
||||||
@ -58,7 +58,10 @@ class TestListDrivers(base.BaseApiTest):
|
|||||||
]
|
]
|
||||||
expected = sorted(expected, key=lambda d: d['name'])
|
expected = sorted(expected, key=lambda d: d['name'])
|
||||||
if use_dynamic:
|
if use_dynamic:
|
||||||
headers[api_base.Version.string] = '1.30'
|
if storage_if:
|
||||||
|
headers[api_base.Version.string] = '1.33'
|
||||||
|
else:
|
||||||
|
headers[api_base.Version.string] = '1.30'
|
||||||
|
|
||||||
path = '/drivers'
|
path = '/drivers'
|
||||||
if detail:
|
if detail:
|
||||||
@ -83,6 +86,12 @@ class TestListDrivers(base.BaseApiTest):
|
|||||||
# as this case can't actually happen.
|
# as this case can't actually happen.
|
||||||
if detail:
|
if detail:
|
||||||
self.assertIn('default_deploy_interface', d)
|
self.assertIn('default_deploy_interface', d)
|
||||||
|
if storage_if:
|
||||||
|
self.assertIn('default_storage_interface', d)
|
||||||
|
self.assertIn('enabled_storage_interfaces', d)
|
||||||
|
else:
|
||||||
|
self.assertNotIn('default_storage_interface', d)
|
||||||
|
self.assertNotIn('enabled_storage_interfaces', d)
|
||||||
else:
|
else:
|
||||||
# ensure we don't spill these fields into driver listing
|
# ensure we don't spill these fields into driver listing
|
||||||
# one should be enough
|
# one should be enough
|
||||||
@ -94,7 +103,7 @@ class TestListDrivers(base.BaseApiTest):
|
|||||||
def test_drivers_with_dynamic(self):
|
def test_drivers_with_dynamic(self):
|
||||||
self._test_drivers(True)
|
self._test_drivers(True)
|
||||||
|
|
||||||
def test_drivers_with_dynamic_detailed(self):
|
def _test_drivers_with_dynamic_detailed(self, storage_if=False):
|
||||||
with mock.patch.object(self.dbapi, 'list_hardware_type_interfaces',
|
with mock.patch.object(self.dbapi, 'list_hardware_type_interfaces',
|
||||||
autospec=True) as mock_hw:
|
autospec=True) as mock_hw:
|
||||||
mock_hw.return_value = [
|
mock_hw.return_value = [
|
||||||
@ -112,7 +121,13 @@ class TestListDrivers(base.BaseApiTest):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
self._test_drivers(True, detail=True)
|
self._test_drivers(True, detail=True, storage_if=storage_if)
|
||||||
|
|
||||||
|
def test_drivers_with_dynamic_detailed(self):
|
||||||
|
self._test_drivers_with_dynamic_detailed()
|
||||||
|
|
||||||
|
def test_drivers_with_dynamic_detailed_storage_interface(self):
|
||||||
|
self._test_drivers_with_dynamic_detailed(storage_if=True)
|
||||||
|
|
||||||
def _test_drivers_type_filter(self, requested_type):
|
def _test_drivers_type_filter(self, requested_type):
|
||||||
self.register_fake_conductors()
|
self.register_fake_conductors()
|
||||||
@ -163,7 +178,8 @@ class TestListDrivers(base.BaseApiTest):
|
|||||||
self.assertEqual([], data['drivers'])
|
self.assertEqual([], data['drivers'])
|
||||||
|
|
||||||
@mock.patch.object(rpcapi.ConductorAPI, 'get_driver_properties')
|
@mock.patch.object(rpcapi.ConductorAPI, 'get_driver_properties')
|
||||||
def _test_drivers_get_one_ok(self, use_dynamic, mock_driver_properties):
|
def _test_drivers_get_one_ok(self, use_dynamic, mock_driver_properties,
|
||||||
|
storage_if=False):
|
||||||
# get_driver_properties mock is required by validate_link()
|
# get_driver_properties mock is required by validate_link()
|
||||||
self.register_fake_conductors()
|
self.register_fake_conductors()
|
||||||
|
|
||||||
@ -176,8 +192,14 @@ class TestListDrivers(base.BaseApiTest):
|
|||||||
driver_type = 'classic'
|
driver_type = 'classic'
|
||||||
hosts = [self.h1]
|
hosts = [self.h1]
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
if storage_if:
|
||||||
|
headers[api_base.Version.string] = '1.33'
|
||||||
|
else:
|
||||||
|
headers[api_base.Version.string] = '1.30'
|
||||||
|
|
||||||
data = self.get_json('/drivers/%s' % driver,
|
data = self.get_json('/drivers/%s' % driver,
|
||||||
headers={api_base.Version.string: '1.30'})
|
headers=headers)
|
||||||
|
|
||||||
self.assertEqual(driver, data['name'])
|
self.assertEqual(driver, data['name'])
|
||||||
self.assertEqual(sorted(hosts), sorted(data['hosts']))
|
self.assertEqual(sorted(hosts), sorted(data['hosts']))
|
||||||
@ -186,8 +208,7 @@ class TestListDrivers(base.BaseApiTest):
|
|||||||
|
|
||||||
if use_dynamic:
|
if use_dynamic:
|
||||||
for iface in driver_base.ALL_INTERFACES:
|
for iface in driver_base.ALL_INTERFACES:
|
||||||
# NOTE(jroll) we don't expose storage interface yet
|
if storage_if or iface != 'storage':
|
||||||
if iface != 'storage':
|
|
||||||
self.assertIn('default_%s_interface' % iface, data)
|
self.assertIn('default_%s_interface' % iface, data)
|
||||||
self.assertIn('enabled_%s_interfaces' % iface, data)
|
self.assertIn('enabled_%s_interfaces' % iface, data)
|
||||||
self.assertIsNotNone(data['default_deploy_interface'])
|
self.assertIsNotNone(data['default_deploy_interface'])
|
||||||
@ -204,7 +225,7 @@ class TestListDrivers(base.BaseApiTest):
|
|||||||
def test_drivers_get_one_ok_classic(self):
|
def test_drivers_get_one_ok_classic(self):
|
||||||
self._test_drivers_get_one_ok(False)
|
self._test_drivers_get_one_ok(False)
|
||||||
|
|
||||||
def test_drivers_get_one_ok_dynamic(self):
|
def _test_drivers_get_one_ok_dynamic(self, storage_if=False):
|
||||||
with mock.patch.object(self.dbapi, 'list_hardware_type_interfaces',
|
with mock.patch.object(self.dbapi, 'list_hardware_type_interfaces',
|
||||||
autospec=True) as mock_hw:
|
autospec=True) as mock_hw:
|
||||||
mock_hw.return_value = [
|
mock_hw.return_value = [
|
||||||
@ -222,9 +243,15 @@ class TestListDrivers(base.BaseApiTest):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
self._test_drivers_get_one_ok(True)
|
self._test_drivers_get_one_ok(True, storage_if=storage_if)
|
||||||
mock_hw.assert_called_once_with([self.d3])
|
mock_hw.assert_called_once_with([self.d3])
|
||||||
|
|
||||||
|
def test_drivers_get_one_ok_dynamic(self):
|
||||||
|
self._test_drivers_get_one_ok_dynamic()
|
||||||
|
|
||||||
|
def test_drivers_get_one_ok_dynamic_storage_interface(self):
|
||||||
|
self._test_drivers_get_one_ok_dynamic(storage_if=True)
|
||||||
|
|
||||||
def test_driver_properties_hidden_in_lower_version(self):
|
def test_driver_properties_hidden_in_lower_version(self):
|
||||||
self.register_fake_conductors()
|
self.register_fake_conductors()
|
||||||
data = self.get_json('/drivers/%s' % self.d1,
|
data = self.get_json('/drivers/%s' % self.d1,
|
||||||
|
@ -116,6 +116,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertNotIn('resource_class', data['nodes'][0])
|
self.assertNotIn('resource_class', data['nodes'][0])
|
||||||
for field in api_utils.V31_FIELDS:
|
for field in api_utils.V31_FIELDS:
|
||||||
self.assertNotIn(field, data['nodes'][0])
|
self.assertNotIn(field, data['nodes'][0])
|
||||||
|
self.assertNotIn('storage_interface', data['nodes'][0])
|
||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
self.assertNotIn('chassis_id', data['nodes'][0])
|
self.assertNotIn('chassis_id', data['nodes'][0])
|
||||||
|
|
||||||
@ -149,6 +150,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertIn('resource_class', data)
|
self.assertIn('resource_class', data)
|
||||||
for field in api_utils.V31_FIELDS:
|
for field in api_utils.V31_FIELDS:
|
||||||
self.assertIn(field, data)
|
self.assertIn(field, data)
|
||||||
|
self.assertIn('storage_interface', data)
|
||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
self.assertNotIn('chassis_id', data)
|
self.assertNotIn('chassis_id', data)
|
||||||
|
|
||||||
@ -168,6 +170,14 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
for field in api_utils.V31_FIELDS:
|
for field in api_utils.V31_FIELDS:
|
||||||
self.assertNotIn(field, data)
|
self.assertNotIn(field, data)
|
||||||
|
|
||||||
|
def test_node_storage_interface_hidden_in_lower_version(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
storage_interface='cinder')
|
||||||
|
data = self.get_json(
|
||||||
|
'/nodes/%s' % node.uuid,
|
||||||
|
headers={api_base.Version.string: '1.32'})
|
||||||
|
self.assertNotIn('storage_interface', data)
|
||||||
|
|
||||||
def test_get_one_custom_fields(self):
|
def test_get_one_custom_fields(self):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
chassis_id=self.chassis.id)
|
chassis_id=self.chassis.id)
|
||||||
@ -267,6 +277,25 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
for field in api_utils.V31_FIELDS:
|
for field in api_utils.V31_FIELDS:
|
||||||
self.assertIn(field, response)
|
self.assertIn(field, response)
|
||||||
|
|
||||||
|
def test_get_storage_interface_fields_invalid_api_version(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
chassis_id=self.chassis.id)
|
||||||
|
fields = 'storage_interface'
|
||||||
|
response = self.get_json(
|
||||||
|
'/nodes/%s?fields=%s' % (node.uuid, fields),
|
||||||
|
headers={api_base.Version.string: str(api_v1.MIN_VER)},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
|
|
||||||
|
def test_get_storage_interface_fields(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
chassis_id=self.chassis.id)
|
||||||
|
fields = 'storage_interface'
|
||||||
|
response = self.get_json(
|
||||||
|
'/nodes/%s?fields=%s' % (node.uuid, fields),
|
||||||
|
headers={api_base.Version.string: str(api_v1.MAX_VER)})
|
||||||
|
self.assertIn('storage_interface', response)
|
||||||
|
|
||||||
def test_detail(self):
|
def test_detail(self):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
chassis_id=self.chassis.id)
|
chassis_id=self.chassis.id)
|
||||||
@ -294,6 +323,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertIn('resource_class', data['nodes'][0])
|
self.assertIn('resource_class', data['nodes'][0])
|
||||||
for field in api_utils.V31_FIELDS:
|
for field in api_utils.V31_FIELDS:
|
||||||
self.assertIn(field, data['nodes'][0])
|
self.assertIn(field, data['nodes'][0])
|
||||||
|
self.assertIn('storage_interface', data['nodes'][0])
|
||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
self.assertNotIn('chassis_id', data['nodes'][0])
|
self.assertNotIn('chassis_id', data['nodes'][0])
|
||||||
|
|
||||||
@ -413,6 +443,17 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
headers={api_base.Version.string: "1.32"})
|
headers={api_base.Version.string: "1.32"})
|
||||||
self.assertIn('volume', data)
|
self.assertIn('volume', data)
|
||||||
|
|
||||||
|
def test_hide_fields_in_newer_versions_storage_interface(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
storage_interface='cinder')
|
||||||
|
data = self.get_json(
|
||||||
|
'/nodes/detail', headers={api_base.Version.string: '1.32'})
|
||||||
|
self.assertNotIn('storage_interface', data['nodes'][0])
|
||||||
|
new_data = self.get_json(
|
||||||
|
'/nodes/detail', headers={api_base.Version.string: '1.33'})
|
||||||
|
self.assertEqual(node.storage_interface,
|
||||||
|
new_data['nodes'][0]["storage_interface"])
|
||||||
|
|
||||||
def test_many(self):
|
def test_many(self):
|
||||||
nodes = []
|
nodes = []
|
||||||
for id in range(5):
|
for id in range(5):
|
||||||
@ -2013,6 +2054,35 @@ class TestPatch(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
|
||||||
|
def test_update_storage_interface(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid())
|
||||||
|
self.mock_update_node.return_value = node
|
||||||
|
storage_interface = 'cinder'
|
||||||
|
headers = {api_base.Version.string: str(api_v1.MAX_VER)}
|
||||||
|
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||||
|
[{'path': '/storage_interface',
|
||||||
|
'value': storage_interface,
|
||||||
|
'op': 'add'}],
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.OK, response.status_code)
|
||||||
|
|
||||||
|
def test_update_storage_interface_old_api(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid())
|
||||||
|
self.mock_update_node.return_value = node
|
||||||
|
storage_interface = 'cinder'
|
||||||
|
headers = {api_base.Version.string: '1.32'}
|
||||||
|
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||||
|
[{'path': '/storage_interface',
|
||||||
|
'value': storage_interface,
|
||||||
|
'op': 'add'}],
|
||||||
|
headers=headers,
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||||
|
|
||||||
|
|
||||||
def _create_node_locally(node):
|
def _create_node_locally(node):
|
||||||
driver_factory.check_and_update_node_interfaces(node)
|
driver_factory.check_and_update_node_interfaces(node)
|
||||||
@ -2122,6 +2192,12 @@ class TestPost(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertTrue(response.json['error_message'])
|
self.assertTrue(response.json['error_message'])
|
||||||
|
|
||||||
|
def test_create_node_explicit_storage_interface(self):
|
||||||
|
headers = {api_base.Version.string: '1.33'}
|
||||||
|
result = self._test_create_node(headers=headers,
|
||||||
|
storage_interface='cinder')
|
||||||
|
self.assertEqual('cinder', result['storage_interface'])
|
||||||
|
|
||||||
def test_create_node_name_empty_invalid(self):
|
def test_create_node_name_empty_invalid(self):
|
||||||
ndict = test_api_utils.post_get_test_node(name='')
|
ndict = test_api_utils.post_get_test_node(name='')
|
||||||
response = self.post_json('/nodes', ndict,
|
response = self.post_json('/nodes', ndict,
|
||||||
@ -2508,6 +2584,22 @@ class TestPost(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
|
|
||||||
|
def test_create_node_storage_interface_old_api_version(self):
|
||||||
|
headers = {api_base.Version.string: '1.32'}
|
||||||
|
ndict = test_api_utils.post_get_test_node(storage_interface='cinder')
|
||||||
|
response = self.post_json('/nodes', ndict, headers=headers,
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
|
|
||||||
|
def test_create_node_invalid_storage_interface(self):
|
||||||
|
ndict = test_api_utils.post_get_test_node(storage_interface='foo')
|
||||||
|
response = self.post_json('/nodes', ndict, expect_errors=True,
|
||||||
|
headers={api_base.Version.string:
|
||||||
|
str(api_v1.MAX_VER)})
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||||
|
|
||||||
|
|
||||||
class TestDelete(test_api_base.BaseApiTest):
|
class TestDelete(test_api_base.BaseApiTest):
|
||||||
|
|
||||||
|
@ -419,6 +419,13 @@ class TestApiUtils(base.TestCase):
|
|||||||
mock_request.version.minor = 31
|
mock_request.version.minor = 31
|
||||||
self.assertFalse(utils.allow_volume())
|
self.assertFalse(utils.allow_volume())
|
||||||
|
|
||||||
|
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||||
|
def test_allow_storage_interface(self, mock_request):
|
||||||
|
mock_request.version.minor = 33
|
||||||
|
self.assertTrue(utils.allow_storage_interface())
|
||||||
|
mock_request.version.minor = 32
|
||||||
|
self.assertFalse(utils.allow_storage_interface())
|
||||||
|
|
||||||
|
|
||||||
class TestNodeIdent(base.TestCase):
|
class TestNodeIdent(base.TestCase):
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ from ironic.drivers.modules import ipmitool
|
|||||||
from ironic.drivers.modules import iscsi_deploy
|
from ironic.drivers.modules import iscsi_deploy
|
||||||
from ironic.drivers.modules import noop
|
from ironic.drivers.modules import noop
|
||||||
from ironic.drivers.modules import pxe
|
from ironic.drivers.modules import pxe
|
||||||
|
from ironic.drivers.modules.storage import cinder
|
||||||
|
from ironic.drivers.modules.storage import noop as noop_storage
|
||||||
from ironic.tests.unit.db import base as db_base
|
from ironic.tests.unit.db import base as db_base
|
||||||
from ironic.tests.unit.objects import utils as obj_utils
|
from ironic.tests.unit.objects import utils as obj_utils
|
||||||
|
|
||||||
@ -34,17 +36,36 @@ class IPMIHardwareTestCase(db_base.DbTestCase):
|
|||||||
enabled_console_interfaces=['no-console'],
|
enabled_console_interfaces=['no-console'],
|
||||||
enabled_vendor_interfaces=['ipmitool', 'no-vendor'])
|
enabled_vendor_interfaces=['ipmitool', 'no-vendor'])
|
||||||
|
|
||||||
|
def _validate_interfaces(self, task, **kwargs):
|
||||||
|
self.assertIsInstance(
|
||||||
|
task.driver.management,
|
||||||
|
kwargs.get('management', ipmitool.IPMIManagement))
|
||||||
|
self.assertIsInstance(
|
||||||
|
task.driver.power,
|
||||||
|
kwargs.get('power', ipmitool.IPMIPower))
|
||||||
|
self.assertIsInstance(
|
||||||
|
task.driver.boot,
|
||||||
|
kwargs.get('boot', pxe.PXEBoot))
|
||||||
|
self.assertIsInstance(
|
||||||
|
task.driver.deploy,
|
||||||
|
kwargs.get('deploy', iscsi_deploy.ISCSIDeploy))
|
||||||
|
self.assertIsInstance(
|
||||||
|
task.driver.console,
|
||||||
|
kwargs.get('console', noop.NoConsole))
|
||||||
|
self.assertIsInstance(
|
||||||
|
task.driver.raid,
|
||||||
|
kwargs.get('raid', noop.NoRAID))
|
||||||
|
self.assertIsInstance(
|
||||||
|
task.driver.vendor,
|
||||||
|
kwargs.get('vendor', ipmitool.VendorPassthru))
|
||||||
|
self.assertIsInstance(
|
||||||
|
task.driver.storage,
|
||||||
|
kwargs.get('storage', noop_storage.NoopStorage))
|
||||||
|
|
||||||
def test_default_interfaces(self):
|
def test_default_interfaces(self):
|
||||||
node = obj_utils.create_test_node(self.context, driver='ipmi')
|
node = obj_utils.create_test_node(self.context, driver='ipmi')
|
||||||
with task_manager.acquire(self.context, node.id) as task:
|
with task_manager.acquire(self.context, node.id) as task:
|
||||||
self.assertIsInstance(task.driver.management,
|
self._validate_interfaces(task)
|
||||||
ipmitool.IPMIManagement)
|
|
||||||
self.assertIsInstance(task.driver.power, ipmitool.IPMIPower)
|
|
||||||
self.assertIsInstance(task.driver.boot, pxe.PXEBoot)
|
|
||||||
self.assertIsInstance(task.driver.deploy, iscsi_deploy.ISCSIDeploy)
|
|
||||||
self.assertIsInstance(task.driver.console, noop.NoConsole)
|
|
||||||
self.assertIsInstance(task.driver.raid, noop.NoRAID)
|
|
||||||
self.assertIsInstance(task.driver.vendor, ipmitool.VendorPassthru)
|
|
||||||
|
|
||||||
def test_override_with_shellinabox(self):
|
def test_override_with_shellinabox(self):
|
||||||
self.config(enabled_console_interfaces=['ipmitool-shellinabox',
|
self.config(enabled_console_interfaces=['ipmitool-shellinabox',
|
||||||
@ -56,15 +77,20 @@ class IPMIHardwareTestCase(db_base.DbTestCase):
|
|||||||
console_interface='ipmitool-shellinabox',
|
console_interface='ipmitool-shellinabox',
|
||||||
vendor_interface='no-vendor')
|
vendor_interface='no-vendor')
|
||||||
with task_manager.acquire(self.context, node.id) as task:
|
with task_manager.acquire(self.context, node.id) as task:
|
||||||
self.assertIsInstance(task.driver.management,
|
self._validate_interfaces(
|
||||||
ipmitool.IPMIManagement)
|
task,
|
||||||
self.assertIsInstance(task.driver.power, ipmitool.IPMIPower)
|
deploy=agent.AgentDeploy,
|
||||||
self.assertIsInstance(task.driver.boot, pxe.PXEBoot)
|
console=ipmitool.IPMIShellinaboxConsole,
|
||||||
self.assertIsInstance(task.driver.deploy, agent.AgentDeploy)
|
raid=agent.AgentRAID,
|
||||||
self.assertIsInstance(task.driver.console,
|
vendor=noop.NoVendor)
|
||||||
ipmitool.IPMIShellinaboxConsole)
|
|
||||||
self.assertIsInstance(task.driver.raid, agent.AgentRAID)
|
def test_override_with_cinder_storage(self):
|
||||||
self.assertIsInstance(task.driver.vendor, noop.NoVendor)
|
self.config(enabled_storage_interfaces=['noop', 'cinder'])
|
||||||
|
node = obj_utils.create_test_node(
|
||||||
|
self.context, driver='ipmi',
|
||||||
|
storage_interface='cinder')
|
||||||
|
with task_manager.acquire(self.context, node.id) as task:
|
||||||
|
self._validate_interfaces(task, storage=cinder.CinderStorage)
|
||||||
|
|
||||||
|
|
||||||
class IPMIClassicDriversTestCase(testtools.TestCase):
|
class IPMIClassicDriversTestCase(testtools.TestCase):
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds version 1.33 of the REST API, which exposes the ``storage_interface``
|
||||||
|
field of the node resource. This version also exposes
|
||||||
|
``default_storage_interface`` and ``enable_storage_interfaces`` fields
|
||||||
|
of the driver resource.
|
||||||
|
|
||||||
|
There are 2 available storage interfaces:
|
||||||
|
|
||||||
|
* ``noop``: This interface provides nothing regarding storage.
|
||||||
|
|
||||||
|
* ``cinder``: This interface enables a node to attach and detach volumes
|
||||||
|
by leveraging cinder API.
|
||||||
|
|
||||||
|
A storage interface can be set when creating or updating a node. Enabled
|
||||||
|
storage interfaces are defined via the
|
||||||
|
``[DEFAULT]/enabled_storage_interfaces`` configuration option. A default
|
||||||
|
interface for a created node can be specified with
|
||||||
|
``[DEFAULT]/default_storage_interface`` configuration option.
|
Loading…
x
Reference in New Issue
Block a user