Merge "Make end-points discoverable via Ironic API"
This commit is contained in:
commit
fb1e3d1e40
@ -32,6 +32,12 @@ always requests the newest supported API version.
|
|||||||
API Versions History
|
API Versions History
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
**1.14**
|
||||||
|
|
||||||
|
Make the following endpoints discoverable via Ironic API:
|
||||||
|
* '/v1/nodes/<UUID or logical name>/states'
|
||||||
|
* '/v1/drivers/<driver name>/properties'
|
||||||
|
|
||||||
**1.13**
|
**1.13**
|
||||||
|
|
||||||
Add a new verb ``abort`` to the API used to abort nodes in
|
Add a new verb ``abort`` to the API used to abort nodes in
|
||||||
|
@ -70,6 +70,9 @@ class Driver(base.APIBase):
|
|||||||
links = wsme.wsattr([link.Link], readonly=True)
|
links = wsme.wsattr([link.Link], readonly=True)
|
||||||
"""A list containing self and bookmark links"""
|
"""A list containing self and bookmark links"""
|
||||||
|
|
||||||
|
properties = wsme.wsattr([link.Link], readonly=True)
|
||||||
|
"""A list containing links to driver properties"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def convert_with_links(name, hosts):
|
def convert_with_links(name, hosts):
|
||||||
driver = Driver()
|
driver = Driver()
|
||||||
@ -84,6 +87,16 @@ class Driver(base.APIBase):
|
|||||||
'drivers', name,
|
'drivers', name,
|
||||||
bookmark=True)
|
bookmark=True)
|
||||||
]
|
]
|
||||||
|
if api_utils.allow_links_node_states_and_driver_properties():
|
||||||
|
driver.properties = [
|
||||||
|
link.Link.make_link('self',
|
||||||
|
pecan.request.public_url,
|
||||||
|
'drivers', name + "/properties"),
|
||||||
|
link.Link.make_link('bookmark',
|
||||||
|
pecan.request.public_url,
|
||||||
|
'drivers', name + "/properties",
|
||||||
|
bookmark=True)
|
||||||
|
]
|
||||||
return driver
|
return driver
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -625,6 +625,9 @@ class Node(base.APIBase):
|
|||||||
ports = wsme.wsattr([link.Link], readonly=True)
|
ports = wsme.wsattr([link.Link], readonly=True)
|
||||||
"""Links to the collection of ports on this node"""
|
"""Links to the collection of ports on this node"""
|
||||||
|
|
||||||
|
states = wsme.wsattr([link.Link], readonly=True)
|
||||||
|
"""Links to endpoint for retrieving and setting node states"""
|
||||||
|
|
||||||
# NOTE(deva): "conductor_affinity" shouldn't be presented on the
|
# NOTE(deva): "conductor_affinity" shouldn't be presented on the
|
||||||
# API because it's an internal value. Don't add it here.
|
# API because it's an internal value. Don't add it here.
|
||||||
|
|
||||||
@ -648,7 +651,8 @@ class Node(base.APIBase):
|
|||||||
setattr(self, 'chassis_uuid', kwargs.get('chassis_id', wtypes.Unset))
|
setattr(self, 'chassis_uuid', kwargs.get('chassis_id', wtypes.Unset))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convert_with_links(node, url, fields=None, show_password=True):
|
def _convert_with_links(node, url, fields=None, show_password=True,
|
||||||
|
show_states_links=True):
|
||||||
# NOTE(lucasagomes): Since we are able to return a specified set of
|
# NOTE(lucasagomes): Since we are able to return a specified set of
|
||||||
# fields the "uuid" can be unset, so we need to save it in another
|
# fields the "uuid" can be unset, so we need to save it in another
|
||||||
# variable to use when building the links
|
# variable to use when building the links
|
||||||
@ -662,6 +666,12 @@ class Node(base.APIBase):
|
|||||||
node_uuid + "/ports",
|
node_uuid + "/ports",
|
||||||
bookmark=True)
|
bookmark=True)
|
||||||
]
|
]
|
||||||
|
if show_states_links:
|
||||||
|
node.states = [link.Link.make_link('self', url, 'nodes',
|
||||||
|
node_uuid + "/states"),
|
||||||
|
link.Link.make_link('bookmark', url, 'nodes',
|
||||||
|
node_uuid + "/states",
|
||||||
|
bookmark=True)]
|
||||||
|
|
||||||
if not show_password and node.driver_info != wtypes.Unset:
|
if not show_password and node.driver_info != wtypes.Unset:
|
||||||
node.driver_info = ast.literal_eval(strutils.mask_password(
|
node.driver_info = ast.literal_eval(strutils.mask_password(
|
||||||
@ -689,9 +699,12 @@ class Node(base.APIBase):
|
|||||||
assert_juno_provision_state_name(node)
|
assert_juno_provision_state_name(node)
|
||||||
hide_fields_in_newer_versions(node)
|
hide_fields_in_newer_versions(node)
|
||||||
show_password = pecan.request.context.show_password
|
show_password = pecan.request.context.show_password
|
||||||
|
show_states_links = (
|
||||||
|
api_utils.allow_links_node_states_and_driver_properties())
|
||||||
return cls._convert_with_links(node, pecan.request.public_url,
|
return cls._convert_with_links(node, pecan.request.public_url,
|
||||||
fields=fields,
|
fields=fields,
|
||||||
show_password=show_password)
|
show_password=show_password,
|
||||||
|
show_states_links=show_states_links)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sample(cls, expand=True):
|
def sample(cls, expand=True):
|
||||||
|
@ -231,3 +231,13 @@ def allow_raid_config():
|
|||||||
Version 1.12 of the API allows RAID configuration for the node.
|
Version 1.12 of the API allows RAID configuration for the node.
|
||||||
"""
|
"""
|
||||||
return pecan.request.version.minor >= versions.MINOR_12_RAID_CONFIG
|
return pecan.request.version.minor >= versions.MINOR_12_RAID_CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
def allow_links_node_states_and_driver_properties():
|
||||||
|
"""Check if links are displayable.
|
||||||
|
|
||||||
|
Version 1.14 of the API allows the display of links to node states
|
||||||
|
and driver properties.
|
||||||
|
"""
|
||||||
|
return (pecan.request.version.minor >=
|
||||||
|
versions.MINOR_14_LINKS_NODESTATES_DRIVERPROPERTIES)
|
||||||
|
@ -41,6 +41,9 @@ BASE_VERSION = 1
|
|||||||
# v1.11: Nodes appear in ENROLL state by default
|
# v1.11: Nodes appear in ENROLL state by default
|
||||||
# v1.12: Add support for RAID
|
# v1.12: Add support for RAID
|
||||||
# v1.13: Add 'abort' verb to CLEANWAIT
|
# v1.13: Add 'abort' verb to CLEANWAIT
|
||||||
|
# v1.14: Make the following endpoints discoverable via API:
|
||||||
|
# 1. '/v1/nodes/<uuid>/states'
|
||||||
|
# 2. '/v1/drivers/<driver-name>/properties'
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -56,11 +59,12 @@ MINOR_10_UNRESTRICTED_NODE_NAME = 10
|
|||||||
MINOR_11_ENROLL_STATE = 11
|
MINOR_11_ENROLL_STATE = 11
|
||||||
MINOR_12_RAID_CONFIG = 12
|
MINOR_12_RAID_CONFIG = 12
|
||||||
MINOR_13_ABORT_VERB = 13
|
MINOR_13_ABORT_VERB = 13
|
||||||
|
MINOR_14_LINKS_NODESTATES_DRIVERPROPERTIES = 14
|
||||||
|
|
||||||
# When adding another version, update MINOR_MAX_VERSION and also update
|
# When adding another version, update MINOR_MAX_VERSION and also update
|
||||||
# doc/source/webapi/v1.rst with a detailed explanation of what the version has
|
# doc/source/webapi/v1.rst with a detailed explanation of what the version has
|
||||||
# changed.
|
# changed.
|
||||||
MINOR_MAX_VERSION = MINOR_13_ABORT_VERB
|
MINOR_MAX_VERSION = MINOR_14_LINKS_NODESTATES_DRIVERPROPERTIES
|
||||||
|
|
||||||
# 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)
|
||||||
|
@ -64,13 +64,25 @@ class TestListDrivers(base.FunctionalTest):
|
|||||||
self.assertThat(data['drivers'], HasLength(0))
|
self.assertThat(data['drivers'], HasLength(0))
|
||||||
self.assertEqual([], data['drivers'])
|
self.assertEqual([], data['drivers'])
|
||||||
|
|
||||||
def test_drivers_get_one_ok(self):
|
@mock.patch.object(rpcapi.ConductorAPI, 'get_driver_properties')
|
||||||
|
def test_drivers_get_one_ok(self, mock_driver_properties):
|
||||||
|
# get_driver_properties mock is required by validate_link()
|
||||||
self.register_fake_conductors()
|
self.register_fake_conductors()
|
||||||
data = self.get_json('/drivers/%s' % self.d1)
|
data = self.get_json('/drivers/%s' % self.d1,
|
||||||
|
headers={api_base.Version.string: '1.14'})
|
||||||
self.assertEqual(self.d1, data['name'])
|
self.assertEqual(self.d1, data['name'])
|
||||||
self.assertEqual([self.h1], data['hosts'])
|
self.assertEqual([self.h1], data['hosts'])
|
||||||
|
self.assertIn('properties', data.keys())
|
||||||
self.validate_link(data['links'][0]['href'])
|
self.validate_link(data['links'][0]['href'])
|
||||||
self.validate_link(data['links'][1]['href'])
|
self.validate_link(data['links'][1]['href'])
|
||||||
|
self.validate_link(data['properties'][0]['href'])
|
||||||
|
self.validate_link(data['properties'][1]['href'])
|
||||||
|
|
||||||
|
def test_driver_properties_hidden_in_lower_version(self):
|
||||||
|
self.register_fake_conductors()
|
||||||
|
data = self.get_json('/drivers/%s' % self.d1,
|
||||||
|
headers={api_base.Version.string: '1.8'})
|
||||||
|
self.assertNotIn('properties', data.keys())
|
||||||
|
|
||||||
def test_drivers_get_one_not_found(self):
|
def test_drivers_get_one_not_found(self):
|
||||||
response = self.get_json('/drivers/%s' % self.d1, expect_errors=True)
|
response = self.get_json('/drivers/%s' % self.d1, expect_errors=True)
|
||||||
|
@ -134,9 +134,18 @@ class TestListNodes(test_api_base.FunctionalTest):
|
|||||||
self.assertIn('inspection_finished_at', data)
|
self.assertIn('inspection_finished_at', data)
|
||||||
self.assertIn('inspection_started_at', data)
|
self.assertIn('inspection_started_at', data)
|
||||||
self.assertIn('clean_step', data)
|
self.assertIn('clean_step', data)
|
||||||
|
self.assertIn('states', data)
|
||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
self.assertNotIn('chassis_id', data)
|
self.assertNotIn('chassis_id', data)
|
||||||
|
|
||||||
|
def test_node_states_field_hidden_in_lower_version(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
chassis_id=self.chassis.id)
|
||||||
|
data = self.get_json(
|
||||||
|
'/nodes/%s' % node.uuid,
|
||||||
|
headers={api_base.Version.string: '1.8'})
|
||||||
|
self.assertNotIn('states', 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)
|
||||||
|
@ -79,6 +79,13 @@ class TestApiUtils(base.TestCase):
|
|||||||
self.assertRaises(exception.NotAcceptable,
|
self.assertRaises(exception.NotAcceptable,
|
||||||
utils.check_allow_specify_fields, ['foo'])
|
utils.check_allow_specify_fields, ['foo'])
|
||||||
|
|
||||||
|
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||||
|
def test_allow_links_node_states_and_driver_properties(self, mock_request):
|
||||||
|
mock_request.version.minor = 14
|
||||||
|
self.assertTrue(utils.allow_links_node_states_and_driver_properties())
|
||||||
|
mock_request.version.minor = 10
|
||||||
|
self.assertFalse(utils.allow_links_node_states_and_driver_properties())
|
||||||
|
|
||||||
|
|
||||||
class TestNodeIdent(base.TestCase):
|
class TestNodeIdent(base.TestCase):
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user