summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuriy Zveryanskyy <yzveryanskyy@mirantis.com>2013-12-17 16:36:55 +0200
committerRoman Prykhodchenko <me@romcheg.me>2014-01-15 00:26:25 +0200
commit9bc5f92fb88169acdac48b367e9ea71930cfaf38 (patch)
tree5191e00d89912debf6f43cc801500e1b4917a685
parent0fc3ad85e90a05322e20f4c2c0fce299d1c352f1 (diff)
downloadironic-9bc5f92fb88169acdac48b367e9ea71930cfaf38.tar.gz
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
-rw-r--r--ironic/api/controllers/v1/node.py8
-rw-r--r--ironic/common/exception.py5
-rw-r--r--ironic/conductor/manager.py26
-rw-r--r--ironic/conductor/rpcapi.py16
-rw-r--r--ironic/db/sqlalchemy/migrate_repo/versions/015_nodes_add_maintenance.py28
-rw-r--r--ironic/db/sqlalchemy/models.py3
-rw-r--r--ironic/objects/node.py2
-rw-r--r--ironic/tests/api/test_nodes.py9
-rw-r--r--ironic/tests/conductor/test_manager.py36
-rw-r--r--ironic/tests/conductor/test_rpcapi.py6
-rw-r--r--ironic/tests/db/sqlalchemy/test_migrations.py11
-rw-r--r--ironic/tests/db/utils.py1
12 files changed, 148 insertions, 3 deletions
diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
index 020615960..f38853cb4 100644
--- a/ironic/api/controllers/v1/node.py
+++ b/ironic/api/controllers/v1/node.py
@@ -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."
diff --git a/ironic/common/exception.py b/ironic/common/exception.py
index 854434c99..e8c5bae37 100644
--- a/ironic/common/exception.py
+++ b/ironic/common/exception.py
@@ -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.")
diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py
index 30bb73546..c7145405a 100644
--- a/ironic/conductor/manager.py
+++ b/ironic/conductor/manager.py
@@ -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
diff --git a/ironic/conductor/rpcapi.py b/ironic/conductor/rpcapi.py
index efb6a3122..711ae7f4d 100644
--- a/ironic/conductor/rpcapi.py
+++ b/ironic/conductor/rpcapi.py
@@ -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))
diff --git a/ironic/db/sqlalchemy/migrate_repo/versions/015_nodes_add_maintenance.py b/ironic/db/sqlalchemy/migrate_repo/versions/015_nodes_add_maintenance.py
new file mode 100644
index 000000000..8266939d9
--- /dev/null
+++ b/ironic/db/sqlalchemy/migrate_repo/versions/015_nodes_add_maintenance.py
@@ -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.')
diff --git a/ironic/db/sqlalchemy/models.py b/ironic/db/sqlalchemy/models.py
index b102c125c..6ed5bd94b 100644
--- a/ironic/db/sqlalchemy/models.py
+++ b/ironic/db/sqlalchemy/models.py
@@ -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)
diff --git a/ironic/objects/node.py b/ironic/objects/node.py
index 6720b4872..21698c805 100644
--- a/ironic/objects/node.py
+++ b/ironic/objects/node.py
@@ -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,
diff --git a/ironic/tests/api/test_nodes.py b/ironic/tests/api/test_nodes.py
index 473af5e58..4c01e2c68 100644
--- a/ironic/tests/api/test_nodes.py
+++ b/ironic/tests/api/test_nodes.py
@@ -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):
diff --git a/ironic/tests/conductor/test_manager.py b/ironic/tests/conductor/test_manager.py
index 9a72ef2c0..e346b25c8 100644
--- a/ironic/tests/conductor/test_manager.py
+++ b/ironic/tests/conductor/test_manager.py
@@ -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)
diff --git a/ironic/tests/conductor/test_rpcapi.py b/ironic/tests/conductor/test_rpcapi.py
index b38a0621e..86e0c366d 100644
--- a/ironic/tests/conductor/test_rpcapi.py
+++ b/ironic/tests/conductor/test_rpcapi.py
@@ -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)
diff --git a/ironic/tests/db/sqlalchemy/test_migrations.py b/ironic/tests/db/sqlalchemy/test_migrations.py
index da7873efc..eb50c11ce 100644
--- a/ironic/tests/db/sqlalchemy/test_migrations.py
+++ b/ironic/tests/db/sqlalchemy/test_migrations.py
@@ -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))
diff --git a/ironic/tests/db/utils.py b/ironic/tests/db/utils.py
index 6a6de1556..95a3b0b38 100644
--- a/ironic/tests/db/utils.py
+++ b/ironic/tests/db/utils.py
@@ -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'),