diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index e43fd5baf4..082f13fff8 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -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//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', @@ -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.""" @@ -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 diff --git a/ironic/tests/api/v1/test_nodes.py b/ironic/tests/api/v1/test_nodes.py index b45934ab33..f787f6be34 100644 --- a/ironic/tests/api/v1/test_nodes.py +++ b/ironic/tests/api/v1/test_nodes.py @@ -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): @@ -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)