summaryrefslogtreecommitdiff
path: root/ironic/api/controllers/v1/node.py
diff options
context:
space:
mode:
authorLucas Alvares Gomes <lucasagomes@gmail.com>2013-08-07 09:49:24 +0100
committerLucas Alvares Gomes <lucasagomes@gmail.com>2013-08-22 09:17:02 +0100
commitbce74ec6fe779409f8020f2b989e9de7308df36f (patch)
tree96c2417b7cf209194cfb6058523c2e0164c5a6b5 /ironic/api/controllers/v1/node.py
parentf15510bae38c345143a677af72c300f9c54cd7dc (diff)
downloadironic-bce74ec6fe779409f8020f2b989e9de7308df36f.tar.gz
API Nodes states
Expose the power state and provision state of a baremetal node in the API, each state is represented by a separate entity where clients can GET/PUT value from/to it. A generic /state subresource is also exposed allowing clients to GET all the current states of that node and links to each one of the specific states. Partially implements blueprint state-machine Change-Id: I0dcea268718dc3bcb152880654631f9fd61295e7
Diffstat (limited to 'ironic/api/controllers/v1/node.py')
-rw-r--r--ironic/api/controllers/v1/node.py165
1 files changed, 153 insertions, 12 deletions
diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
index d6a0006b2..13c9ab9a9 100644
--- a/ironic/api/controllers/v1/node.py
+++ b/ironic/api/controllers/v1/node.py
@@ -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]: