summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2015-09-23 02:36:10 +0000
committerGerrit Code Review <review@openstack.org>2015-09-23 02:36:10 +0000
commitfb1e3d1e40697dd6b94d0037394a3f5d28cc7138 (patch)
tree912d4f5b6e309412edec1f59cf566711fe83e8ac
parentb893b0ed3195f687f5be627dbecee4d0cb736712 (diff)
parent3274cdeed452e6fb37872001b615a0b37ed69d65 (diff)
downloadironic-fb1e3d1e40697dd6b94d0037394a3f5d28cc7138.tar.gz
Merge "Make end-points discoverable via Ironic API"
-rw-r--r--doc/source/webapi/v1.rst6
-rw-r--r--ironic/api/controllers/v1/driver.py13
-rw-r--r--ironic/api/controllers/v1/node.py17
-rw-r--r--ironic/api/controllers/v1/utils.py10
-rw-r--r--ironic/api/controllers/v1/versions.py6
-rw-r--r--ironic/tests/api/v1/test_drivers.py16
-rw-r--r--ironic/tests/api/v1/test_nodes.py9
-rw-r--r--ironic/tests/api/v1/test_utils.py7
8 files changed, 79 insertions, 5 deletions
diff --git a/doc/source/webapi/v1.rst b/doc/source/webapi/v1.rst
index d40f591dc..506aa1acb 100644
--- a/doc/source/webapi/v1.rst
+++ b/doc/source/webapi/v1.rst
@@ -32,6 +32,12 @@ always requests the newest supported API version.
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**
Add a new verb ``abort`` to the API used to abort nodes in
diff --git a/ironic/api/controllers/v1/driver.py b/ironic/api/controllers/v1/driver.py
index 74299028b..f84c1aad2 100644
--- a/ironic/api/controllers/v1/driver.py
+++ b/ironic/api/controllers/v1/driver.py
@@ -70,6 +70,9 @@ class Driver(base.APIBase):
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing self and bookmark links"""
+ properties = wsme.wsattr([link.Link], readonly=True)
+ """A list containing links to driver properties"""
+
@staticmethod
def convert_with_links(name, hosts):
driver = Driver()
@@ -84,6 +87,16 @@ class Driver(base.APIBase):
'drivers', name,
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
@classmethod
diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
index 664016b9c..d95298f71 100644
--- a/ironic/api/controllers/v1/node.py
+++ b/ironic/api/controllers/v1/node.py
@@ -625,6 +625,9 @@ class Node(base.APIBase):
ports = wsme.wsattr([link.Link], readonly=True)
"""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
# 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))
@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
# fields the "uuid" can be unset, so we need to save it in another
# variable to use when building the links
@@ -662,6 +666,12 @@ class Node(base.APIBase):
node_uuid + "/ports",
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:
node.driver_info = ast.literal_eval(strutils.mask_password(
@@ -689,9 +699,12 @@ class Node(base.APIBase):
assert_juno_provision_state_name(node)
hide_fields_in_newer_versions(node)
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,
fields=fields,
- show_password=show_password)
+ show_password=show_password,
+ show_states_links=show_states_links)
@classmethod
def sample(cls, expand=True):
diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py
index 323e1b66c..538ca4540 100644
--- a/ironic/api/controllers/v1/utils.py
+++ b/ironic/api/controllers/v1/utils.py
@@ -231,3 +231,13 @@ def allow_raid_config():
Version 1.12 of the API allows RAID configuration for the node.
"""
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)
diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py
index 4ca3ddf66..a9367d3ff 100644
--- a/ironic/api/controllers/v1/versions.py
+++ b/ironic/api/controllers/v1/versions.py
@@ -41,6 +41,9 @@ BASE_VERSION = 1
# v1.11: Nodes appear in ENROLL state by default
# v1.12: Add support for RAID
# 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_1_INITIAL_VERSION = 1
@@ -56,11 +59,12 @@ MINOR_10_UNRESTRICTED_NODE_NAME = 10
MINOR_11_ENROLL_STATE = 11
MINOR_12_RAID_CONFIG = 12
MINOR_13_ABORT_VERB = 13
+MINOR_14_LINKS_NODESTATES_DRIVERPROPERTIES = 14
# 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
# changed.
-MINOR_MAX_VERSION = MINOR_13_ABORT_VERB
+MINOR_MAX_VERSION = MINOR_14_LINKS_NODESTATES_DRIVERPROPERTIES
# String representations of the minor and maximum versions
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
diff --git a/ironic/tests/api/v1/test_drivers.py b/ironic/tests/api/v1/test_drivers.py
index 989c6a71a..5e548a063 100644
--- a/ironic/tests/api/v1/test_drivers.py
+++ b/ironic/tests/api/v1/test_drivers.py
@@ -64,13 +64,25 @@ class TestListDrivers(base.FunctionalTest):
self.assertThat(data['drivers'], HasLength(0))
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()
- 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.h1], data['hosts'])
+ self.assertIn('properties', data.keys())
self.validate_link(data['links'][0]['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):
response = self.get_json('/drivers/%s' % self.d1, expect_errors=True)
diff --git a/ironic/tests/api/v1/test_nodes.py b/ironic/tests/api/v1/test_nodes.py
index de09e5b42..37a641123 100644
--- a/ironic/tests/api/v1/test_nodes.py
+++ b/ironic/tests/api/v1/test_nodes.py
@@ -134,9 +134,18 @@ class TestListNodes(test_api_base.FunctionalTest):
self.assertIn('inspection_finished_at', data)
self.assertIn('inspection_started_at', data)
self.assertIn('clean_step', data)
+ self.assertIn('states', data)
# never expose the chassis_id
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):
node = obj_utils.create_test_node(self.context,
chassis_id=self.chassis.id)
diff --git a/ironic/tests/api/v1/test_utils.py b/ironic/tests/api/v1/test_utils.py
index 079307641..f8c0f417f 100644
--- a/ironic/tests/api/v1/test_utils.py
+++ b/ironic/tests/api/v1/test_utils.py
@@ -79,6 +79,13 @@ class TestApiUtils(base.TestCase):
self.assertRaises(exception.NotAcceptable,
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):