def do_node_deploy(self, context, node_id, rebuild=False): """RPC method to initiate deployment to a node. Initiate the deployment of a node. Validations are done synchronously and the actual deploy work is performed in background (asynchronously). :param context: an admin context. :param node_id: the id or uuid of a node. :param rebuild: True if this is a rebuild request. A rebuild will recreate the instance on the same node, overwriting all disk. The ephemeral partition, if it exists, can optionally be preserved. :raises: InstanceDeployFailure :raises: NodeInMaintenance if the node is in maintenance mode. :raises: NoFreeConductorWorker when there is no free worker to start async task. """ LOG.debug("RPC do_node_deploy called for node %s." % node_id) # NOTE(comstud): If the _sync_power_states() periodic task happens # to have locked this node, we'll fail to acquire the lock. The # client should perhaps retry in this case unless we decide we # want to add retries or extra synchronization here. with task_manager.acquire(context, node_id, shared=False) as task: node = task.node # May only rebuild a node in ACTIVE state if rebuild and (node.provision_state != states.ACTIVE): raise exception.InstanceDeployFailure(_( "RPC do_node_deploy called to rebuild %(node)s, but " "provision state is %(curstate)s. Must be %(state)s.") % {'node': node.uuid, 'curstate': node.provision_state, 'state': states.ACTIVE}) elif node.provision_state != states.NOSTATE and not rebuild: raise exception.InstanceDeployFailure(_( "RPC do_node_deploy called for %(node)s, but provision " "state is already %(state)s.") % {'node': node.uuid, 'state': node.provision_state}) if node.maintenance: raise exception.NodeInMaintenance(op=_('provisioning'), node=node.uuid) try: task.driver.deploy.validate(task) except exception.InvalidParameterValue as e: raise exception.InstanceDeployFailure(_( "RPC do_node_deploy failed to validate deploy info. " "Error: %(msg)s") % {'msg': e}) # Set target state to expose that work is in progress node.provision_state = states.DEPLOYING node.target_provision_state = states.DEPLOYDONE node.last_error = None node.save(context) task.spawn_after(self._spawn_worker, self._do_node_deploy, context, task)
def do_node_deploy(self, context, node_id): """RPC method to initiate deployment to a node. Initiate the deployment of a node. Validations are done synchronously and the actual deploy work is performed in background (asynchronously). :param context: an admin context. :param node_id: the id or uuid of a node. :raises: InstanceDeployFailure :raises: NodeInMaintenance if the node is in maintenance mode. :raises: NoFreeConductorWorker when there is no free worker to start async task. """ LOG.debug(_("RPC do_node_deploy called for node %s.") % node_id) task = task_manager.TaskManager(context, node_id, shared=False) node = task.node try: if node.provision_state is not states.NOSTATE: raise exception.InstanceDeployFailure( _("RPC do_node_deploy called for %(node)s, but provision " "state is already %(state)s.") % { 'node': node.uuid, 'state': node.provision_state }) if node.maintenance: raise exception.NodeInMaintenance(op=_('provisioning'), node=node.uuid) try: task.driver.deploy.validate(task, node) except exception.InvalidParameterValue as e: raise exception.InstanceDeployFailure( _("RPC do_node_deploy failed to validate deploy info. " "Error: %(msg)s") % {'msg': e}) # Set target state to expose that work is in progress node.provision_state = states.DEPLOYING node.target_provision_state = states.DEPLOYDONE node.last_error = None node.save(context) # Start requested action in the background. thread = self._spawn_worker(self._do_node_deploy, context, task) # Release node lock at the end. thread.link(lambda t: task.release_resources()) except Exception: with excutils.save_and_reraise_exception(): # Release node lock if error occurred. task.release_resources()
def test_provision_node_in_maintenance_fail(self): with mock.patch.object(rpcapi.ConductorAPI, 'do_node_deploy') as dnd: ndict = dbutils.get_test_node(id=1, uuid=utils.generate_uuid(), maintenance=True) node = self.dbapi.create_node(ndict) dnd.side_effect = exception.NodeInMaintenance(op='provisioning', node=node.uuid) ret = self.put_json('/nodes/%s/states/provision' % node.uuid, {'target': states.ACTIVE}, expect_errors=True) self.assertEqual(400, ret.status_code) self.assertTrue(ret.json['error_message'])
def provision(self, node_uuid, target): """Asynchronous trigger the provisioning of the node. This will set the target provision state of the node, and a background task will begin which actually applies the state change. This call will return a 202 (Accepted) indicating the request was accepted and is in progress; the client should continue to GET the status of this node to observe the status of the requested action. :param node_uuid: UUID of a node. :param target: The desired provision state of the node. :raises: ClientSideError (HTTP 409) if the node is already being provisioned. :raises: ClientSideError (HTTP 400) if the node is already in the requested state. :raises: InvalidStateRequested (HTTP 400) if the requested target state is not valid. """ rpc_node = objects.Node.get_by_uuid(pecan.request.context, node_uuid) topic = pecan.request.rpcapi.get_topic_for(rpc_node) if rpc_node.maintenance: op = _('provisioning') raise exception.NodeInMaintenance(op=op, node=node_uuid) if rpc_node.target_provision_state is not None: msg = _('Node %s is already being provisioned.') % rpc_node['uuid'] LOG.exception(msg) raise wsme.exc.ClientSideError(msg, status_code=409) # Conflict if target == rpc_node.provision_state: msg = (_("Node %(node)s is already in the '%(state)s' state.") % { 'node': rpc_node['uuid'], 'state': target }) LOG.exception(msg) raise wsme.exc.ClientSideError(msg, status_code=400) # Note that there is a race condition. The node state(s) could change # by the time the RPC call is made and the TaskManager manager gets a # lock. if target == ir_states.ACTIVE: pecan.request.rpcapi.do_node_deploy(pecan.request.context, node_uuid, topic) elif target == ir_states.DELETED: pecan.request.rpcapi.do_node_tear_down(pecan.request.context, node_uuid, topic) else: raise exception.InvalidStateRequested(state=target, node=node_uuid)
def validate_node(task, event='deploy'): """Validate that a node is suitable for deployment/rebuilding. :param task: a TaskManager instance. :param event: event to process: deploy or rebuild. :raises: NodeInMaintenance, NodeProtected, InvalidStateRequested """ if task.node.maintenance: raise exception.NodeInMaintenance(op=_('provisioning'), node=task.node.uuid) if event == 'rebuild' and task.node.protected: raise exception.NodeProtected(node=task.node.uuid) if not task.fsm.is_actionable_event(event): raise exception.InvalidStateRequested( action=event, node=task.node.uuid, state=task.node.provision_state)
def provision(self, node_ident, target, configdrive=None): """Asynchronous trigger the provisioning of the node. This will set the target provision state of the node, and a background task will begin which actually applies the state change. This call will return a 202 (Accepted) indicating the request was accepted and is in progress; the client should continue to GET the status of this node to observe the status of the requested action. :param node_ident: UUID or logical name of a node. :param target: The desired provision state of the node. :param configdrive: Optional. A gzipped and base64 encoded configdrive. Only valid when setting provision state to "active". :raises: NodeLocked (HTTP 409) if the node is currently locked. :raises: ClientSideError (HTTP 409) if the node is already being provisioned. :raises: InvalidStateRequested (HTTP 400) if the requested transition is not possible from the current state. :raises: NotAcceptable (HTTP 406) if the API version specified does not allow the requested state transition. """ check_allow_management_verbs(target) rpc_node = api_utils.get_rpc_node(node_ident) topic = pecan.request.rpcapi.get_topic_for(rpc_node) # Normally, we let the task manager recognize and deal with # NodeLocked exceptions. However, that isn't done until the RPC calls # below. In order to main backward compatibility with our API HTTP # response codes, we have this check here to deal with cases where # a node is already being operated on (DEPLOYING or such) and we # want to continue returning 409. Without it, we'd return 400. if rpc_node.reservation: raise exception.NodeLocked(node=rpc_node.uuid, host=rpc_node.reservation) if (target in (ir_states.ACTIVE, ir_states.REBUILD) and rpc_node.maintenance): raise exception.NodeInMaintenance(op=_('provisioning'), node=rpc_node.uuid) m = ir_states.machine.copy() m.initialize(rpc_node.provision_state) if not m.is_valid_event(ir_states.VERBS.get(target, target)): raise exception.InvalidStateRequested( action=target, node=rpc_node.uuid, state=rpc_node.provision_state) if configdrive and target != ir_states.ACTIVE: msg = (_('Adding a config drive is only supported when setting ' 'provision state to %s') % ir_states.ACTIVE) raise wsme.exc.ClientSideError(msg, status_code=400) # Note that there is a race condition. The node state(s) could change # by the time the RPC call is made and the TaskManager manager gets a # lock. if target == ir_states.ACTIVE: pecan.request.rpcapi.do_node_deploy(pecan.request.context, rpc_node.uuid, False, configdrive, topic) elif target == ir_states.REBUILD: pecan.request.rpcapi.do_node_deploy(pecan.request.context, rpc_node.uuid, True, None, topic) elif target == ir_states.DELETED: pecan.request.rpcapi.do_node_tear_down( pecan.request.context, rpc_node.uuid, topic) elif target == ir_states.VERBS['inspect']: pecan.request.rpcapi.inspect_hardware( pecan.request.context, rpc_node.uuid, topic=topic) elif target in ( ir_states.VERBS['manage'], ir_states.VERBS['provide']): pecan.request.rpcapi.do_provisioning_action( pecan.request.context, rpc_node.uuid, target, topic) else: msg = (_('The requested action "%(action)s" could not be ' 'understood.') % {'action': target}) raise exception.InvalidStateRequested(message=msg) # Set the HTTP Location Header url_args = '/'.join([node_ident, 'states']) pecan.response.location = link.build_url('nodes', url_args)