Merge "API Nodes states"
This commit is contained in:
commit
c4de596b11
|
@ -28,6 +28,7 @@ from ironic.api.controllers.v1 import base
|
|||
from ironic.api.controllers.v1 import collection
|
||||
from ironic.api.controllers.v1 import link
|
||||
from ironic.api.controllers.v1 import port
|
||||
from ironic.api.controllers.v1 import state
|
||||
from ironic.api.controllers.v1 import utils
|
||||
from ironic.common import exception
|
||||
from ironic.openstack.common import log
|
||||
|
@ -35,6 +36,136 @@ from ironic.openstack.common import log
|
|||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class NodePowerState(state.State):
|
||||
@classmethod
|
||||
def convert_with_links(cls, rpc_node, expand=True):
|
||||
power_state = NodePowerState()
|
||||
# FIXME(lucasagomes): this request could potentially take a
|
||||
# while. It's dependent upon the driver talking to the hardware. At
|
||||
# least with IPMI, this often times out, and even fails after 3
|
||||
# retries at a statistically significant frequency....
|
||||
power_state.current = pecan.request.rpcapi.get_node_power_state(
|
||||
pecan.request.context,
|
||||
rpc_node.uuid)
|
||||
url_arg = '%s/state/power' % rpc_node.uuid
|
||||
power_state.links = [link.Link.make_link('self',
|
||||
pecan.request.host_url,
|
||||
'nodes', url_arg),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'nodes', url_arg,
|
||||
bookmark=True)
|
||||
]
|
||||
if expand:
|
||||
power_state.target = rpc_node.target_power_state
|
||||
# TODO(lucasagomes): get_next_power_available_states
|
||||
power_state.available = []
|
||||
return power_state
|
||||
|
||||
|
||||
class NodePowerStateController(rest.RestController):
|
||||
|
||||
# GET nodes/<uuid>/state/power
|
||||
@wsme_pecan.wsexpose(NodePowerState, unicode)
|
||||
def get(self, node_id):
|
||||
node = objects.Node.get_by_uuid(pecan.request.context, node_id)
|
||||
return NodePowerState.convert_with_links(node)
|
||||
|
||||
# PUT nodes/<uuid>/state/power
|
||||
@wsme_pecan.wsexpose(NodePowerState, unicode, unicode, status=202)
|
||||
def put(self, node_id, target):
|
||||
"""Set the power state of the machine."""
|
||||
node = objects.Node.get_by_uuid(pecan.request.context, node_id)
|
||||
if node.target_power_state is not None:
|
||||
raise wsme.exc.ClientSideError(_("One power operation is "
|
||||
"already in process"))
|
||||
#TODO(lucasagomes): Test if target is a valid state and if it's able
|
||||
# to transition to the target state from the current one
|
||||
|
||||
node['target_power_state'] = target
|
||||
updated_node = pecan.request.rpcapi.update_node(pecan.request.context,
|
||||
node)
|
||||
pecan.request.rpcapi.start_power_state_change(pecan.request.context,
|
||||
updated_node, target)
|
||||
return NodePowerState.convert_with_links(updated_node, expand=False)
|
||||
|
||||
|
||||
class NodeProvisionState(state.State):
|
||||
@classmethod
|
||||
def convert_with_links(cls, rpc_node, expand=True):
|
||||
provision_state = NodeProvisionState()
|
||||
provision_state.current = rpc_node.provision_state
|
||||
url_arg = '%s/state/provision' % rpc_node.uuid
|
||||
provision_state.links = [link.Link.make_link('self',
|
||||
pecan.request.host_url,
|
||||
'nodes', url_arg),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'nodes', url_arg,
|
||||
bookmark=True)
|
||||
]
|
||||
if expand:
|
||||
provision_state.target = rpc_node.target_provision_state
|
||||
# TODO(lucasagomes): get_next_provision_available_states
|
||||
provision_state.available = []
|
||||
return provision_state
|
||||
|
||||
|
||||
class NodeProvisionStateController(rest.RestController):
|
||||
|
||||
# GET nodes/<uuid>/state/provision
|
||||
@wsme_pecan.wsexpose(NodeProvisionState, unicode)
|
||||
def get(self, node_id):
|
||||
node = objects.Node.get_by_uuid(pecan.request.context, node_id)
|
||||
provision_state = NodeProvisionState.convert_with_links(node)
|
||||
return provision_state
|
||||
|
||||
# PUT nodes/<uuid>/state/provision
|
||||
@wsme_pecan.wsexpose(NodeProvisionState, unicode, unicode, status=202)
|
||||
def put(self, node_id, target):
|
||||
"""Set the provision state of the machine."""
|
||||
#TODO(lucasagomes): Test if target is a valid state and if it's able
|
||||
# to transition to the target state from the current one
|
||||
# TODO(lucasagomes): rpcapi.start_provision_state_change()
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class NodeStates(base.APIBase):
|
||||
"""API representation of the states of a node."""
|
||||
|
||||
power = NodePowerState
|
||||
"The current power state of the node"
|
||||
|
||||
provision = NodeProvisionState
|
||||
"The current provision state of the node"
|
||||
|
||||
@classmethod
|
||||
def convert_with_links(cls, rpc_node):
|
||||
states = NodeStates()
|
||||
states.power = NodePowerState.convert_with_links(rpc_node,
|
||||
expand=False)
|
||||
states.provision = NodeProvisionState.convert_with_links(rpc_node,
|
||||
expand=False)
|
||||
return states
|
||||
|
||||
|
||||
class NodeStatesController(rest.RestController):
|
||||
|
||||
power = NodePowerStateController()
|
||||
"Expose the power controller action as a sub-element of state"
|
||||
|
||||
provision = NodeProvisionStateController()
|
||||
"Expose the provision controller action as a sub-element of state"
|
||||
|
||||
# GET nodes/<uuid>/state
|
||||
@wsme_pecan.wsexpose(NodeStates, unicode)
|
||||
def get(self, node_id):
|
||||
"""List or update the state of a node."""
|
||||
node = objects.Node.get_by_uuid(pecan.request.context, node_id)
|
||||
state = NodeStates.convert_with_links(node)
|
||||
return state
|
||||
|
||||
|
||||
class Node(base.APIBase):
|
||||
"""API representation of a bare metal node.
|
||||
|
||||
|
@ -46,9 +177,17 @@ class Node(base.APIBase):
|
|||
uuid = wtypes.text
|
||||
instance_uuid = wtypes.text
|
||||
|
||||
# NOTE: task_* fields probably need to be reworked to match API spec
|
||||
task_state = wtypes.text
|
||||
task_start = wtypes.text
|
||||
power_state = wtypes.text
|
||||
"Represent the current (not transition) power state of the node"
|
||||
|
||||
target_power_state = wtypes.text
|
||||
"The user modified desired power state of the node."
|
||||
|
||||
provision_state = wtypes.text
|
||||
"Represent the current (not transition) provision state of the node"
|
||||
|
||||
target_provision_state = wtypes.text
|
||||
"The user modified desired provision state of the node."
|
||||
|
||||
# NOTE: allow arbitrary dicts for driver_info and extra so that drivers
|
||||
# and vendors can expand on them without requiring API changes.
|
||||
|
@ -120,6 +259,9 @@ class NodeCollection(collection.Collection):
|
|||
class NodesController(rest.RestController):
|
||||
"""REST controller for Nodes."""
|
||||
|
||||
state = NodeStatesController()
|
||||
"Expose the state controller action as a sub-element of nodes"
|
||||
|
||||
_custom_actions = {
|
||||
'ports': ['GET'],
|
||||
}
|
||||
|
@ -169,9 +311,14 @@ class NodesController(rest.RestController):
|
|||
# to a dict and stripping keys with value=None
|
||||
delta = node_data.as_terse_dict()
|
||||
|
||||
# NOTE: state transitions are separate from informational changes
|
||||
# so don't pass a task_state to update_node.
|
||||
new_state = delta.pop('task_state', None)
|
||||
# Prevent states from being updated
|
||||
state_rel_attr = ['power_state', 'target_power_state',
|
||||
'provision_state', 'target_provision_state']
|
||||
if any((getattr(node_data, attr) for attr in state_rel_attr)):
|
||||
raise wsme.exc.ClientSideError(_("Changing states is not allowed "
|
||||
"here; You must use the "
|
||||
"nodes/%s/state interface.")
|
||||
% node_id)
|
||||
|
||||
response = wsme.api.Response(Node(), status_code=200)
|
||||
try:
|
||||
|
@ -190,12 +337,6 @@ class NodesController(rest.RestController):
|
|||
LOG.exception(e)
|
||||
response.status_code = 500
|
||||
|
||||
if new_state:
|
||||
# NOTE: state change is async, so change the REST response
|
||||
response.status_code = 202
|
||||
pecan.request.rpcapi.start_state_change(pecan.request.context,
|
||||
node, new_state)
|
||||
|
||||
# TODO(deva): return the response object instead of raising
|
||||
# after wsme 0.5b3 is released
|
||||
if response.status_code not in [200, 202]:
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 wsme import types as wtypes
|
||||
|
||||
from ironic.api.controllers.v1 import base
|
||||
from ironic.api.controllers.v1 import link
|
||||
|
||||
|
||||
class State(base.APIBase):
|
||||
|
||||
current = wtypes.text
|
||||
"The current state"
|
||||
|
||||
target = wtypes.text
|
||||
"The user modified desired state"
|
||||
|
||||
available = [wtypes.text]
|
||||
"A list of available states it is able to transition to"
|
||||
|
||||
links = [link.Link]
|
||||
"A list containing a self link and associated state links"
|
|
@ -98,7 +98,7 @@ class ConductorManager(service.PeriodicService):
|
|||
LOG.debug("RPC update_node called for node %s." % node_id)
|
||||
|
||||
delta = node_obj.obj_what_changed()
|
||||
if 'task_state' in delta:
|
||||
if 'power_state' in delta:
|
||||
raise exception.IronicException(_(
|
||||
"Invalid method call: update_node can not change node state."))
|
||||
|
||||
|
@ -108,30 +108,30 @@ class ConductorManager(service.PeriodicService):
|
|||
if 'driver_info' in delta:
|
||||
task.driver.deploy.validate(node_obj)
|
||||
task.driver.power.validate(node_obj)
|
||||
node_obj['task_state'] = task.driver.power.get_power_state
|
||||
node_obj['power_state'] = task.driver.power.get_power_state
|
||||
|
||||
# TODO(deva): Determine what value will be passed by API when
|
||||
# instance_uuid needs to be unset, and handle it.
|
||||
if 'instance_uuid' in delta:
|
||||
if node_obj['task_state'] != states.POWER_OFF:
|
||||
if node_obj['power_state'] != states.POWER_OFF:
|
||||
raise exception.NodeInWrongPowerState(
|
||||
node=node_id,
|
||||
pstate=node_obj['task_state'])
|
||||
pstate=node_obj['power_state'])
|
||||
|
||||
# update any remaining parameters, then save
|
||||
node_obj.save(context)
|
||||
|
||||
return node_obj
|
||||
|
||||
def start_state_change(self, context, node_obj, new_state):
|
||||
def start_power_state_change(self, context, node_obj, new_state):
|
||||
"""RPC method to encapsulate changes to a node's state.
|
||||
|
||||
Perform actions such as power on, power off, deploy, and cleanup.
|
||||
Perform actions such as power on and power off.
|
||||
|
||||
TODO
|
||||
|
||||
:param context: an admin context
|
||||
:param node_obj: an RPC-style node object
|
||||
:param new_state: the desired state of the node
|
||||
:param new_state: the desired power state of the node
|
||||
"""
|
||||
pass
|
||||
|
|
|
@ -32,7 +32,7 @@ class ConductorAPI(ironic.openstack.common.rpc.proxy.RpcProxy):
|
|||
|
||||
1.0 - Initial version.
|
||||
Included get_node_power_status
|
||||
1.1 - Added update_node and start_state_change.
|
||||
1.1 - Added update_node and start_power_state_change.
|
||||
"""
|
||||
|
||||
RPC_API_VERSION = '1.1'
|
||||
|
@ -66,8 +66,8 @@ class ConductorAPI(ironic.openstack.common.rpc.proxy.RpcProxy):
|
|||
the core drivers. If instance_uuid is passed, it will be set or unset
|
||||
only if the node is properly configured.
|
||||
|
||||
Note that task_state should not be passed via this method.
|
||||
Use start_state_change for initiating driver actions.
|
||||
Note that power_state should not be passed via this method.
|
||||
Use start_power_state_change for initiating driver actions.
|
||||
|
||||
:param context: request context.
|
||||
:param node_obj: a changed (but not saved) node object.
|
||||
|
@ -77,7 +77,7 @@ class ConductorAPI(ironic.openstack.common.rpc.proxy.RpcProxy):
|
|||
self.make_msg('update_node',
|
||||
node_obj=node_obj))
|
||||
|
||||
def start_state_change(self, context, node_obj, new_state):
|
||||
def start_power_state_change(self, context, node_obj, new_state):
|
||||
"""Asynchronously perform an action on a node.
|
||||
|
||||
:param context: request context.
|
||||
|
@ -85,6 +85,6 @@ class ConductorAPI(ironic.openstack.common.rpc.proxy.RpcProxy):
|
|||
:param new_state: one of ironic.common.states power state values
|
||||
"""
|
||||
self.cast(context,
|
||||
self.make_msg('start_state_change',
|
||||
self.make_msg('start_power_state_change',
|
||||
node_obj=node_obj,
|
||||
new_state=new_state))
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# 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, String
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
nodes = Table('nodes', meta, autoload=True)
|
||||
|
||||
# Drop task_* columns
|
||||
nodes.c.task_start.drop()
|
||||
nodes.c.task_state.drop()
|
||||
|
||||
# Create new states columns
|
||||
nodes.create_column(Column('power_state', String(15), nullable=True))
|
||||
nodes.create_column(Column('target_power_state', String(15),
|
||||
nullable=True))
|
||||
nodes.create_column(Column('provision_state', String(15), nullable=True))
|
||||
nodes.create_column(Column('target_provision_state', String(15),
|
||||
nullable=True))
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
raise NotImplementedError('Downgrade from version 009 is unsupported.')
|
|
@ -25,7 +25,7 @@ import urlparse
|
|||
from oslo.config import cfg
|
||||
|
||||
from sqlalchemy import Column, ForeignKey
|
||||
from sqlalchemy import Integer, String, DateTime
|
||||
from sqlalchemy import Integer, String
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.types import TypeDecorator, VARCHAR
|
||||
|
||||
|
@ -97,8 +97,10 @@ class Node(Base):
|
|||
uuid = Column(String(36), unique=True)
|
||||
instance_uuid = Column(String(36), nullable=True, unique=True)
|
||||
chassis_id = Column(Integer, ForeignKey('chassis.id'), nullable=True)
|
||||
task_start = Column(DateTime, nullable=True)
|
||||
task_state = Column(String(15))
|
||||
power_state = Column(String(15), nullable=True)
|
||||
target_power_state = Column(String(15), nullable=True)
|
||||
provision_state = Column(String(15), nullable=True)
|
||||
target_provision_state = Column(String(15), nullable=True)
|
||||
properties = Column(JSONEncodedDict)
|
||||
driver = Column(String(15))
|
||||
driver_info = Column(JSONEncodedDict)
|
||||
|
|
|
@ -40,8 +40,10 @@ class Node(base.IronicObject):
|
|||
|
||||
'properties': utils.dict_or_none,
|
||||
'reservation': utils.str_or_none,
|
||||
'task_state': utils.str_or_none,
|
||||
'task_start': utils.datetime_or_none,
|
||||
'power_state': utils.str_or_none,
|
||||
'target_power_state': utils.str_or_none,
|
||||
'provision_state': utils.str_or_none,
|
||||
'target_provision_state': utils.str_or_none,
|
||||
'extra': utils.dict_or_none,
|
||||
}
|
||||
|
||||
|
@ -70,7 +72,7 @@ class Node(base.IronicObject):
|
|||
"""Save updates to this Node.
|
||||
|
||||
Column-wise updates will be made based on the result of
|
||||
self.what_changed(). If expected_task_state is provided,
|
||||
self.what_changed(). If target_power_state is provided,
|
||||
it will be checked against the in-database copy of the
|
||||
node before updates are made.
|
||||
|
||||
|
|
|
@ -17,8 +17,10 @@ Tests for the API /nodes/ methods.
|
|||
"""
|
||||
|
||||
import mox
|
||||
import webtest.app
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import states
|
||||
from ironic.conductor import rpcapi
|
||||
from ironic.openstack.common import uuidutils
|
||||
from ironic.tests.api import base
|
||||
|
@ -101,6 +103,38 @@ class TestListNodes(base.FunctionalTest):
|
|||
self.assertEqual(len(data['items']), 1)
|
||||
self.assertEqual(len(data['links']), 1)
|
||||
|
||||
def test_state(self):
|
||||
ndict = dbutils.get_test_node()
|
||||
self.dbapi.create_node(ndict)
|
||||
data = self.get_json('/nodes/%s/state' % ndict['uuid'])
|
||||
[self.assertIn(key, data) for key in ['power', 'provision']]
|
||||
|
||||
# Check if it only returns a sub-set of the attributes
|
||||
[self.assertIn(key, ['current', 'links'])
|
||||
for key in data['power'].keys()]
|
||||
[self.assertIn(key, ['current', 'links'])
|
||||
for key in data['provision'].keys()]
|
||||
|
||||
def test_power_state(self):
|
||||
ndict = dbutils.get_test_node()
|
||||
self.dbapi.create_node(ndict)
|
||||
data = self.get_json('/nodes/%s/state/power' % ndict['uuid'])
|
||||
[self.assertIn(key, data) for key in
|
||||
['available', 'current', 'target', 'links']]
|
||||
#TODO(lucasagomes): Add more tests to check to which states it can
|
||||
# transition to from the current one, and check if they are present
|
||||
# in the available list.
|
||||
|
||||
def test_provision_state(self):
|
||||
ndict = dbutils.get_test_node()
|
||||
self.dbapi.create_node(ndict)
|
||||
data = self.get_json('/nodes/%s/state/provision' % ndict['uuid'])
|
||||
[self.assertIn(key, data) for key in
|
||||
['available', 'current', 'target', 'links']]
|
||||
#TODO(lucasagomes): Add more tests to check to which states it can
|
||||
# transition to from the current one, and check if they are present
|
||||
# in the available list.
|
||||
|
||||
|
||||
class TestPatch(base.FunctionalTest):
|
||||
|
||||
|
@ -109,7 +143,8 @@ class TestPatch(base.FunctionalTest):
|
|||
ndict = dbutils.get_test_node()
|
||||
self.node = self.dbapi.create_node(ndict)
|
||||
self.mox.StubOutWithMock(rpcapi.ConductorAPI, 'update_node')
|
||||
self.mox.StubOutWithMock(rpcapi.ConductorAPI, 'start_state_change')
|
||||
self.mox.StubOutWithMock(rpcapi.ConductorAPI,
|
||||
'start_power_state_change')
|
||||
|
||||
def test_update_ok(self):
|
||||
rpcapi.ConductorAPI.update_node(mox.IgnoreArg(), mox.IgnoreArg()).\
|
||||
|
@ -123,18 +158,9 @@ class TestPatch(base.FunctionalTest):
|
|||
self.mox.VerifyAll()
|
||||
|
||||
def test_update_state(self):
|
||||
rpcapi.ConductorAPI.update_node(mox.IgnoreArg(), mox.IgnoreArg()).\
|
||||
AndReturn(self.node)
|
||||
rpcapi.ConductorAPI.start_state_change(mox.IgnoreArg(),
|
||||
mox.IgnoreArg(), mox.IgnoreArg())
|
||||
self.mox.ReplayAll()
|
||||
|
||||
response = self.patch_json('/nodes/%s' % self.node['uuid'],
|
||||
{'task_state': 'new state'})
|
||||
self.assertEqual(response.content_type, 'application/json')
|
||||
# TODO(deva): change to 202 when wsme 0.5b3 is released
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.mox.VerifyAll()
|
||||
self.assertRaises(webtest.app.AppError, self.patch_json,
|
||||
'/nodes/%s' % self.node['uuid'],
|
||||
{'power_state': 'new state'})
|
||||
|
||||
def test_update_fails_bad_driver_info(self):
|
||||
fake_err = 'Fake Error Message'
|
||||
|
@ -185,3 +211,43 @@ class TestDelete(base.FunctionalTest):
|
|||
self.assertEqual(response.status_int, 500)
|
||||
self.assertEqual(response.content_type, 'application/json')
|
||||
self.assertTrue(response.json['error_message'])
|
||||
|
||||
|
||||
class TestPut(base.FunctionalTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPut, self).setUp()
|
||||
ndict = dbutils.get_test_node()
|
||||
self.node = self.dbapi.create_node(ndict)
|
||||
self.mox.StubOutWithMock(rpcapi.ConductorAPI, 'update_node')
|
||||
self.mox.StubOutWithMock(rpcapi.ConductorAPI,
|
||||
'start_power_state_change')
|
||||
|
||||
def test_power_state(self):
|
||||
rpcapi.ConductorAPI.update_node(mox.IgnoreArg(), mox.IgnoreArg()).\
|
||||
AndReturn(self.node)
|
||||
rpcapi.ConductorAPI.start_power_state_change(mox.IgnoreArg(),
|
||||
mox.IgnoreArg(),
|
||||
mox.IgnoreArg())
|
||||
self.mox.ReplayAll()
|
||||
|
||||
response = self.put_json('/nodes/%s/state/power' % self.node['uuid'],
|
||||
{'target': states.POWER_ON})
|
||||
self.assertEqual(response.content_type, 'application/json')
|
||||
# FIXME(lucasagomes): WSME should return 202 not 200
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_power_state_in_progress(self):
|
||||
rpcapi.ConductorAPI.update_node(mox.IgnoreArg(), mox.IgnoreArg()).\
|
||||
AndReturn(self.node)
|
||||
rpcapi.ConductorAPI.start_power_state_change(mox.IgnoreArg(),
|
||||
mox.IgnoreArg(),
|
||||
mox.IgnoreArg())
|
||||
self.mox.ReplayAll()
|
||||
self.put_json('/nodes/%s/state/power' % self.node['uuid'],
|
||||
{'target': states.POWER_ON})
|
||||
self.assertRaises(webtest.app.AppError, self.put_json,
|
||||
'/nodes/%s/state/power' % self.node['uuid'],
|
||||
{'target': states.POWER_ON})
|
||||
self.mox.VerifyAll()
|
||||
|
|
|
@ -96,7 +96,8 @@ class ManagerTestCase(base.DbTestCase):
|
|||
|
||||
def test_update_node_invalid_state(self):
|
||||
ndict = utils.get_test_node(driver='fake', extra={'test': 'one'},
|
||||
instance_uuid=None, task_state=states.POWER_ON)
|
||||
instance_uuid=None,
|
||||
power_state=states.POWER_ON)
|
||||
node = self.dbapi.create_node(ndict)
|
||||
|
||||
# check that it fails because state is POWER_ON
|
||||
|
|
|
@ -90,8 +90,8 @@ class RPCAPITestCase(base.DbTestCase):
|
|||
'call',
|
||||
node_obj=self.fake_node)
|
||||
|
||||
def test_start_state_change(self):
|
||||
self._test_rpcapi('start_state_change',
|
||||
def test_start_power_state_change(self):
|
||||
self._test_rpcapi('start_power_state_change',
|
||||
'cast',
|
||||
node_obj=self.fake_node,
|
||||
new_state=states.POWER_ON)
|
||||
|
|
|
@ -125,7 +125,7 @@ class ExclusiveLockDecoratorTestCase(base.DbTestCase):
|
|||
def do_state_change(task):
|
||||
for r in task.resources:
|
||||
task.dbapi.update_node(r.node.uuid,
|
||||
{'task_state': 'test-state'})
|
||||
{'power_state': 'test-state'})
|
||||
|
||||
with task_manager.acquire(self.uuids, shared=True) as task:
|
||||
self.assertRaises(exception.ExclusiveLockRequired,
|
||||
|
@ -137,13 +137,13 @@ class ExclusiveLockDecoratorTestCase(base.DbTestCase):
|
|||
|
||||
for uuid in self.uuids:
|
||||
res = self.dbapi.get_node(uuid)
|
||||
self.assertEqual('test-state', res.task_state)
|
||||
self.assertEqual('test-state', res.power_state)
|
||||
|
||||
@task_manager.require_exclusive_lock
|
||||
def _do_state_change(self, task):
|
||||
for r in task.resources:
|
||||
task.dbapi.update_node(r.node.uuid,
|
||||
{'task_state': 'test-state'})
|
||||
{'power_state': 'test-state'})
|
||||
|
||||
def test_require_exclusive_lock_on_object(self):
|
||||
with task_manager.acquire(self.uuids, shared=True) as task:
|
||||
|
@ -156,7 +156,7 @@ class ExclusiveLockDecoratorTestCase(base.DbTestCase):
|
|||
|
||||
for uuid in self.uuids:
|
||||
res = self.dbapi.get_node(uuid)
|
||||
self.assertEqual('test-state', res.task_state)
|
||||
self.assertEqual('test-state', res.power_state)
|
||||
|
||||
def test_one_node_per_task_properties(self):
|
||||
with task_manager.acquire(self.uuids) as task:
|
||||
|
|
|
@ -656,3 +656,18 @@ class TestMigrations(BaseMigrationTestCase, WalkVersionsMixin):
|
|||
chassis = db_utils.get_table(engine, 'chassis')
|
||||
self.assertTrue(isinstance(chassis.c.description.type,
|
||||
sqlalchemy.types.String))
|
||||
|
||||
def _check_009(self, engine, data):
|
||||
nodes = db_utils.get_table(engine, 'nodes')
|
||||
col_names = [column.name for column in nodes.c]
|
||||
|
||||
self.assertFalse('task_start' in col_names)
|
||||
self.assertFalse('task_state' in col_names)
|
||||
|
||||
new_col = {'power_state': 'String',
|
||||
'target_power_state': 'String',
|
||||
'provision_state': 'String',
|
||||
'target_provision_state': 'String'}
|
||||
for col, coltype in new_col.items():
|
||||
self.assertTrue(isinstance(nodes.c[col].type,
|
||||
getattr(sqlalchemy.types, coltype)))
|
||||
|
|
|
@ -158,12 +158,12 @@ class DbNodeTestCase(base.DbTestCase):
|
|||
def test_update_node(self):
|
||||
n = self._create_test_node()
|
||||
|
||||
old_state = n['task_state']
|
||||
new_state = 'TESTSTATE'
|
||||
self.assertNotEqual(old_state, new_state)
|
||||
old_extra = n['extra']
|
||||
new_extra = {'foo': 'bar'}
|
||||
self.assertNotEqual(old_extra, new_extra)
|
||||
|
||||
res = self.dbapi.update_node(n['id'], {'task_state': new_state})
|
||||
self.assertEqual(new_state, res['task_state'])
|
||||
res = self.dbapi.update_node(n['id'], {'extra': new_extra})
|
||||
self.assertEqual(new_extra, res['extra'])
|
||||
|
||||
def test_reserve_one_node(self):
|
||||
n = self._create_test_node()
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
# under the License.
|
||||
"""Ironic test utilities."""
|
||||
|
||||
from ironic.common import states
|
||||
|
||||
from ironic.openstack.common import jsonutils as json
|
||||
|
||||
|
||||
|
@ -70,8 +72,10 @@ def get_test_node(**kw):
|
|||
'id': kw.get('id', 123),
|
||||
'uuid': kw.get('uuid', '1be26c0b-03f2-4d2e-ae87-c02d7f33c123'),
|
||||
'chassis_id': 42,
|
||||
'task_start': None,
|
||||
'task_state': kw.get('task_state', 'NOSTATE'),
|
||||
'power_state': kw.get('power_state', states.NOSTATE),
|
||||
'target_power_state': None,
|
||||
'provision_state': kw.get('provision_state', states.NOSTATE),
|
||||
'target_provision_state': None,
|
||||
'instance_uuid': kw.get('instance_uuid',
|
||||
'8227348d-5f1d-4488-aad1-7c92b2d42504'),
|
||||
'driver': kw.get('driver', 'fake'),
|
||||
|
|
Loading…
Reference in New Issue