diff options
author | Yuriy Zveryanskyy <yzveryanskyy@mirantis.com> | 2013-12-17 16:36:55 +0200 |
---|---|---|
committer | Roman Prykhodchenko <me@romcheg.me> | 2014-01-15 00:26:25 +0200 |
commit | 9bc5f92fb88169acdac48b367e9ea71930cfaf38 (patch) | |
tree | 5191e00d89912debf6f43cc801500e1b4917a685 | |
parent | 0fc3ad85e90a05322e20f4c2c0fce299d1c352f1 (diff) | |
download | ironic-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.py | 8 | ||||
-rw-r--r-- | ironic/common/exception.py | 5 | ||||
-rw-r--r-- | ironic/conductor/manager.py | 26 | ||||
-rw-r--r-- | ironic/conductor/rpcapi.py | 16 | ||||
-rw-r--r-- | ironic/db/sqlalchemy/migrate_repo/versions/015_nodes_add_maintenance.py | 28 | ||||
-rw-r--r-- | ironic/db/sqlalchemy/models.py | 3 | ||||
-rw-r--r-- | ironic/objects/node.py | 2 | ||||
-rw-r--r-- | ironic/tests/api/test_nodes.py | 9 | ||||
-rw-r--r-- | ironic/tests/conductor/test_manager.py | 36 | ||||
-rw-r--r-- | ironic/tests/conductor/test_rpcapi.py | 6 | ||||
-rw-r--r-- | ironic/tests/db/sqlalchemy/test_migrations.py | 11 | ||||
-rw-r--r-- | ironic/tests/db/utils.py | 1 |
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'), |