Merge "Changes power_state and adds last_error field"
This commit is contained in:
commit
df9bd77624
|
@ -88,12 +88,12 @@ class NodePowerStateController(rest.RestController):
|
||||||
#TODO(lucasagomes): Test if target is a valid state and if it's able
|
#TODO(lucasagomes): Test if target is a valid state and if it's able
|
||||||
# to transition to the target state from the current one
|
# to transition to the target state from the current one
|
||||||
|
|
||||||
node['target_power_state'] = target
|
# Note that there is a race condition. The node state(s) could change
|
||||||
updated_node = pecan.request.rpcapi.update_node(pecan.request.context,
|
# by the time the RPC call is made and the TaskManager manager gets a
|
||||||
node)
|
# lock.
|
||||||
pecan.request.rpcapi.change_node_power_state(pecan.request.context,
|
pecan.request.rpcapi.change_node_power_state(pecan.request.context,
|
||||||
updated_node, target)
|
node, target)
|
||||||
return NodePowerState.convert_with_links(updated_node, expand=False)
|
return NodePowerState.convert_with_links(node, expand=False)
|
||||||
|
|
||||||
|
|
||||||
class NodeProvisionState(state.State):
|
class NodeProvisionState(state.State):
|
||||||
|
@ -190,6 +190,10 @@ class Node(base.APIBase):
|
||||||
target_power_state = wtypes.text
|
target_power_state = wtypes.text
|
||||||
"The user modified desired power state of the node."
|
"The user modified desired power state of the node."
|
||||||
|
|
||||||
|
last_error = wtypes.text
|
||||||
|
"Any error from the most recent (last) asynchronous transaction that"
|
||||||
|
"started but failed to finish."
|
||||||
|
|
||||||
provision_state = wtypes.text
|
provision_state = wtypes.text
|
||||||
"Represent the current (not transition) provision state of the node"
|
"Represent the current (not transition) provision state of the node"
|
||||||
|
|
||||||
|
@ -230,6 +234,7 @@ class Node(base.APIBase):
|
||||||
def convert_with_links(cls, rpc_node, expand=True):
|
def convert_with_links(cls, rpc_node, expand=True):
|
||||||
minimum_fields = ['uuid', 'power_state', 'target_power_state',
|
minimum_fields = ['uuid', 'power_state', 'target_power_state',
|
||||||
'provision_state', 'target_provision_state',
|
'provision_state', 'target_provision_state',
|
||||||
|
'last_error',
|
||||||
'instance_uuid']
|
'instance_uuid']
|
||||||
fields = minimum_fields if not expand else None
|
fields = minimum_fields if not expand else None
|
||||||
node = Node.from_rpc_object(rpc_node, fields)
|
node = Node.from_rpc_object(rpc_node, fields)
|
||||||
|
|
|
@ -28,7 +28,7 @@ validated by the driver. Any node with non-empty `properties` is said to be
|
||||||
|
|
||||||
When the driver has received both `properties` and `driver_info`, it will check
|
When the driver has received both `properties` and `driver_info`, it will check
|
||||||
the power status of the node and update the `power_state` accordingly. If the
|
the power status of the node and update the `power_state` accordingly. If the
|
||||||
driver fails to read the the power state from the node, it will reject the
|
driver fails to read the power state from the node, it will reject the
|
||||||
`driver_info` change, and the state will remain as INIT. If the power status
|
`driver_info` change, and the state will remain as INIT. If the power status
|
||||||
check succeeds, `power_state` will change to one of POWER_ON or POWER_OFF,
|
check succeeds, `power_state` will change to one of POWER_ON or POWER_OFF,
|
||||||
accordingly.
|
accordingly.
|
||||||
|
@ -36,7 +36,9 @@ accordingly.
|
||||||
At this point, the power state may be changed via the API, a console
|
At this point, the power state may be changed via the API, a console
|
||||||
may be started, and a tenant may be associated.
|
may be started, and a tenant may be associated.
|
||||||
|
|
||||||
The `power_state` for a node which fails to transition will be set to ERROR.
|
The `power_state` for a node always represents the current power state. Any
|
||||||
|
power operation sets this to the actual state when done (whether successful or
|
||||||
|
not). It is set to ERROR only when unable to get the power state from a node.
|
||||||
|
|
||||||
When `instance_uuid` is set to a non-empty / non-None value, the node is said
|
When `instance_uuid` is set to a non-empty / non-None value, the node is said
|
||||||
to be "associated" with a tenant.
|
to be "associated" with a tenant.
|
||||||
|
|
|
@ -154,7 +154,7 @@ class ConductorManager(service.PeriodicService):
|
||||||
"""RPC method to encapsulate changes to a node's state.
|
"""RPC method to encapsulate changes to a node's state.
|
||||||
|
|
||||||
Perform actions such as power on, power off. It waits for the power
|
Perform actions such as power on, power off. It waits for the power
|
||||||
action to finish, then if succesful, it updates the power_state for
|
action to finish, then if successful, it updates the power_state for
|
||||||
the node with the new power state.
|
the node with the new power state.
|
||||||
|
|
||||||
:param context: an admin context.
|
:param context: an admin context.
|
||||||
|
@ -162,10 +162,8 @@ class ConductorManager(service.PeriodicService):
|
||||||
:param new_state: the desired power state of the node.
|
:param new_state: the desired power state of the node.
|
||||||
:raises: InvalidParameterValue when the wrong state is specified
|
:raises: InvalidParameterValue when the wrong state is specified
|
||||||
or the wrong driver info is specified.
|
or the wrong driver info is specified.
|
||||||
:raises: NodeInWrongPowerState when the node is in the state.
|
:raises: other exceptions by the node's power driver if something
|
||||||
that cannot perform and requested power action.
|
wrong occurred during the power action.
|
||||||
:raises: other exceptins by the node's power driver if something
|
|
||||||
wrong during the power action.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
node_id = node_obj.get('uuid')
|
node_id = node_obj.get('uuid')
|
||||||
|
@ -174,31 +172,57 @@ class ConductorManager(service.PeriodicService):
|
||||||
% {'node': node_id, 'state': new_state})
|
% {'node': node_id, 'state': new_state})
|
||||||
|
|
||||||
with task_manager.acquire(context, node_id, shared=False) as task:
|
with task_manager.acquire(context, node_id, shared=False) as task:
|
||||||
# an exception will be raised if validate fails.
|
node = task.node
|
||||||
task.driver.power.validate(node_obj)
|
|
||||||
curr_state = task.driver.power.get_power_state(task, node_obj)
|
|
||||||
if curr_state == new_state:
|
|
||||||
raise exception.NodeInWrongPowerState(node=node_id,
|
|
||||||
pstate=curr_state)
|
|
||||||
|
|
||||||
# set the target_power_state.
|
|
||||||
# This will expose to other processes and clients that the work
|
|
||||||
# is in progress
|
|
||||||
node_obj['target_power_state'] = new_state
|
|
||||||
node_obj.save(context)
|
|
||||||
|
|
||||||
#take power action, set the power_state to error if fails
|
|
||||||
try:
|
try:
|
||||||
task.driver.power.set_power_state(task, node_obj, new_state)
|
task.driver.power.validate(node)
|
||||||
except exception.IronicException:
|
curr_state = task.driver.power.get_power_state(task, node)
|
||||||
node_obj['power_state'] = states.ERROR
|
except Exception as e:
|
||||||
node_obj.save(context)
|
with excutils.save_and_reraise_exception():
|
||||||
raise
|
node['last_error'] = \
|
||||||
|
_("Failed to change power state to '%(target)s'. "
|
||||||
|
"Error: %(error)s") % {
|
||||||
|
'target': new_state, 'error': e}
|
||||||
|
node.save(context)
|
||||||
|
|
||||||
# update the node power states
|
if curr_state == new_state:
|
||||||
node_obj['power_state'] = new_state
|
# Neither the ironic service nor the hardware has erred. The
|
||||||
node_obj['target_power_state'] = states.NOSTATE
|
# node is, for some reason, already in the requested state,
|
||||||
node_obj.save(context)
|
# though we don't know why. eg, perhaps the user previously
|
||||||
|
# requested the node POWER_ON, the network delayed those IPMI
|
||||||
|
# packets, and they are trying again -- but the node finally
|
||||||
|
# responds to the first request, and so the second request
|
||||||
|
# gets to this check and stops.
|
||||||
|
# This isn't an error, so we'll clear last_error field
|
||||||
|
# (from previous operation), log a warning, and return.
|
||||||
|
node['last_error'] = None
|
||||||
|
node.save(context)
|
||||||
|
LOG.warn(_("Not going to change_node_power_state because "
|
||||||
|
"current state = requested state = '%(state)s'.")
|
||||||
|
% {'state': curr_state})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set the target_power_state and clear any last_error, since we're
|
||||||
|
# starting a new operation. This will expose to other processes
|
||||||
|
# and clients that work is in progress.
|
||||||
|
node['target_power_state'] = new_state
|
||||||
|
node['last_error'] = None
|
||||||
|
node.save(context)
|
||||||
|
|
||||||
|
# take power action
|
||||||
|
try:
|
||||||
|
task.driver.power.set_power_state(task, node, new_state)
|
||||||
|
except Exception as e:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
node['last_error'] = \
|
||||||
|
_("Failed to change power state to '%(target)s'. "
|
||||||
|
"Error: %(error)s") % {
|
||||||
|
'target': new_state, 'error': e}
|
||||||
|
else:
|
||||||
|
# success!
|
||||||
|
node['power_state'] = new_state
|
||||||
|
finally:
|
||||||
|
node['target_power_state'] = states.NOSTATE
|
||||||
|
node.save(context)
|
||||||
|
|
||||||
# NOTE(deva): There is a race condition in the RPC API for vendor_passthru.
|
# NOTE(deva): There is a race condition in the RPC API for vendor_passthru.
|
||||||
# Between the validate_vendor_action and do_vendor_action calls, it's
|
# Between the validate_vendor_action and do_vendor_action calls, it's
|
||||||
|
@ -256,7 +280,7 @@ class ConductorManager(service.PeriodicService):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_state = task.driver.deploy.deploy(task, node_obj)
|
new_state = task.driver.deploy.deploy(task, node_obj)
|
||||||
except exception.IronicException:
|
except Exception:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
node_obj['provision_state'] = states.ERROR
|
node_obj['provision_state'] = states.ERROR
|
||||||
node_obj.save(context)
|
node_obj.save(context)
|
||||||
|
@ -299,7 +323,7 @@ class ConductorManager(service.PeriodicService):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_state = task.driver.deploy.tear_down(task, node_obj)
|
new_state = task.driver.deploy.tear_down(task, node_obj)
|
||||||
except exception.IronicException:
|
except Exception:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
node_obj['provision_state'] = states.ERROR
|
node_obj['provision_state'] = states.ERROR
|
||||||
node_obj.save(context)
|
node_obj.save(context)
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
# 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, Text
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(migrate_engine):
|
||||||
|
meta = MetaData()
|
||||||
|
meta.bind = migrate_engine
|
||||||
|
|
||||||
|
nodes = Table('nodes', meta, autoload=True)
|
||||||
|
|
||||||
|
# Create new last_error column
|
||||||
|
nodes.create_column(Column('last_error', Text, nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(migrate_engine):
|
||||||
|
raise NotImplementedError('Downgrade from version 013 is unsupported.')
|
|
@ -26,7 +26,7 @@ from oslo.config import cfg
|
||||||
|
|
||||||
from sqlalchemy import Column, ForeignKey
|
from sqlalchemy import Column, ForeignKey
|
||||||
from sqlalchemy import Integer, Index
|
from sqlalchemy import Integer, Index
|
||||||
from sqlalchemy import schema, String
|
from sqlalchemy import schema, String, Text
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.types import TypeDecorator, VARCHAR
|
from sqlalchemy.types import TypeDecorator, VARCHAR
|
||||||
|
|
||||||
|
@ -120,6 +120,7 @@ class Node(Base):
|
||||||
target_power_state = Column(String(15), nullable=True)
|
target_power_state = Column(String(15), nullable=True)
|
||||||
provision_state = Column(String(15), nullable=True)
|
provision_state = Column(String(15), nullable=True)
|
||||||
target_provision_state = Column(String(15), nullable=True)
|
target_provision_state = Column(String(15), nullable=True)
|
||||||
|
last_error = Column(Text, nullable=True)
|
||||||
properties = Column(JSONEncodedDict)
|
properties = Column(JSONEncodedDict)
|
||||||
driver = Column(String(15))
|
driver = Column(String(15))
|
||||||
driver_info = Column(JSONEncodedDict)
|
driver_info = Column(JSONEncodedDict)
|
||||||
|
|
|
@ -33,7 +33,9 @@ class FakePower(base.PowerInterface):
|
||||||
return states.NOSTATE
|
return states.NOSTATE
|
||||||
|
|
||||||
def set_power_state(self, task, node, power_state):
|
def set_power_state(self, task, node, power_state):
|
||||||
pass
|
if power_state not in [states.POWER_ON, states.POWER_OFF]:
|
||||||
|
raise exception.InvalidParameterValue(_("set_power_state called "
|
||||||
|
"with an invalid power state: %s.") % power_state)
|
||||||
|
|
||||||
def reboot(self, task, node):
|
def reboot(self, task, node):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -35,10 +35,22 @@ class Node(base.IronicObject):
|
||||||
|
|
||||||
'properties': utils.dict_or_none,
|
'properties': utils.dict_or_none,
|
||||||
'reservation': utils.str_or_none,
|
'reservation': utils.str_or_none,
|
||||||
|
|
||||||
|
# One of states.POWER_ON|POWER_OFF|NOSTATE|ERROR
|
||||||
'power_state': utils.str_or_none,
|
'power_state': utils.str_or_none,
|
||||||
|
|
||||||
|
# Set to one of states.POWER_ON|POWER_OFF when a power operation
|
||||||
|
# starts, and set to NOSTATE when the operation finishes
|
||||||
|
# (successfully or unsuccessfully).
|
||||||
'target_power_state': utils.str_or_none,
|
'target_power_state': utils.str_or_none,
|
||||||
|
|
||||||
'provision_state': utils.str_or_none,
|
'provision_state': utils.str_or_none,
|
||||||
'target_provision_state': utils.str_or_none,
|
'target_provision_state': utils.str_or_none,
|
||||||
|
|
||||||
|
# Any error from the most recent (last) asynchronous transaction
|
||||||
|
# that started but failed to finish.
|
||||||
|
'last_error': utils.str_or_none,
|
||||||
|
|
||||||
'extra': utils.dict_or_none,
|
'extra': utils.dict_or_none,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -539,49 +539,37 @@ class TestPut(base.FunctionalTest):
|
||||||
self.chassis = self.dbapi.create_chassis(cdict)
|
self.chassis = self.dbapi.create_chassis(cdict)
|
||||||
ndict = dbutils.get_test_node()
|
ndict = dbutils.get_test_node()
|
||||||
self.node = self.dbapi.create_node(ndict)
|
self.node = self.dbapi.create_node(ndict)
|
||||||
p = mock.patch.object(rpcapi.ConductorAPI, 'update_node')
|
|
||||||
self.mock_update_node = p.start()
|
|
||||||
self.addCleanup(p.stop)
|
|
||||||
p = mock.patch.object(rpcapi.ConductorAPI, 'change_node_power_state')
|
p = mock.patch.object(rpcapi.ConductorAPI, 'change_node_power_state')
|
||||||
self.mock_cnps = p.start()
|
self.mock_cnps = p.start()
|
||||||
self.addCleanup(p.stop)
|
self.addCleanup(p.stop)
|
||||||
|
|
||||||
def test_power_state(self):
|
def test_power_state(self):
|
||||||
self.mock_update_node.return_value = self.node
|
|
||||||
|
|
||||||
response = self.put_json('/nodes/%s/state/power' % self.node['uuid'],
|
response = self.put_json('/nodes/%s/state/power' % self.node['uuid'],
|
||||||
{'target': states.POWER_ON})
|
{'target': states.POWER_ON})
|
||||||
self.assertEqual(response.content_type, 'application/json')
|
self.assertEqual(response.content_type, 'application/json')
|
||||||
self.assertEqual(response.status_code, 202)
|
self.assertEqual(response.status_code, 202)
|
||||||
|
|
||||||
self.mock_update_node.assert_called_once_with(mock.ANY, mock.ANY)
|
|
||||||
self.mock_cnps.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY)
|
self.mock_cnps.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY)
|
||||||
|
|
||||||
def test_power_state_in_progress(self):
|
def test_power_state_in_progress(self):
|
||||||
self.mock_update_node.return_value = self.node
|
|
||||||
manager = mock.MagicMock()
|
manager = mock.MagicMock()
|
||||||
with mock.patch.object(objects.Node, 'get_by_uuid') as mock_get_node:
|
with mock.patch.object(objects.Node, 'get_by_uuid') as mock_get_node:
|
||||||
mock_get_node.return_value = self.node
|
mock_get_node.return_value = self.node
|
||||||
manager.attach_mock(mock_get_node, 'get_by_uuid')
|
manager.attach_mock(mock_get_node, 'get_by_uuid')
|
||||||
manager.attach_mock(self.mock_update_node, 'update_node')
|
|
||||||
manager.attach_mock(self.mock_cnps, 'change_node_power_state')
|
manager.attach_mock(self.mock_cnps, 'change_node_power_state')
|
||||||
expected = [mock.call.get_by_uuid(mock.ANY, self.node['uuid']),
|
expected = [mock.call.get_by_uuid(mock.ANY, self.node['uuid']),
|
||||||
mock.call.update_node(mock.ANY, mock.ANY),
|
|
||||||
mock.call.change_node_power_state(mock.ANY, mock.ANY,
|
mock.call.change_node_power_state(mock.ANY, mock.ANY,
|
||||||
mock.ANY),
|
mock.ANY)]
|
||||||
mock.call.get_by_uuid(mock.ANY, self.node['uuid'])]
|
|
||||||
|
|
||||||
self.put_json('/nodes/%s/state/power' % self.node['uuid'],
|
self.put_json('/nodes/%s/state/power' % self.node['uuid'],
|
||||||
{'target': states.POWER_ON})
|
{'target': states.POWER_ON})
|
||||||
self.assertRaises(webtest.app.AppError, self.put_json,
|
|
||||||
'/nodes/%s/state/power' % self.node['uuid'],
|
|
||||||
{'target': states.POWER_ON})
|
|
||||||
|
|
||||||
self.assertEqual(manager.mock_calls, expected)
|
self.assertEqual(manager.mock_calls, expected)
|
||||||
|
|
||||||
# check status code
|
|
||||||
self.dbapi.update_node(self.node['uuid'],
|
self.dbapi.update_node(self.node['uuid'],
|
||||||
{'target_power_state': 'fake'})
|
{'target_power_state': 'fake'})
|
||||||
|
self.assertRaises(webtest.app.AppError, self.put_json,
|
||||||
|
'/nodes/%s/state/power' % self.node['uuid'],
|
||||||
|
{'target': states.POWER_ON})
|
||||||
response = self.put_json('/nodes/%s/state/power' % self.node['uuid'],
|
response = self.put_json('/nodes/%s/state/power' % self.node['uuid'],
|
||||||
{'target': states.POWER_ON},
|
{'target': states.POWER_ON},
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
|
|
|
@ -181,12 +181,13 @@ class ManagerTestCase(base.DbTestCase):
|
||||||
self.assertEqual(res['driver'], existing_driver)
|
self.assertEqual(res['driver'], existing_driver)
|
||||||
|
|
||||||
def test_change_node_power_state_power_on(self):
|
def test_change_node_power_state_power_on(self):
|
||||||
"""Test if start_power_state to turn node power on
|
"""Test if change_node_power_state to turn node power on
|
||||||
is successful or not.
|
is successful or not.
|
||||||
"""
|
"""
|
||||||
ndict = utils.get_test_node(driver='fake',
|
ndict = utils.get_test_node(driver='fake',
|
||||||
power_state=states.POWER_OFF)
|
power_state=states.POWER_OFF)
|
||||||
node = self.dbapi.create_node(ndict)
|
node = self.dbapi.create_node(ndict)
|
||||||
|
node = objects.Node.get_by_uuid(self.context, node['uuid'])
|
||||||
|
|
||||||
with mock.patch.object(self.driver.power, 'get_power_state') \
|
with mock.patch.object(self.driver.power, 'get_power_state') \
|
||||||
as get_power_mock:
|
as get_power_mock:
|
||||||
|
@ -194,18 +195,20 @@ class ManagerTestCase(base.DbTestCase):
|
||||||
|
|
||||||
self.service.change_node_power_state(self.context,
|
self.service.change_node_power_state(self.context,
|
||||||
node, states.POWER_ON)
|
node, states.POWER_ON)
|
||||||
|
node.refresh()
|
||||||
get_power_mock.assert_called_once_with(mock.ANY, node)
|
get_power_mock.assert_called_once_with(mock.ANY, mock.ANY)
|
||||||
self.assertEqual(node['power_state'], states.POWER_ON)
|
self.assertEqual(node['power_state'], states.POWER_ON)
|
||||||
self.assertEqual(node['target_power_state'], None)
|
self.assertEqual(node['target_power_state'], None)
|
||||||
|
self.assertEqual(node['last_error'], None)
|
||||||
|
|
||||||
def test_change_node_power_state_power_off(self):
|
def test_change_node_power_state_power_off(self):
|
||||||
"""Test if start_power_state to turn node power off
|
"""Test if change_node_power_state to turn node power off
|
||||||
is successful or not.
|
is successful or not.
|
||||||
"""
|
"""
|
||||||
ndict = utils.get_test_node(driver='fake',
|
ndict = utils.get_test_node(driver='fake',
|
||||||
power_state=states.POWER_ON)
|
power_state=states.POWER_ON)
|
||||||
node = self.dbapi.create_node(ndict)
|
node = self.dbapi.create_node(ndict)
|
||||||
|
node = objects.Node.get_by_uuid(self.context, node['uuid'])
|
||||||
|
|
||||||
with mock.patch.object(self.driver.power, 'get_power_state') \
|
with mock.patch.object(self.driver.power, 'get_power_state') \
|
||||||
as get_power_mock:
|
as get_power_mock:
|
||||||
|
@ -213,10 +216,43 @@ class ManagerTestCase(base.DbTestCase):
|
||||||
|
|
||||||
self.service.change_node_power_state(self.context, node,
|
self.service.change_node_power_state(self.context, node,
|
||||||
states.POWER_OFF)
|
states.POWER_OFF)
|
||||||
|
node.refresh()
|
||||||
get_power_mock.assert_called_once_with(mock.ANY, node)
|
get_power_mock.assert_called_once_with(mock.ANY, mock.ANY)
|
||||||
self.assertEqual(node['power_state'], states.POWER_OFF)
|
self.assertEqual(node['power_state'], states.POWER_OFF)
|
||||||
self.assertEqual(node['target_power_state'], None)
|
self.assertEqual(node['target_power_state'], None)
|
||||||
|
self.assertEqual(node['last_error'], None)
|
||||||
|
|
||||||
|
def test_change_node_power_state_to_invalid_state(self):
|
||||||
|
"""Test if an exception is thrown when changing to an invalid
|
||||||
|
power state.
|
||||||
|
"""
|
||||||
|
ndict = utils.get_test_node(driver='fake',
|
||||||
|
power_state=states.POWER_ON)
|
||||||
|
node = self.dbapi.create_node(ndict)
|
||||||
|
node = objects.Node.get_by_uuid(self.context, node['uuid'])
|
||||||
|
|
||||||
|
with mock.patch.object(self.driver.power, 'get_power_state') \
|
||||||
|
as get_power_mock:
|
||||||
|
get_power_mock.return_value = states.POWER_ON
|
||||||
|
|
||||||
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
|
self.service.change_node_power_state,
|
||||||
|
self.context,
|
||||||
|
node,
|
||||||
|
"POWER")
|
||||||
|
node.refresh()
|
||||||
|
get_power_mock.assert_called_once_with(mock.ANY, mock.ANY)
|
||||||
|
self.assertEqual(node['power_state'], states.POWER_ON)
|
||||||
|
self.assertEqual(node['target_power_state'], None)
|
||||||
|
self.assertNotEqual(node['last_error'], None)
|
||||||
|
|
||||||
|
# last_error is cleared when a new transaction happens
|
||||||
|
self.service.change_node_power_state(self.context, node,
|
||||||
|
states.POWER_OFF)
|
||||||
|
node.refresh()
|
||||||
|
self.assertEqual(node['power_state'], states.POWER_OFF)
|
||||||
|
self.assertEqual(node['target_power_state'], None)
|
||||||
|
self.assertEqual(node['last_error'], None)
|
||||||
|
|
||||||
def test_change_node_power_state_already_locked(self):
|
def test_change_node_power_state_already_locked(self):
|
||||||
"""Test if an exception is thrown when applying an exclusive
|
"""Test if an exception is thrown when applying an exclusive
|
||||||
|
@ -225,6 +261,7 @@ class ManagerTestCase(base.DbTestCase):
|
||||||
ndict = utils.get_test_node(driver='fake',
|
ndict = utils.get_test_node(driver='fake',
|
||||||
power_state=states.POWER_ON)
|
power_state=states.POWER_ON)
|
||||||
node = self.dbapi.create_node(ndict)
|
node = self.dbapi.create_node(ndict)
|
||||||
|
node = objects.Node.get_by_uuid(self.context, node['uuid'])
|
||||||
|
|
||||||
# check if the node is locked
|
# check if the node is locked
|
||||||
with task_manager.acquire(self.context, node['id'], shared=False):
|
with task_manager.acquire(self.context, node['id'], shared=False):
|
||||||
|
@ -233,33 +270,65 @@ class ManagerTestCase(base.DbTestCase):
|
||||||
self.context,
|
self.context,
|
||||||
node,
|
node,
|
||||||
states.POWER_ON)
|
states.POWER_ON)
|
||||||
|
node.refresh()
|
||||||
|
self.assertEqual(node['power_state'], states.POWER_ON)
|
||||||
|
self.assertEqual(node['target_power_state'], None)
|
||||||
|
self.assertEqual(node['last_error'], None)
|
||||||
|
|
||||||
def test_change_node_power_state_invalid_state(self):
|
def test_change_node_power_state_already_being_processed(self):
|
||||||
"""Test if an NodeInWrongPowerState exception is thrown
|
"""The target_power_state is expected to be None so it isn't
|
||||||
when the node is in an invalid state to perform current operation.
|
checked in the code. This is what happens if it is not None.
|
||||||
|
(Eg, if a conductor had died during a previous power-off
|
||||||
|
attempt and left the target_power_state set to states.POWER_OFF,
|
||||||
|
and the user is attempting to power-off again.)
|
||||||
"""
|
"""
|
||||||
ndict = utils.get_test_node(driver='fake',
|
ndict = utils.get_test_node(driver='fake',
|
||||||
|
power_state=states.POWER_ON,
|
||||||
|
target_power_state=states.POWER_OFF)
|
||||||
|
node = self.dbapi.create_node(ndict)
|
||||||
|
node = objects.Node.get_by_uuid(self.context, node['uuid'])
|
||||||
|
|
||||||
|
self.service.change_node_power_state(self.context, node,
|
||||||
|
states.POWER_OFF)
|
||||||
|
node.refresh()
|
||||||
|
self.assertEqual(node['power_state'], states.POWER_OFF)
|
||||||
|
self.assertEqual(node['target_power_state'], states.NOSTATE)
|
||||||
|
self.assertEqual(node['last_error'], None)
|
||||||
|
|
||||||
|
def test_change_node_power_state_in_same_state(self):
|
||||||
|
"""Test that we don't try to set the power state if the requested
|
||||||
|
state is the same as the current state.
|
||||||
|
"""
|
||||||
|
ndict = utils.get_test_node(driver='fake',
|
||||||
|
last_error='anything but None',
|
||||||
power_state=states.POWER_ON)
|
power_state=states.POWER_ON)
|
||||||
node = self.dbapi.create_node(ndict)
|
node = self.dbapi.create_node(ndict)
|
||||||
|
node = objects.Node.get_by_uuid(self.context, node['uuid'])
|
||||||
|
|
||||||
with mock.patch.object(self.driver.power, 'get_power_state') \
|
with mock.patch.object(self.driver.power, 'get_power_state') \
|
||||||
as get_power_mock:
|
as get_power_mock:
|
||||||
get_power_mock.return_value = states.POWER_ON
|
get_power_mock.return_value = states.POWER_ON
|
||||||
|
with mock.patch.object(self.driver.power, 'set_power_state') \
|
||||||
|
as set_power_mock:
|
||||||
|
set_power_mock.side_effect = exception.IronicException()
|
||||||
|
|
||||||
self.assertRaises(exception.NodeInWrongPowerState,
|
self.service.change_node_power_state(self.context, node,
|
||||||
self.service.change_node_power_state,
|
states.POWER_ON)
|
||||||
self.context,
|
node.refresh()
|
||||||
node,
|
get_power_mock.assert_called_once_with(mock.ANY, mock.ANY)
|
||||||
states.POWER_ON)
|
self.assertFalse(set_power_mock.called)
|
||||||
get_power_mock.assert_called_once_with(mock.ANY, node)
|
self.assertEqual(node['power_state'], states.POWER_ON)
|
||||||
|
self.assertEqual(node['target_power_state'], None)
|
||||||
|
self.assertEqual(node['last_error'], None)
|
||||||
|
|
||||||
def test_change_node_power_state_invalid_driver_info(self):
|
def test_change_node_power_state_invalid_driver_info(self):
|
||||||
"""Test if an exception is thrown when the driver validation is
|
"""Test if an exception is thrown when the driver validation
|
||||||
failed.
|
fails.
|
||||||
"""
|
"""
|
||||||
ndict = utils.get_test_node(driver='fake',
|
ndict = utils.get_test_node(driver='fake',
|
||||||
power_state=states.POWER_ON)
|
power_state=states.POWER_ON)
|
||||||
node = self.dbapi.create_node(ndict)
|
node = self.dbapi.create_node(ndict)
|
||||||
|
node = objects.Node.get_by_uuid(self.context, node['uuid'])
|
||||||
|
|
||||||
with mock.patch.object(self.driver.power, 'validate') \
|
with mock.patch.object(self.driver.power, 'validate') \
|
||||||
as validate_mock:
|
as validate_mock:
|
||||||
|
@ -271,34 +340,40 @@ class ManagerTestCase(base.DbTestCase):
|
||||||
self.context,
|
self.context,
|
||||||
node,
|
node,
|
||||||
states.POWER_ON)
|
states.POWER_ON)
|
||||||
validate_mock.assert_called_once_with(node)
|
node.refresh()
|
||||||
|
validate_mock.assert_called_once_with(mock.ANY)
|
||||||
|
self.assertEqual(node['power_state'], states.POWER_ON)
|
||||||
|
self.assertEqual(node['target_power_state'], None)
|
||||||
|
self.assertNotEqual(node['last_error'], None)
|
||||||
|
|
||||||
def test_change_node_power_state_set_power_failure(self):
|
def test_change_node_power_state_set_power_failure(self):
|
||||||
"""Test if an exception is thrown when the set_power call is
|
"""Test if an exception is thrown when the set_power call
|
||||||
failed.
|
fails.
|
||||||
"""
|
"""
|
||||||
class TestException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
ndict = utils.get_test_node(driver='fake',
|
ndict = utils.get_test_node(driver='fake',
|
||||||
power_state=states.POWER_OFF)
|
power_state=states.POWER_OFF)
|
||||||
node = self.dbapi.create_node(ndict)
|
node = self.dbapi.create_node(ndict)
|
||||||
|
node = objects.Node.get_by_uuid(self.context, node['uuid'])
|
||||||
|
|
||||||
with mock.patch.object(self.driver.power, 'get_power_state') \
|
with mock.patch.object(self.driver.power, 'get_power_state') \
|
||||||
as get_power_mock:
|
as get_power_mock:
|
||||||
with mock.patch.object(self.driver.power, 'set_power_state') \
|
with mock.patch.object(self.driver.power, 'set_power_state') \
|
||||||
as set_power_mock:
|
as set_power_mock:
|
||||||
get_power_mock.return_value = states.POWER_OFF
|
get_power_mock.return_value = states.POWER_OFF
|
||||||
set_power_mock.side_effect = TestException()
|
set_power_mock.side_effect = exception.IronicException()
|
||||||
|
|
||||||
self.assertRaises(TestException,
|
self.assertRaises(exception.IronicException,
|
||||||
self.service.change_node_power_state,
|
self.service.change_node_power_state,
|
||||||
self.context,
|
self.context,
|
||||||
node,
|
node,
|
||||||
states.POWER_ON)
|
states.POWER_ON)
|
||||||
get_power_mock.assert_called_once_with(mock.ANY, node)
|
node.refresh()
|
||||||
set_power_mock.assert_called_once_with(mock.ANY, node,
|
get_power_mock.assert_called_once_with(mock.ANY, mock.ANY)
|
||||||
|
set_power_mock.assert_called_once_with(mock.ANY, mock.ANY,
|
||||||
states.POWER_ON)
|
states.POWER_ON)
|
||||||
|
self.assertEqual(node['power_state'], states.POWER_OFF)
|
||||||
|
self.assertEqual(node['target_power_state'], None)
|
||||||
|
self.assertNotEqual(node['last_error'], None)
|
||||||
|
|
||||||
def test_vendor_action(self):
|
def test_vendor_action(self):
|
||||||
n = utils.get_test_node(driver='fake')
|
n = utils.get_test_node(driver='fake')
|
||||||
|
|
|
@ -709,3 +709,22 @@ class TestMigrations(BaseMigrationTestCase, WalkVersionsMixin):
|
||||||
conductor.insert().execute,
|
conductor.insert().execute,
|
||||||
{'hostname': None})
|
{'hostname': None})
|
||||||
# FIXME: add check for postgres
|
# FIXME: add check for postgres
|
||||||
|
|
||||||
|
def _pre_upgrade_013(self, engine):
|
||||||
|
nodes = db_utils.get_table(engine, 'nodes')
|
||||||
|
col_names = set(column.name for column in nodes.c)
|
||||||
|
|
||||||
|
self.assertFalse('last_error' in col_names)
|
||||||
|
return col_names
|
||||||
|
|
||||||
|
def _check_013(self, engine, col_names_pre):
|
||||||
|
nodes = db_utils.get_table(engine, 'nodes')
|
||||||
|
col_names = set(column.name for column in nodes.c)
|
||||||
|
|
||||||
|
# didn't lose any columns in the migration
|
||||||
|
self.assertEqual(col_names_pre, col_names.intersection(col_names_pre))
|
||||||
|
|
||||||
|
# only added one 'last_error' column
|
||||||
|
self.assertEqual(len(col_names_pre), len(col_names) - 1)
|
||||||
|
self.assertTrue(isinstance(nodes.c['last_error'].type,
|
||||||
|
getattr(sqlalchemy.types, 'Text')))
|
||||||
|
|
|
@ -72,6 +72,7 @@ def get_test_node(**kw):
|
||||||
'provision_state': kw.get('provision_state', states.NOSTATE),
|
'provision_state': kw.get('provision_state', states.NOSTATE),
|
||||||
'target_provision_state': kw.get('target_provision_state',
|
'target_provision_state': kw.get('target_provision_state',
|
||||||
states.NOSTATE),
|
states.NOSTATE),
|
||||||
|
'last_error': kw.get('last_error', None),
|
||||||
'instance_uuid': kw.get('instance_uuid', None),
|
'instance_uuid': kw.get('instance_uuid', None),
|
||||||
'driver': kw.get('driver', 'fake'),
|
'driver': kw.get('driver', 'fake'),
|
||||||
'driver_info': kw.get('driver_info', fake_info),
|
'driver_info': kw.get('driver_info', fake_info),
|
||||||
|
|
|
@ -44,7 +44,10 @@ class FakeDriverTestCase(base.TestCase):
|
||||||
def test_power_interface(self):
|
def test_power_interface(self):
|
||||||
self.driver.power.validate(self.node)
|
self.driver.power.validate(self.node)
|
||||||
self.driver.power.get_power_state(None, self.node)
|
self.driver.power.get_power_state(None, self.node)
|
||||||
self.driver.power.set_power_state(None, None, states.NOSTATE)
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
|
self.driver.power.set_power_state,
|
||||||
|
None, None, states.NOSTATE)
|
||||||
|
self.driver.power.set_power_state(None, None, states.POWER_ON)
|
||||||
self.driver.power.reboot(None, None)
|
self.driver.power.reboot(None, None)
|
||||||
|
|
||||||
def test_deploy_interface(self):
|
def test_deploy_interface(self):
|
||||||
|
|
Loading…
Reference in New Issue