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:
parent
0fc3ad85e9
commit
9bc5f92fb8
|
@ -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."
|
||||
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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.')
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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'),
|
||||
|
|
Loading…
Reference in New Issue