baremetal: implement validate_node
Change-Id: I2cc71931e0352f70dacb479512cdb4fb7cb011dc
This commit is contained in:
parent
5abdc60590
commit
a7489106b4
@ -24,6 +24,7 @@ Node Operations
|
|||||||
.. automethod:: openstack.baremetal.v1._proxy.Proxy.nodes
|
.. automethod:: openstack.baremetal.v1._proxy.Proxy.nodes
|
||||||
.. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_provision_state
|
.. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_provision_state
|
||||||
.. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_nodes_provision_state
|
.. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_nodes_provision_state
|
||||||
|
.. automethod:: openstack.baremetal.v1._proxy.Proxy.validate_node
|
||||||
|
|
||||||
Port Operations
|
Port Operations
|
||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^
|
||||||
|
@ -10,3 +10,11 @@ The ``Node`` class inherits from :class:`~openstack.resource.Resource`.
|
|||||||
|
|
||||||
.. autoclass:: openstack.baremetal.v1.node.Node
|
.. autoclass:: openstack.baremetal.v1.node.Node
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
The ValidationResult Class
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The ``ValidationResult`` class represents the result of a validation.
|
||||||
|
|
||||||
|
.. autoclass:: openstack.baremetal.v1.node.ValidationResult
|
||||||
|
:members:
|
||||||
|
@ -325,6 +325,23 @@ class Proxy(proxy.Proxy):
|
|||||||
{'nodes': ', '.join(n.id for n in remaining),
|
{'nodes': ', '.join(n.id for n in remaining),
|
||||||
'target': expected_state})
|
'target': expected_state})
|
||||||
|
|
||||||
|
def validate_node(self, node, required=('boot', 'deploy', 'power')):
|
||||||
|
"""Validate required information on a node.
|
||||||
|
|
||||||
|
:param node: The value can be either the name or ID of a node or
|
||||||
|
a :class:`~openstack.baremetal.v1.node.Node` instance.
|
||||||
|
:param required: List of interfaces that are required to pass
|
||||||
|
validation. The default value is the list of minimum required
|
||||||
|
interfaces for provisioning.
|
||||||
|
|
||||||
|
:return: dict mapping interface names to
|
||||||
|
:class:`~openstack.baremetal.v1.node.ValidationResult` objects.
|
||||||
|
:raises: :exc:`~openstack.exceptions.ValidationException` if validation
|
||||||
|
fails for a required interface.
|
||||||
|
"""
|
||||||
|
res = self._get_resource(_node.Node, node)
|
||||||
|
return res.validate(self, required=required)
|
||||||
|
|
||||||
def delete_node(self, node, ignore_missing=True):
|
def delete_node(self, node, ignore_missing=True):
|
||||||
"""Delete a node.
|
"""Delete a node.
|
||||||
|
|
||||||
|
@ -21,6 +21,20 @@ from openstack import utils
|
|||||||
_logger = _log.setup_logging('openstack')
|
_logger = _log.setup_logging('openstack')
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationResult(object):
|
||||||
|
"""Result of a single interface validation.
|
||||||
|
|
||||||
|
:ivar result: Result of a validation, ``True`` for success, ``False`` for
|
||||||
|
failure, ``None`` for unsupported interface.
|
||||||
|
:ivar reason: If ``result`` is ``False`` or ``None``, explanation of
|
||||||
|
the result.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, result, reason):
|
||||||
|
self.result = result
|
||||||
|
self.reason = reason
|
||||||
|
|
||||||
|
|
||||||
class Node(resource.Resource):
|
class Node(resource.Resource):
|
||||||
|
|
||||||
resources_key = 'nodes'
|
resources_key = 'nodes'
|
||||||
@ -443,6 +457,48 @@ class Node(resource.Resource):
|
|||||||
exceptions.raise_from_response(response, error_message=msg)
|
exceptions.raise_from_response(response, error_message=msg)
|
||||||
return [vif['id'] for vif in response.json()['vifs']]
|
return [vif['id'] for vif in response.json()['vifs']]
|
||||||
|
|
||||||
|
def validate(self, session, required=('boot', 'deploy', 'power')):
|
||||||
|
"""Validate required information on a node.
|
||||||
|
|
||||||
|
:param session: The session to use for making this request.
|
||||||
|
:type session: :class:`~keystoneauth1.adapter.Adapter`
|
||||||
|
:param required: List of interfaces that are required to pass
|
||||||
|
validation. The default value is the list of minimum required
|
||||||
|
interfaces for provisioning.
|
||||||
|
|
||||||
|
:return: dict mapping interface names to :class:`ValidationResult`
|
||||||
|
objects.
|
||||||
|
:raises: :exc:`~openstack.exceptions.ValidationException` if validation
|
||||||
|
fails for a required interface.
|
||||||
|
"""
|
||||||
|
session = self._get_session(session)
|
||||||
|
version = self._get_microversion_for(session, 'fetch')
|
||||||
|
|
||||||
|
request = self._prepare_request(requires_id=True)
|
||||||
|
request.url = utils.urljoin(request.url, 'validate')
|
||||||
|
response = session.get(request.url, headers=request.headers,
|
||||||
|
microversion=version)
|
||||||
|
|
||||||
|
msg = ("Failed to validate node {node}".format(node=self.id))
|
||||||
|
exceptions.raise_from_response(response, error_message=msg)
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if required:
|
||||||
|
failed = [
|
||||||
|
'%s (%s)' % (key, value.get('reason', 'no reason'))
|
||||||
|
for key, value in result.items()
|
||||||
|
if key in required and not value.get('result')
|
||||||
|
]
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
raise exceptions.ValidationException(
|
||||||
|
'Validation failed for required interfaces of node {node}:'
|
||||||
|
' {failures}'.format(node=self.id,
|
||||||
|
failures=', '.join(failed)))
|
||||||
|
|
||||||
|
return {key: ValidationResult(value.get('result'), value.get('reason'))
|
||||||
|
for key, value in result.items()}
|
||||||
|
|
||||||
|
|
||||||
class NodeDetail(Node):
|
class NodeDetail(Node):
|
||||||
|
|
||||||
|
@ -9856,20 +9856,9 @@ class OpenStackCloud(_normalize.Normalizer):
|
|||||||
return [self.get_port(vif) for vif in vif_ids]
|
return [self.get_port(vif) for vif in vif_ids]
|
||||||
|
|
||||||
def validate_node(self, uuid):
|
def validate_node(self, uuid):
|
||||||
# TODO(TheJulia): There are soooooo many other interfaces
|
# TODO(dtantsur): deprecate this short method in favor of a fully
|
||||||
# that we can support validating, while these are essential,
|
# written validate_machine call.
|
||||||
# we should support more.
|
self.baremetal.validate_node(uuid)
|
||||||
# TODO(TheJulia): Add a doc string :(
|
|
||||||
msg = ("Failed to query the API for validation status of "
|
|
||||||
"node {node_id}").format(node_id=uuid)
|
|
||||||
url = '/nodes/{node_id}/validate'.format(node_id=uuid)
|
|
||||||
ifaces = self._baremetal_client.get(url, error_message=msg)
|
|
||||||
|
|
||||||
if not ifaces['deploy'] or not ifaces['power']:
|
|
||||||
raise exc.OpenStackCloudException(
|
|
||||||
"ironic node %s failed to validate. "
|
|
||||||
"(deploy: %s, power: %s)" % (ifaces['deploy'],
|
|
||||||
ifaces['power']))
|
|
||||||
|
|
||||||
def node_set_provision_state(self,
|
def node_set_provision_state(self,
|
||||||
name_or_id,
|
name_or_id,
|
||||||
|
@ -222,3 +222,7 @@ class ConfigException(SDKException):
|
|||||||
|
|
||||||
class NotSupported(SDKException):
|
class NotSupported(SDKException):
|
||||||
"""Request cannot be performed by any supported API version."""
|
"""Request cannot be performed by any supported API version."""
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationException(SDKException):
|
||||||
|
"""Validation failed for resource."""
|
||||||
|
@ -82,6 +82,14 @@ class TestBareMetalNode(base.BaseBaremetalTest):
|
|||||||
wait=True)
|
wait=True)
|
||||||
self.assertEqual(node.provision_state, 'available')
|
self.assertEqual(node.provision_state, 'available')
|
||||||
|
|
||||||
|
def test_node_validate(self):
|
||||||
|
node = self.create_node()
|
||||||
|
# Fake hardware passes validation for all interfaces
|
||||||
|
result = self.conn.baremetal.validate_node(node)
|
||||||
|
for iface in ('boot', 'deploy', 'management', 'power'):
|
||||||
|
self.assertTrue(result[iface].result)
|
||||||
|
self.assertFalse(result[iface].reason)
|
||||||
|
|
||||||
def test_node_negative_non_existing(self):
|
def test_node_negative_non_existing(self):
|
||||||
uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971"
|
uuid = "5c9dcd04-2073-49bc-9618-99ae634d8971"
|
||||||
self.assertRaises(exceptions.ResourceNotFound,
|
self.assertRaises(exceptions.ResourceNotFound,
|
||||||
|
@ -432,3 +432,60 @@ class TestNodeVif(base.TestCase):
|
|||||||
self.assertRaises(exceptions.NotSupported,
|
self.assertRaises(exceptions.NotSupported,
|
||||||
self.node.list_vifs,
|
self.node.list_vifs,
|
||||||
self.session)
|
self.session)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(exceptions, 'raise_from_response', mock.Mock())
|
||||||
|
@mock.patch.object(node.Node, '_get_session', lambda self, x: x)
|
||||||
|
class TestNodeValidate(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestNodeValidate, self).setUp()
|
||||||
|
self.session = mock.Mock(spec=adapter.Adapter)
|
||||||
|
self.session.default_microversion = '1.28'
|
||||||
|
self.node = node.Node(**FAKE)
|
||||||
|
|
||||||
|
def test_validate_ok(self):
|
||||||
|
self.session.get.return_value.json.return_value = {
|
||||||
|
'boot': {'result': True},
|
||||||
|
'console': {'result': False, 'reason': 'Not configured'},
|
||||||
|
'deploy': {'result': True},
|
||||||
|
'inspect': {'result': None, 'reason': 'Not supported'},
|
||||||
|
'power': {'result': True}
|
||||||
|
}
|
||||||
|
result = self.node.validate(self.session)
|
||||||
|
for iface in ('boot', 'deploy', 'power'):
|
||||||
|
self.assertTrue(result[iface].result)
|
||||||
|
self.assertFalse(result[iface].reason)
|
||||||
|
for iface in ('console', 'inspect'):
|
||||||
|
self.assertIsNot(True, result[iface].result)
|
||||||
|
self.assertTrue(result[iface].reason)
|
||||||
|
|
||||||
|
def test_validate_failed(self):
|
||||||
|
self.session.get.return_value.json.return_value = {
|
||||||
|
'boot': {'result': False},
|
||||||
|
'console': {'result': False, 'reason': 'Not configured'},
|
||||||
|
'deploy': {'result': False, 'reason': 'No deploy for you'},
|
||||||
|
'inspect': {'result': None, 'reason': 'Not supported'},
|
||||||
|
'power': {'result': True}
|
||||||
|
}
|
||||||
|
self.assertRaisesRegex(exceptions.ValidationException,
|
||||||
|
'No deploy for you',
|
||||||
|
self.node.validate, self.session)
|
||||||
|
|
||||||
|
def test_validate_no_failure(self):
|
||||||
|
self.session.get.return_value.json.return_value = {
|
||||||
|
'boot': {'result': False},
|
||||||
|
'console': {'result': False, 'reason': 'Not configured'},
|
||||||
|
'deploy': {'result': False, 'reason': 'No deploy for you'},
|
||||||
|
'inspect': {'result': None, 'reason': 'Not supported'},
|
||||||
|
'power': {'result': True}
|
||||||
|
}
|
||||||
|
result = self.node.validate(self.session, required=None)
|
||||||
|
self.assertTrue(result['power'].result)
|
||||||
|
self.assertFalse(result['power'].reason)
|
||||||
|
for iface in ('deploy', 'console', 'inspect'):
|
||||||
|
self.assertIsNot(True, result[iface].result)
|
||||||
|
self.assertTrue(result[iface].reason)
|
||||||
|
# Reason can be empty
|
||||||
|
self.assertFalse(result['boot'].result)
|
||||||
|
self.assertIsNone(result['boot'].reason)
|
||||||
|
@ -22,6 +22,7 @@ import uuid
|
|||||||
from testscenarios import load_tests_apply_scenarios as load_tests # noqa
|
from testscenarios import load_tests_apply_scenarios as load_tests # noqa
|
||||||
|
|
||||||
from openstack.cloud import exc
|
from openstack.cloud import exc
|
||||||
|
from openstack import exceptions
|
||||||
from openstack.tests import fakes
|
from openstack.tests import fakes
|
||||||
from openstack.tests.unit import base
|
from openstack.tests.unit import base
|
||||||
|
|
||||||
@ -119,36 +120,33 @@ class TestBaremetalNode(base.IronicTestCase):
|
|||||||
|
|
||||||
self.assert_calls()
|
self.assert_calls()
|
||||||
|
|
||||||
# FIXME(TheJulia): So, this doesn't presently fail, but should fail.
|
def test_validate_node_raises_exception(self):
|
||||||
# Placing the test here, so we can sort out the issue in the actual
|
validate_return = {
|
||||||
# method later.
|
'deploy': {
|
||||||
# def test_validate_node_raises_exception(self):
|
'result': False,
|
||||||
# validate_return = {
|
'reason': 'error!',
|
||||||
# 'deploy': {
|
},
|
||||||
# 'result': False,
|
'power': {
|
||||||
# 'reason': 'error!',
|
'result': False,
|
||||||
# },
|
'reason': 'meow!',
|
||||||
# 'power': {
|
},
|
||||||
# 'result': False,
|
'foo': {
|
||||||
# 'reason': 'meow!',
|
'result': True
|
||||||
# },
|
}}
|
||||||
# 'foo': {
|
self.register_uris([
|
||||||
# 'result': True
|
dict(method='GET',
|
||||||
# }}
|
uri=self.get_mock_url(
|
||||||
# self.register_uris([
|
resource='nodes',
|
||||||
# dict(method='GET',
|
append=[self.fake_baremetal_node['uuid'],
|
||||||
# uri=self.get_mock_url(
|
'validate']),
|
||||||
# resource='nodes',
|
json=validate_return),
|
||||||
# append=[self.fake_baremetal_node['uuid'],
|
])
|
||||||
# 'validate']),
|
self.assertRaises(
|
||||||
# json=validate_return),
|
exceptions.ValidationException,
|
||||||
# ])
|
self.cloud.validate_node,
|
||||||
# self.assertRaises(
|
self.fake_baremetal_node['uuid'])
|
||||||
# Exception,
|
|
||||||
# self.cloud.validate_node,
|
self.assert_calls()
|
||||||
# self.fake_baremetal_node['uuid'])
|
|
||||||
#
|
|
||||||
# self.assert_calls()
|
|
||||||
|
|
||||||
def test_patch_machine(self):
|
def test_patch_machine(self):
|
||||||
test_patch = [{
|
test_patch = [{
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds support for bare metal node validation to the bare metal proxy.
|
Loading…
x
Reference in New Issue
Block a user