Skip to content

Commit

Permalink
Add API endpoint to set/unset the node maintenance mode
Browse files Browse the repository at this point in the history
This patch is adding an API endpoint to user set/unset the maintenance
mode of a node, since now we have an extra db field where operators can
specify the reason why the node was put in maintenance mode we should
make sure we clean this field as well once the node is removed from
maintenance mode, operations that requires extra actions as a consequence
of updating a attribute from a resource should have a separated endpoint
for it. Right now the endpoint is only updating the DB, but in the future
setting a node to maintenance mode should do more things like signalizing
Nova that the node is now in maintenance mode so that instances do not
gets scheduled onto it.

Implements: blueprint maintenance-reason
Change-Id: I1d4f609d248535064b9e3daeb67481d5b921aa7c
  • Loading branch information
umago committed Oct 21, 2014
1 parent 827db7f commit 68eed82
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 0 deletions.
40 changes: 40 additions & 0 deletions ironic/api/controllers/v1/node.py
Expand Up @@ -46,6 +46,8 @@ class NodePatchType(types.JsonPatchType):
@staticmethod
def internal_attrs():
defaults = types.JsonPatchType.internal_attrs()
# TODO(lucasagomes): Include maintenance once the endpoint
# v1/nodes/<uuid>/maintenance do more things than updating the DB.
return defaults + ['/console_enabled', '/last_error',
'/power_state', '/provision_state', '/reservation',
'/target_power_state', '/target_provision_state',
Expand Down Expand Up @@ -566,6 +568,41 @@ def post(self, node_uuid, method, data):
pecan.request.context, node_uuid, method, data, topic)


class NodeMaintenanceController(rest.RestController):

def _set_maintenance(self, node_uuid, maintenance_mode, reason=None):
rpc_node = objects.Node.get_by_uuid(pecan.request.context, node_uuid)
rpc_node.maintenance = maintenance_mode
rpc_node.maintenance_reason = reason

try:
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
except exception.NoValidHost as e:
e.code = 400
raise e
pecan.request.rpcapi.update_node(pecan.request.context,
rpc_node, topic=topic)

@wsme_pecan.wsexpose(None, types.uuid, wtypes.text, status_code=202)
def put(self, node_uuid, reason=None):
"""Put the node in maintenance mode.
:param node_uuid: UUID of a node.
:param reason: Optional, the reason why it's in maintenance.
"""
self._set_maintenance(node_uuid, True, reason=reason)

@wsme_pecan.wsexpose(None, types.uuid, status_code=202)
def delete(self, node_uuid):
"""Remove the node from maintenance mode.
:param node_uuid: UUID of a node.
"""
self._set_maintenance(node_uuid, False)


class NodesController(rest.RestController):
"""REST controller for Nodes."""

Expand All @@ -581,6 +618,9 @@ class NodesController(rest.RestController):
management = NodeManagementController()
"Expose management as a sub-element of nodes"

maintenance = NodeMaintenanceController()
"Expose maintenance as a sub-element of nodes"

# Set the flag to indicate that the requests to this resource are
# coming from a top-level resource
ports.from_nodes = True
Expand Down
43 changes: 43 additions & 0 deletions ironic/tests/api/v1/test_nodes.py
Expand Up @@ -962,6 +962,21 @@ def test_delete_associated(self, mock_dn):
self.assertEqual(409, response.status_int)
mock_dn.assert_called_once_with(mock.ANY, node.uuid, 'test-topic')

@mock.patch.object(objects.Node, 'get_by_uuid')
@mock.patch.object(rpcapi.ConductorAPI, 'update_node')
def test_delete_node_maintenance_mode(self, mock_update, mock_get):
node = obj_utils.create_test_node(self.context, maintenance=True,
maintenance_reason='blah')
mock_get.return_value = node
response = self.delete('/nodes/%s/maintenance' % node.uuid)
self.assertEqual(202, response.status_int)
self.assertEqual('', response.body)
self.assertEqual(False, node.maintenance)
self.assertEqual(None, node.maintenance_reason)
mock_get.assert_called_once_with(mock.ANY, node.uuid)
mock_update.assert_called_once_with(mock.ANY, mock.ANY,
topic='test-topic')


class TestPut(base.FunctionalTest):

Expand Down Expand Up @@ -1188,3 +1203,31 @@ def test_set_boot_device_persistent_invalid_value(self, mock_sbd):
expect_errors=True)
self.assertEqual('application/json', ret.content_type)
self.assertEqual(400, ret.status_code)

def _test_set_node_maintenance_mode(self, mock_update, mock_get, reason):
request_body = {}
if reason:
request_body['reason'] = reason

self.node.maintenance = False
mock_get.return_value = self.node
ret = self.put_json('/nodes/%s/maintenance' % self.node.uuid,
request_body)
self.assertEqual(202, ret.status_code)
self.assertEqual('', ret.body)
self.assertEqual(True, self.node.maintenance)
self.assertEqual(reason, self.node.maintenance_reason)
mock_get.assert_called_once_with(mock.ANY, self.node.uuid)
mock_update.assert_called_once_with(mock.ANY, mock.ANY,
topic='test-topic')

@mock.patch.object(objects.Node, 'get_by_uuid')
@mock.patch.object(rpcapi.ConductorAPI, 'update_node')
def test_set_node_maintenance_mode(self, mock_update, mock_get):
self._test_set_node_maintenance_mode(mock_update, mock_get,
'fake_reason')

@mock.patch.object(objects.Node, 'get_by_uuid')
@mock.patch.object(rpcapi.ConductorAPI, 'update_node')
def test_set_node_maintenance_mode_no_reason(self, mock_update, mock_get):
self._test_set_node_maintenance_mode(mock_update, mock_get, None)

0 comments on commit 68eed82

Please sign in to comment.