Skip to content

Commit ae0af56

Browse files
[PLAT-13282] Support manually decommissioning a node from the on-prem provider
Summary: Allow user to manually decommission a node from `FREE` to `DECOMMISSIONED` state. This allows user to skip picking up the nodes when a new universe is created. A node placed this way will skip the node cleanup when user tries to recommission a node. Endpoint call is as follows: ``` curl --location --request PUT '<YBA_base_url>:9000/api/v1/customers/<customer_uuid>/providers/<provider_uuid>/instances/<node_instance_ip>/state' \ --header 'X-AUTH-YW-API-TOKEN: <api-token>' \ --header 'Content-Type: application/json' \ --data '{ "state": "DECOMMISSIONED" }' ``` Also, add improvement to prevent race condition between task submitting and finishing before customer task object is saved to YBA db. Test Plan: Added UTs for all below scenarios: 1. User has a node in FREE state and calls the endpoint to set the node instance to `DECOMMISSIONED` state. Validate that the `manually_decommissioned` bit is set as true. If user calls the same endpoint but with state as `FREE` state, we should see the node instance move to `FREE` state with the `manually_decommissioned` bit in YBA db set to false. Also, validate that no clean up is done. 2. Validate original behavior of node being placed in decommissioned state if node cleanup fails and that the `manually_decommissioned` bit is set to false. Also, if we call the endpoint to set the node to `FREE` state, it will run the clean up. Reviewers: nsingh, sanketh, hzare, rmadhavan Reviewed By: nsingh Subscribers: sanketh, yugaware Differential Revision: https://phorge.dev.yugabyte.com/D39747
1 parent e8c9e7d commit ae0af56

File tree

14 files changed

+356
-72
lines changed

14 files changed

+356
-72
lines changed

managed/src/main/java/com/yugabyte/yw/commissioner/Commissioner.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.yugabyte.yw.models.TaskInfo;
3030
import com.yugabyte.yw.models.Universe;
3131
import com.yugabyte.yw.models.helpers.TaskType;
32+
import io.ebean.annotation.Transactional;
3233
import java.util.Collections;
3334
import java.util.HashMap;
3435
import java.util.HashSet;
@@ -44,6 +45,7 @@
4445
import java.util.concurrent.ThreadFactory;
4546
import java.util.function.Consumer;
4647
import java.util.function.Predicate;
48+
import javax.annotation.Nullable;
4749
import lombok.extern.slf4j.Slf4j;
4850
import org.slf4j.MDC;
4951
import play.inject.ApplicationLifecycle;
@@ -124,6 +126,22 @@ public UUID submit(TaskType taskType, ITaskParams taskParams) {
124126
* @param taskUUID the task UUID
125127
*/
126128
public UUID submit(TaskType taskType, ITaskParams taskParams, UUID taskUUID) {
129+
return submit(taskType, taskParams, taskUUID, null);
130+
}
131+
132+
/**
133+
* Creates a new task runnable to run the required task, and submits it to the TaskExecutor.
134+
*
135+
* @param taskType the task type.
136+
* @param taskParams the task parameters.
137+
* @param taskUUID the task UUID
138+
* @param preTaskSubmitWork function to run before task submission
139+
*/
140+
public UUID submit(
141+
TaskType taskType,
142+
ITaskParams taskParams,
143+
UUID taskUUID,
144+
@Nullable Consumer<RunnableTask> preTaskSubmitWork) {
127145
RunnableTask taskRunnable = null;
128146
try {
129147
if (runtimeConfGetter.getGlobalConf(
@@ -135,7 +153,7 @@ public UUID submit(TaskType taskType, ITaskParams taskParams, UUID taskUUID) {
135153
"Executing TaskType {} with params {}", taskType.toString(), redactedJson.toString());
136154
}
137155
// Create the task runnable object based on the various parameters passed in.
138-
taskRunnable = taskExecutor.createRunnableTask(taskType, taskParams, taskUUID);
156+
taskRunnable = createRunnableTask(taskType, taskParams, taskUUID, preTaskSubmitWork);
139157
// Add the consumer to handle before task if available.
140158
taskRunnable.setTaskExecutionListener(getTaskExecutionListener());
141159
onTaskCreated(taskRunnable, taskParams);
@@ -147,7 +165,7 @@ public UUID submit(TaskType taskType, ITaskParams taskParams, UUID taskUUID) {
147165
TaskInfo taskInfo = taskRunnable.getTaskInfo();
148166
if (taskInfo.getTaskState() != TaskInfo.State.Failure) {
149167
taskInfo.setTaskState(TaskInfo.State.Failure);
150-
taskInfo.save();
168+
taskInfo.update();
151169
}
152170
}
153171
String msg = "Error processing " + taskType + " task for " + taskParams.toString();
@@ -159,6 +177,20 @@ public UUID submit(TaskType taskType, ITaskParams taskParams, UUID taskUUID) {
159177
}
160178
}
161179

180+
@Transactional
181+
private RunnableTask createRunnableTask(
182+
TaskType taskType,
183+
ITaskParams taskParams,
184+
UUID taskUUID,
185+
@Nullable Consumer<RunnableTask> preTaskSubmitWork) {
186+
// Create the task runnable object based on the various parameters passed in.
187+
RunnableTask taskRunnable = taskExecutor.createRunnableTask(taskType, taskParams, taskUUID);
188+
if (preTaskSubmitWork != null) {
189+
preTaskSubmitWork.accept(taskRunnable);
190+
}
191+
return taskRunnable;
192+
}
193+
162194
private void onTaskCreated(RunnableTask taskRunnable, ITaskParams taskParams) {
163195
providerEditRestrictionManager.onTaskCreated(
164196
taskRunnable.getTaskUUID(), taskRunnable.getTask(), taskParams);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) YugaByte, Inc.
2+
3+
package com.yugabyte.yw.commissioner.tasks;
4+
5+
import com.yugabyte.yw.commissioner.AbstractTaskBase;
6+
import com.yugabyte.yw.commissioner.BaseTaskDependencies;
7+
import com.yugabyte.yw.commissioner.tasks.params.DetachedNodeTaskParams;
8+
import com.yugabyte.yw.models.NodeInstance;
9+
import com.yugabyte.yw.models.NodeInstance.State;
10+
import javax.inject.Inject;
11+
import lombok.extern.slf4j.Slf4j;
12+
13+
@Slf4j
14+
public class DecommissionNodeInstance extends AbstractTaskBase {
15+
16+
@Inject
17+
protected DecommissionNodeInstance(BaseTaskDependencies baseTaskDependencies) {
18+
super(baseTaskDependencies);
19+
}
20+
21+
@Override
22+
protected DetachedNodeTaskParams taskParams() {
23+
return (DetachedNodeTaskParams) taskParams;
24+
}
25+
26+
@Override
27+
public void run() {
28+
NodeInstance nodeInstance = NodeInstance.getOrBadRequest(taskParams().getNodeUuid());
29+
30+
if (nodeInstance.getState() != NodeInstance.State.FREE) {
31+
throw new RuntimeException(
32+
String.format(
33+
"Node instance %s in %s state cannot be manually decommissioned. Node instance must"
34+
+ " be in %s state to be recommissioned.",
35+
nodeInstance.getNodeUuid(), nodeInstance.getState(), NodeInstance.State.FREE));
36+
}
37+
38+
nodeInstance.setState(State.DECOMMISSIONED);
39+
nodeInstance.setManuallyDecommissioned(true);
40+
nodeInstance.update();
41+
log.debug(
42+
"Successfully set node instance {} to {} state",
43+
nodeInstance.getNodeUuid(),
44+
State.DECOMMISSIONED);
45+
}
46+
}

managed/src/main/java/com/yugabyte/yw/commissioner/tasks/RecommissionNodeInstance.java

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,34 @@ protected DetachedNodeTaskParams taskParams() {
2828
public void run() {
2929
NodeInstance nodeInstance = NodeInstance.getOrBadRequest(taskParams().getNodeUuid());
3030

31-
try {
32-
ShellResponse response =
33-
nodeManager
34-
.detachedNodeCommand(NodeManager.NodeCommandType.Destroy, taskParams())
35-
.processErrors();
36-
} catch (Exception e) {
37-
log.error("Clean up failed for node instance: {}", nodeInstance.getNodeUuid(), e);
38-
throw e;
31+
if (nodeInstance.getState() != NodeInstance.State.DECOMMISSIONED) {
32+
throw new RuntimeException(
33+
String.format(
34+
"Node instance %s in %s state cannot be recommissioned. Node instance must be in %s"
35+
+ " state to be recommissioned.",
36+
nodeInstance.getNodeUuid(),
37+
nodeInstance.getState(),
38+
NodeInstance.State.DECOMMISSIONED));
39+
}
40+
41+
if (!nodeInstance.isManuallyDecommissioned()) {
42+
log.debug("Cleaning up node instance {}", nodeInstance.getNodeUuid());
43+
try {
44+
ShellResponse response =
45+
nodeManager
46+
.detachedNodeCommand(NodeManager.NodeCommandType.Destroy, taskParams())
47+
.processErrors();
48+
} catch (Exception e) {
49+
log.error("Clean up failed for node instance: {}", nodeInstance.getNodeUuid(), e);
50+
throw e;
51+
}
52+
log.debug("Successfully cleaned up node instance: {}", nodeInstance.getNodeUuid());
53+
} else {
54+
log.debug(
55+
"Skipping clean up node instance {} as node instance was manually decommissioned by user",
56+
nodeInstance.getNodeUuid());
3957
}
4058

41-
log.debug("Successfully cleaned up node instance: {}", nodeInstance.getNodeUuid());
4259
nodeInstance.clearNodeDetails();
4360
}
4461
}

managed/src/main/java/com/yugabyte/yw/controllers/NodeInstanceController.java

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -592,15 +592,7 @@ public Result updateState(
592592
throw new PlatformServiceException(
593593
CONFLICT, "Node " + node.getNodeUuid() + " has incomplete tasks");
594594
}
595-
UUID taskUUID = nodeInstanceHandler.updateState(payload, node, provider);
596-
597-
CustomerTask.create(
598-
c,
599-
node.getNodeUuid(),
600-
taskUUID,
601-
CustomerTask.TargetType.Node,
602-
CustomerTask.TaskType.Update,
603-
node.getNodeName());
595+
UUID taskUUID = nodeInstanceHandler.updateState(payload, node, provider, c);
604596

605597
auditService()
606598
.createAuditEntryWithReqBody(

managed/src/main/java/com/yugabyte/yw/controllers/handlers/NodeInstanceHandler.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
import static play.mvc.Http.Status.BAD_REQUEST;
66

77
import com.yugabyte.yw.commissioner.Commissioner;
8+
import com.yugabyte.yw.commissioner.TaskExecutor.RunnableTask;
89
import com.yugabyte.yw.commissioner.tasks.params.DetachedNodeTaskParams;
910
import com.yugabyte.yw.common.PlatformServiceException;
1011
import com.yugabyte.yw.forms.NodeInstanceStateFormData;
12+
import com.yugabyte.yw.models.Customer;
13+
import com.yugabyte.yw.models.CustomerTask;
1114
import com.yugabyte.yw.models.NodeInstance;
1215
import com.yugabyte.yw.models.Provider;
1316
import com.yugabyte.yw.models.helpers.TaskType;
1417
import java.util.UUID;
18+
import java.util.function.Consumer;
1519
import javax.inject.Inject;
1620
import javax.inject.Singleton;
1721

@@ -26,8 +30,18 @@ public NodeInstanceHandler(Commissioner commissioner) {
2630
}
2731

2832
public UUID updateState(
29-
NodeInstanceStateFormData payload, NodeInstance nodeInstance, Provider provider) {
33+
NodeInstanceStateFormData payload, NodeInstance nodeInstance, Provider provider, Customer c) {
3034
NodeInstance.State nodeState = nodeInstance.getState();
35+
Consumer<RunnableTask> customerTaskCreation =
36+
runnableTask -> {
37+
CustomerTask.create(
38+
c,
39+
nodeInstance.getNodeUuid(),
40+
runnableTask.getTaskUUID(),
41+
CustomerTask.TargetType.Node,
42+
CustomerTask.TaskType.Update,
43+
nodeInstance.getNodeName());
44+
};
3145

3246
// Decommissioned -> Free.
3347
if (nodeState == NodeInstance.State.DECOMMISSIONED
@@ -36,7 +50,16 @@ public UUID updateState(
3650
taskParams.setNodeUuid(nodeInstance.getNodeUuid());
3751
taskParams.setInstanceType(nodeInstance.getInstanceTypeCode());
3852
taskParams.setAzUuid(nodeInstance.getZoneUuid());
39-
return commissioner.submit(TaskType.RecommissionNodeInstance, taskParams);
53+
return commissioner.submit(
54+
TaskType.RecommissionNodeInstance, taskParams, null, customerTaskCreation);
55+
} else if (nodeState == NodeInstance.State.FREE
56+
&& payload.state == NodeInstance.State.DECOMMISSIONED) {
57+
DetachedNodeTaskParams taskParams = new DetachedNodeTaskParams();
58+
taskParams.setNodeUuid(nodeInstance.getNodeUuid());
59+
taskParams.setInstanceType(nodeInstance.getInstanceTypeCode());
60+
taskParams.setAzUuid(nodeInstance.getZoneUuid());
61+
return commissioner.submit(
62+
TaskType.DecommissionNodeInstance, taskParams, null, customerTaskCreation);
4063
}
4164

4265
throw new PlatformServiceException(

managed/src/main/java/com/yugabyte/yw/models/NodeInstance.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ public enum State {
136136
@Enumerated(EnumType.STRING)
137137
private State state;
138138

139+
@Column(nullable = false)
140+
@ApiModelProperty(value = "Manually set to decommissioned state by user", accessMode = READ_ONLY)
141+
private boolean manuallyDecommissioned;
142+
139143
@DbJson @JsonIgnore private UniverseMetadata universeMetadata;
140144

141145
@Getter(AccessLevel.NONE)
@@ -181,6 +185,7 @@ public void clearNodeDetails() {
181185
this.setState(State.FREE);
182186
this.setNodeName("");
183187
this.universeMetadata = null;
188+
this.setManuallyDecommissioned(false);
184189
this.save();
185190
}
186191

managed/src/main/java/com/yugabyte/yw/models/helpers/TaskType.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,11 @@ public enum TaskType {
586586
CustomerTask.TaskType.Update,
587587
CustomerTask.TargetType.Node),
588588

589+
DecommissionNodeInstance(
590+
com.yugabyte.yw.commissioner.tasks.DecommissionNodeInstance.class,
591+
CustomerTask.TaskType.Update,
592+
CustomerTask.TargetType.Node),
593+
589594
MasterFailover(
590595
com.yugabyte.yw.commissioner.tasks.MasterFailover.class,
591596
CustomerTask.TaskType.MasterFailover,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- Copyright (c) YugaByte, Inc.
2+
3+
ALTER TABLE IF EXISTS node_instance ADD COLUMN if NOT EXISTS manually_decommissioned boolean DEFAULT false NOT NULL;

0 commit comments

Comments
 (0)