Add RPC method for node maintenance mode

Method 'change_node_maintenance_mode' added to manager and rpcapi.
This method triggered maintenance mode for a node.
New column 'maintenance' added to nodes table.

Partial-Bug: #1260099
Change-Id: I945a1ce72c04e5ee2a9427a58dae72b0719c160f
This commit is contained in:
Yuriy Zveryanskyy 2013-12-17 16:36:55 +02:00 committed by Roman Prykhodchenko
parent 0fc3ad85e9
commit 9bc5f92fb8
12 changed files with 148 additions and 3 deletions

View File

@ -48,8 +48,9 @@ class NodePatchType(types.JsonPatchType):
@staticmethod
def internal_attrs():
defaults = types.JsonPatchType.internal_attrs()
return defaults + ['/last_error', '/power_state', '/provision_state',
'/target_power_state', '/target_provision_state']
return defaults + ['/last_error', '/maintenance', '/power_state',
'/provision_state', '/target_power_state',
'/target_provision_state']
@staticmethod
def mandatory_attrs():
@ -240,6 +241,9 @@ class Node(base.APIBase):
provision_state = wtypes.text
"Represent the current (not transition) provision state of the node"
maintenance = wsme.wsattr(bool, default=False)
"Indicates whether the node is in maintenance mode."
target_provision_state = wtypes.text
"The user modified desired provision state of the node."

View File

@ -251,6 +251,11 @@ class ExclusiveLockRequired(NotAuthorized):
"but the current context has a shared lock.")
class NodeMaintenanceFailure(Invalid):
message = _("Failed to toggle maintenance-mode flag "
"for node %(node)s: %(reason)s")
class NodeInUse(InvalidState):
message = _("Unable to complete the requested action because node "
"%(node)s is currently in use by another process.")

View File

@ -439,3 +439,29 @@ class ConductorManager(service.PeriodicService):
if reason is not None:
ret_dict[iface_name]['reason'] = reason
return ret_dict
def change_node_maintenance_mode(self, context, node_id, mode):
"""Set node maintenance mode on or off.
:param context: request context.
:param node_id: node id or uuid.
:param mode: True or False.
:raises: NodeMaintenanceFailure
"""
LOG.debug(_("RPC change_node_maintenance_mode called for node %(node)s"
" with maintanence mode: %(mode)s") % {'node': node_id,
'mode': mode})
with task_manager.acquire(context, node_id, shared=True) as task:
node = task.node
if mode is not node.maintenance:
node.maintenance = mode
node.save(context)
else:
msg = _("The node is already in maintenance mode") if mode \
else _("The node is not in maintenance mode")
raise exception.NodeMaintenanceFailure(node=node_id,
reason=msg)
return node

View File

@ -54,6 +54,7 @@ class ConductorAPI(ironic.openstack.common.rpc.proxy.RpcProxy):
1.6 - change_node_power_state, do_node_deploy and do_node_tear_down
accept node id instead of node object.
1.7 - Added topic parameter to RPC methods.
1.8 - Added change_node_maintenance_mode.
"""
@ -220,3 +221,18 @@ class ConductorAPI(ironic.openstack.common.rpc.proxy.RpcProxy):
self.make_msg('validate_driver_interfaces',
node_id=node_id),
topic=topic or self.topic)
def change_node_maintenance_mode(self, context, node_id, mode):
"""Set node maintenance mode on or off.
:param context: request context.
:param node_id: node id or uuid.
:param mode: True or False.
:returns: a node object.
:raises: NodeMaintenanceFailure.
"""
return self.call(context,
self.make_msg('change_node_maintenance_mode',
node_id=node_id,
mode=mode))

View File

@ -0,0 +1,28 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# -*- encoding: utf-8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from sqlalchemy import Table, Column, MetaData, Boolean
def upgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
nodes = Table('nodes', meta, autoload=True)
nodes.create_column(Column('maintenance', Boolean, default=False))
def downgrade(migrate_engine):
raise NotImplementedError('Downgrade from version 015 is unsupported.')

View File

@ -24,7 +24,7 @@ import urlparse
from oslo.config import cfg
from sqlalchemy import Column, ForeignKey
from sqlalchemy import Boolean, Column, ForeignKey
from sqlalchemy import Integer, Index
from sqlalchemy import schema, String, Text
from sqlalchemy.ext.declarative import declarative_base
@ -125,6 +125,7 @@ class Node(Base):
driver = Column(String(15))
driver_info = Column(JSONEncodedDict)
reservation = Column(String(255), nullable=True)
maintenance = Column(Boolean, default=False)
extra = Column(JSONEncodedDict)

View File

@ -47,6 +47,8 @@ class Node(base.IronicObject):
'provision_state': utils.str_or_none,
'target_provision_state': utils.str_or_none,
'maintenance': bool,
# Any error from the most recent (last) asynchronous transaction
# that started but failed to finish.
'last_error': utils.str_or_none,

View File

@ -491,6 +491,15 @@ class TestPatch(base.FunctionalTest):
self.assertEqual(response.status_code, 400)
self.assertTrue(response.json['error_message'])
def test_replace_maintenance(self):
response = self.patch_json('/nodes/%s' % self.node['uuid'],
[{'path': '/maintenance', 'op': 'replace',
'value': 'fake'}],
expect_errors=True)
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.status_code, 400)
self.assertTrue(response.json['error_message'])
class TestPost(base.FunctionalTest):

View File

@ -438,3 +438,39 @@ class ManagerTestCase(base.DbTestCase):
node['uuid'])
self.assertFalse(ret['deploy']['result'])
self.assertEqual(reason, ret['deploy']['reason'])
def test_maintenance_mode_on(self):
ndict = utils.get_test_node(driver='fake')
node = self.dbapi.create_node(ndict)
self.service.change_node_maintenance_mode(self.context, node.uuid,
True)
node.refresh(self.context)
self.assertTrue(node.maintenance)
def test_maintenance_mode_off(self):
ndict = utils.get_test_node(driver='fake',
maintenance=True)
node = self.dbapi.create_node(ndict)
self.service.change_node_maintenance_mode(self.context, node.uuid,
False)
node.refresh(self.context)
self.assertFalse(node.maintenance)
def test_maintenance_mode_on_failed(self):
ndict = utils.get_test_node(driver='fake',
maintenance=True)
node = self.dbapi.create_node(ndict)
self.assertRaises(exception.NodeMaintenanceFailure,
self.service.change_node_maintenance_mode,
self.context, node.uuid, True)
node.refresh(self.context)
self.assertTrue(node.maintenance)
def test_maintenance_mode_off_failed(self):
ndict = utils.get_test_node(driver='fake')
node = self.dbapi.create_node(ndict)
self.assertRaises(exception.NodeMaintenanceFailure,
self.service.change_node_maintenance_mode,
self.context, node.uuid, False)
node.refresh(self.context)
self.assertFalse(node.maintenance)

View File

@ -149,3 +149,9 @@ class RPCAPITestCase(base.DbTestCase):
self._test_rpcapi('validate_driver_interfaces',
'call',
node_id=self.fake_node['uuid'])
def test_change_node_maintenance_mode(self):
self._test_rpcapi('change_node_maintenance_mode',
'call',
node_id=self.fake_node['uuid'],
mode=True)

View File

@ -757,3 +757,14 @@ class TestMigrations(BaseMigrationTestCase, WalkVersionsMixin):
{'address': 'CC:BB:AA:AA:AA:CC',
'uuid': '1be26c0b-03f2-4d2e-ae87-c02d7f33c781',
'extra': 'extra3'})
def _check_015(self, engine, data):
nodes = db_utils.get_table(engine, 'nodes')
col_names = [column.name for column in nodes.c]
self.assertIn('maintenance', col_names)
# in some backends bool type is integer
self.assertTrue(isinstance(nodes.c.maintenance.type,
sqlalchemy.types.Boolean) or
isinstance(nodes.c.maintenance.type,
sqlalchemy.types.Integer))

View File

@ -78,6 +78,7 @@ def get_test_node(**kw):
'driver_info': kw.get('driver_info', fake_info),
'properties': kw.get('properties', properties),
'reservation': kw.get('reservation', None),
'maintenance': kw.get('maintenance', False),
'extra': kw.get('extra', {}),
'updated_at': kw.get('created_at'),
'created_at': kw.get('updated_at'),