Introduce action status scoped custom code (#1277)

* Allow providing a custom code with an action status feedback to give more fine grained device specific details.
* Add ddi rest docs for new optional status code value.
* Provide new code value via mgmt api. Fix review findings.
* Fix failing tests

Signed-off-by: Michael Herdt <Michael.Herdt@bosch.io>
Co-authored-by: Stefan Behl <stefan.behl@bosch.io>
This commit is contained in:
Michael Herdt
2022-09-21 15:20:34 +02:00
committed by GitHub
parent 32718676a4
commit 5e963f8308
28 changed files with 338 additions and 43 deletions

View File

@@ -355,6 +355,11 @@ public class AmqpMessageHandlerService extends BaseAmqpService {
final ActionStatusCreate actionStatus = entityFactory.actionStatus().create(action.getId()).status(status)
.messages(messages);
actionUpdateStatus.getCode().ifPresent(code -> {
actionStatus.code(code);
actionStatus.message("Device reported status code: " + code);
});
final Action updatedAction = (Status.CANCELED == status)
? controllerManagement.addCancelActionStatus(actionStatus)
: controllerManagement.addUpdateActionStatus(actionStatus);

View File

@@ -46,6 +46,8 @@ import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.UpdateMode;
import org.eclipse.hawkbit.repository.builder.ActionStatusBuilder;
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
import org.eclipse.hawkbit.repository.jpa.builder.JpaActionStatusBuilder;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.Status;
@@ -543,7 +545,7 @@ public class AmqpMessageHandlerServiceTest {
}
@Test
@Description("Tests TODO")
@Description("Test next update is provided on finished action")
public void lookupNextUpdateActionAfterFinished() throws IllegalAccessException {
// Mock
@@ -570,7 +572,7 @@ public class AmqpMessageHandlerServiceTest {
final ArgumentCaptor<Target> targetCaptor = ArgumentCaptor.forClass(Target.class);
verify(amqpMessageDispatcherServiceMock, times(1)).sendUpdateMessageToTarget(actionPropertiesCaptor.capture(),
targetCaptor.capture(), any(Map.class));
targetCaptor.capture(), any(Map.class));
final ActionProperties actionProperties = actionPropertiesCaptor.getValue();
assertThat(actionProperties).isNotNull();
assertThat(actionProperties.getTenant()).as("event has tenant").isEqualTo("DEFAULT");
@@ -578,6 +580,42 @@ public class AmqpMessageHandlerServiceTest {
assertThat(actionProperties.getId()).as("event has wrong action id").isEqualTo(22L);
}
@Test
@Description("Test feedback code is persisted in messages when provided with DmfActionUpdateStatus")
public void feedBackCodeIsPersistedInMessages() throws IllegalAccessException {
// Mock
final Action action = createActionWithTarget(22L, Status.FINISHED);
when(controllerManagementMock.findActionWithDetails(anyLong())).thenReturn(Optional.of(action));
when(controllerManagementMock.addUpdateActionStatus(any())).thenReturn(action);
final ActionStatusBuilder builder = new JpaActionStatusBuilder();
when(entityFactoryMock.actionStatus()).thenReturn(builder);
// for the test the same action can be used
when(controllerManagementMock.findActiveActionWithHighestWeight(any())).thenReturn(Optional.of(action));
final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT);
messageProperties.setHeader(MessageHeaderKey.TOPIC, EventTopic.UPDATE_ACTION_STATUS.name());
final DmfActionUpdateStatus actionUpdateStatus = new DmfActionUpdateStatus(23L, DmfActionStatus.RUNNING);
actionUpdateStatus.setSoftwareModuleId(Long.valueOf(2));
actionUpdateStatus.setCode(12);
final Message message = createMessage(actionUpdateStatus, messageProperties);
// test
amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, VIRTUAL_HOST);
final ArgumentCaptor<ActionStatusCreate> actionPropertiesCaptor = ArgumentCaptor
.forClass(ActionStatusCreate.class);
verify(controllerManagementMock, times(1)).addUpdateActionStatus(actionPropertiesCaptor.capture());
final JpaActionStatus jpaActionStatus = (JpaActionStatus) actionPropertiesCaptor.getValue().build();
assertThat(jpaActionStatus.getCode()).as("Action status for reported code is missing").contains(12);
assertThat(jpaActionStatus.getMessages()).as("Action status message for reported code is missing")
.contains("Device reported status code: 12");
}
private DmfActionUpdateStatus createActionUpdateStatus(final DmfActionStatus status) {
return createActionUpdateStatus(status, 2L);
}

View File

@@ -105,6 +105,7 @@ public class BaseAmqpServiceTest {
private DmfActionUpdateStatus createActionStatus() {
final DmfActionUpdateStatus actionUpdateStatus = new DmfActionUpdateStatus(1L, DmfActionStatus.RUNNING);
actionUpdateStatus.setCode(2);
actionUpdateStatus.setSoftwareModuleId(2L);
actionUpdateStatus.addMessage("Message 1");
actionUpdateStatus.addMessage("Message 2");

View File

@@ -12,7 +12,9 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
@@ -31,6 +33,9 @@ public class DmfActionUpdateStatus {
@JsonProperty
private Long softwareModuleId;
@JsonProperty
private Integer code;
@JsonProperty
private List<String> message;
@@ -56,6 +61,15 @@ public class DmfActionUpdateStatus {
return actionStatus;
}
@JsonIgnore
public Optional<Integer> getCode() {
return Optional.ofNullable(code);
}
public void setCode(final Integer code) {
this.code = code;
}
public List<String> getMessage() {
if (message == null) {
return Collections.emptyList();

View File

@@ -37,6 +37,8 @@ public interface ActionStatusCreate {
*/
ActionStatusCreate occurredAt(long occurredAt);
ActionStatusCreate code(int code);
/**
* @param messages
* for {@link ActionStatus#getMessages()}

View File

@@ -8,6 +8,7 @@
*/
package org.eclipse.hawkbit.repository.model;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.eclipse.hawkbit.repository.model.Action.Status;
@@ -39,4 +40,6 @@ public interface ActionStatus extends TenantAwareBaseEntity {
*/
Status getStatus();
Optional<Integer> getCode();
}

View File

@@ -25,9 +25,13 @@ import org.springframework.util.StringUtils;
* update or create builder interface
*/
public abstract class AbstractActionStatusCreate<T> {
protected Status status;
protected Long occurredAt;
protected Integer code;
protected List<@ValidString String> messages;
protected Long actionId;
@@ -48,6 +52,12 @@ public abstract class AbstractActionStatusCreate<T> {
return (T) this;
}
public T code(final int code) {
this.code = code;
return (T) this;
}
public T messages(final Collection<String> messages) {
if (this.messages == null) {
this.messages = messages.stream().map(StringUtils::trimWhitespace).collect(Collectors.toList());

View File

@@ -29,6 +29,9 @@ public class JpaActionStatusCreate extends AbstractActionStatusCreate<ActionStat
if (messages != null) {
messages.forEach(result::addMessage);
}
if (code != null) {
result.setCode(code);
}
return result;
}
}

View File

@@ -11,6 +11,7 @@ package org.eclipse.hawkbit.repository.jpa.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
@@ -85,6 +86,9 @@ public class JpaActionStatus extends AbstractJpaTenantAwareBaseEntity implements
@Column(name = "detail_message", length = MESSAGE_ENTRY_LENGTH, nullable = false, updatable = false)
private List<String> messages;
@Column(name = "code", nullable = true, updatable = false)
private Integer code;
/**
* Creates a new {@link ActionStatus} object.
*
@@ -184,4 +188,11 @@ public class JpaActionStatus extends AbstractJpaTenantAwareBaseEntity implements
this.status = status;
}
public Optional<Integer> getCode() {
return Optional.ofNullable(code);
}
public void setCode(final Integer code) {
this.code = code;
}
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_action_status ADD COLUMN code INTEGER;
CREATE INDEX sp_idx_action_status_03 ON sp_action_status (tenant, code);

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_action_status ADD column code integer;
CREATE INDEX sp_idx_action_status_03 ON sp_action_status (tenant, code);

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_action_status ADD COLUMN code integer;
CREATE INDEX sp_idx_action_status_03 ON sp_action_status (tenant, code);

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_action_status ADD code INT;
CREATE INDEX sp_idx_action_status_03 ON sp_action_status (tenant, code);

View File

@@ -47,7 +47,6 @@ import org.eclipse.hawkbit.repository.exception.IncompatibleTargetTypeException;
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException;
import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;

View File

@@ -10,6 +10,8 @@ package org.eclipse.hawkbit.ddi.json.model;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@@ -33,6 +35,8 @@ public class DdiStatus {
@Valid
private final DdiResult result;
private final Integer code;
private final List<String> details;
/**
@@ -42,14 +46,18 @@ public class DdiStatus {
* status
* @param result
* information
* @param code
* as optional code (can be null)
* @param details
* as optional addition
*/
@JsonCreator
public DdiStatus(@JsonProperty("execution") final ExecutionStatus execution,
@JsonProperty("result") final DdiResult result, @JsonProperty("details") final List<String> details) {
@JsonProperty("result") final DdiResult result, @JsonProperty("code") final Integer code,
@JsonProperty("details") final List<String> details) {
this.execution = execution;
this.result = result;
this.code = code;
this.details = details;
}
@@ -69,6 +77,10 @@ public class DdiStatus {
return Collections.unmodifiableList(details);
}
public Integer getCode() {
return code;
}
/**
* The element status contains information about the execution of the
* operation.
@@ -129,7 +141,8 @@ public class DdiStatus {
@Override
public String toString() {
return "Status [execution=" + execution + ", result=" + result + ", details=" + details + "]";
return "Status [execution=" + execution + ", result=" + result + ", code="
+ code + ", details=" + details + "]";
}
}

View File

@@ -39,7 +39,7 @@ public class DdiActionFeedbackTest {
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
final String time = Instant.now().toString();
final DdiStatus ddiStatus = new DdiStatus(DdiStatus.ExecutionStatus.CLOSED, null, Lists.emptyList());
final DdiStatus ddiStatus = new DdiStatus(DdiStatus.ExecutionStatus.CLOSED, null, null, Lists.emptyList());
final DdiActionFeedback ddiActionFeedback = new DdiActionFeedback(time, ddiStatus);
// Test

View File

@@ -16,6 +16,7 @@ import static org.eclipse.hawkbit.ddi.json.model.DdiStatus.ExecutionStatus.PROCE
import java.io.IOException;
import java.util.Collections;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
@@ -25,6 +26,9 @@ import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
/**
* Test serializability of DDI api model 'DdiStatus'
@@ -35,14 +39,10 @@ public class DdiStatusTest {
private ObjectMapper mapper = new ObjectMapper();
@Test
@ParameterizedTest
@MethodSource("ddiStatusPossibilities")
@Description("Verify the correct serialization and deserialization of the model")
public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup
DdiProgress ddiProgress = new DdiProgress(30, 100);
DdiResult ddiResult = new DdiResult(NONE, ddiProgress);
DdiStatus ddiStatus = new DdiStatus(PROCEEDING, ddiResult, Collections.emptyList());
public void shouldSerializeAndDeserializeObject(final DdiResult ddiResult, final DdiStatus ddiStatus) throws IOException {
// Test
String serializedDdiStatus = mapper.writeValueAsString(ddiStatus);
DdiStatus deserializedDdiStatus = mapper.readValue(serializedDdiStatus, DdiStatus.class);
@@ -55,19 +55,47 @@ public class DdiStatusTest {
ddiStatus.getResult().getProgress().getCnt());
assertThat(deserializedDdiStatus.getResult().getProgress().getOf()).isEqualTo(
ddiStatus.getResult().getProgress().getOf());
assertThat(deserializedDdiStatus.getDetails()).isEqualTo(ddiStatus.getDetails());
}
private static Stream<Arguments> ddiStatusPossibilities(){
final DdiProgress ddiProgress = new DdiProgress(30, 100);
final DdiResult ddiResult = new DdiResult(NONE, ddiProgress);
return Stream.of(
Arguments.of(ddiResult, new DdiStatus(PROCEEDING, ddiResult, null, Collections.emptyList())),
Arguments.of(ddiResult, new DdiStatus(PROCEEDING, ddiResult, null, Collections.singletonList("testMessage"))),
Arguments.of(ddiResult, new DdiStatus(PROCEEDING, ddiResult, 12, Collections.emptyList())));
}
@Test
@Description("Verify the correct deserialization of a model with a additional unknown property")
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
// Setup
final String serializedDdiStatus = "{\"execution\":\"proceeding\",\"result\":{\"finished\":\"none\","
+ "\"progress\":{\"cnt\":30,\"of\":100}},\"details\":[],\"unknownProperty\":\"test\"}";
// Test
final DdiStatus ddiStatus = mapper.readValue(serializedDdiStatus, DdiStatus.class);
assertThat(ddiStatus.getExecution()).isEqualTo(PROCEEDING);
assertThat(ddiStatus.getCode()).isNull();
assertThat(ddiStatus.getResult().getFinished()).isEqualTo(NONE);
assertThat(ddiStatus.getResult().getProgress().getCnt()).isEqualTo(30);
assertThat(ddiStatus.getResult().getProgress().getOf()).isEqualTo(100);
}
@Test
@Description("Verify the correct deserialization of a model with a provided code (optional)")
public void shouldDeserializeObjectWithOptionalCode() throws IOException {
// Setup
String serializedDdiStatus = "{\"execution\":\"proceeding\",\"result\":{\"finished\":\"none\","
+ "\"progress\":{\"cnt\":30,\"of\":100}},\"details\":[],\"unknownProperty\":\"test\"}";
+ "\"progress\":{\"cnt\":30,\"of\":100}},\"code\": 12,\"details\":[]}";
// Test
DdiStatus ddiStatus = mapper.readValue(serializedDdiStatus, DdiStatus.class);
assertThat(ddiStatus.getExecution()).isEqualTo(PROCEEDING);
assertThat(ddiStatus.getCode()).isEqualTo(12);
assertThat(ddiStatus.getResult().getFinished()).isEqualTo(NONE);
assertThat(ddiStatus.getResult().getProgress().getCnt()).isEqualTo(30);
assertThat(ddiStatus.getResult().getProgress().getOf()).isEqualTo(100);

View File

@@ -324,7 +324,6 @@ public class DdiRootController implements DdiRootControllerRestApi {
final Target target = findTarget(controllerId);
final Action action = findActionForTarget(actionId, target);
if (!action.isActive()) {
LOG.warn("Updating action {} with feedback {} not possible since action not active anymore.",
action.getId(), feedback.getStatus());
@@ -340,12 +339,20 @@ public class DdiRootController implements DdiRootControllerRestApi {
private ActionStatusCreate generateUpdateStatus(final DdiActionFeedback feedback, final String controllerId,
final Long actionId) {
final ActionStatusCreate actionStatusCreate = entityFactory.actionStatus().create(actionId);
final List<String> messages = new ArrayList<>();
if (!CollectionUtils.isEmpty(feedback.getStatus().getDetails())) {
messages.addAll(feedback.getStatus().getDetails());
}
final Integer code = feedback.getStatus().getCode();
if (code != null) {
actionStatusCreate.code(code);
messages.add("Device reported status code: " + code);
}
final Status status;
switch (feedback.getStatus().getExecution()) {
case CANCELED:
@@ -380,7 +387,7 @@ public class DdiRootController implements DdiRootControllerRestApi {
break;
}
return entityFactory.actionStatus().create(actionId).status(status).messages(messages);
return actionStatusCreate.status(status).messages(messages);
}
private static void addMessageIfEmpty(final String text, final List<String> messages) {
@@ -431,7 +438,6 @@ public class DdiRootController implements DdiRootControllerRestApi {
final Target target = findTarget(controllerId);
final Action action = findActionForTarget(actionId, target);
if (action.isCancelingOrCanceled()) {
final DdiCancel cancel = new DdiCancel(String.valueOf(action.getId()),
new DdiCancelActionToStop(String.valueOf(action.getId())));
@@ -507,6 +513,8 @@ public class DdiRootController implements DdiRootControllerRestApi {
private static ActionStatusCreate generateActionCancelStatus(final DdiActionFeedback feedback, final Target target,
final Long actionId, final EntityFactory entityFactory) {
final ActionStatusCreate actionStatusCreate = entityFactory.actionStatus().create(actionId);
final List<String> messages = new ArrayList<>();
final Status status;
switch (feedback.getStatus().getExecution()) {
@@ -531,7 +539,13 @@ public class DdiRootController implements DdiRootControllerRestApi {
messages.addAll(feedback.getStatus().getDetails());
}
return entityFactory.actionStatus().create(actionId).status(status).messages(messages);
final Integer code = feedback.getStatus().getCode();
if (code != null) {
actionStatusCreate.code(code);
messages.add("Device reported status code: " + code);
}
return actionStatusCreate.status(status).messages(messages);
}

View File

@@ -323,13 +323,18 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat
protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus, final DdiResult ddiResult,
final List<String> messages) throws JsonProcessingException {
final DdiStatus ddiStatus = new DdiStatus(executionStatus, ddiResult, messages);
final DdiStatus ddiStatus = new DdiStatus(executionStatus, ddiResult, null, messages);
return objectMapper.writeValueAsString(new DdiActionFeedback(Instant.now().toString(), ddiStatus));
}
protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus,
final DdiResult.FinalResult finalResult, final List<String> messages) throws JsonProcessingException {
final DdiStatus ddiStatus = new DdiStatus(executionStatus, new DdiResult(finalResult, new DdiProgress(2, 5)),
return getJsonActionFeedback(executionStatus, finalResult, null, messages);
}
protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus,
final DdiResult.FinalResult finalResult, final Integer code, final List<String> messages) throws JsonProcessingException {
final DdiStatus ddiStatus = new DdiStatus(executionStatus, new DdiResult(finalResult, new DdiProgress(2, 5)), code,
messages);
return objectMapper.writeValueAsString(new DdiActionFeedback(Instant.now().toString(), ddiStatus));
}

View File

@@ -492,7 +492,7 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest {
}
final String feedback = getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING, DdiResult.FinalResult.NONE,
messages);
null, messages);
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isForbidden());
}

View File

@@ -33,7 +33,10 @@ public class MgmtActionStatus {
private List<String> messages;
@JsonProperty
private Long reportedAt;
private Long reportedAt;
@JsonProperty
private Integer code;
/**
* @return the statusId
@@ -95,4 +98,18 @@ public class MgmtActionStatus {
this.reportedAt = reportedAt;
}
/**
* @return the code
*/
public Integer getCode() {
return code;
}
/**
* @param code
* the reported code to set
*/
public void setCode(final Integer code) {
this.code = code;
}
}

View File

@@ -299,6 +299,7 @@ public final class MgmtTargetMapper {
result.setReportedAt(actionStatus.getCreatedAt());
result.setStatusId(actionStatus.getId());
result.setType(actionStatus.getStatus().name().toLowerCase());
actionStatus.getCode().ifPresent(result::setCode);
return result;
}

View File

@@ -8,11 +8,13 @@
*/
package org.eclipse.hawkbit.rest.util;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
@@ -183,6 +185,101 @@ public abstract class JsonBuilder {
return builder.toString();
}
/**
* builds a json string for the feedback for the execution "proceeding".
*
* @param id
* of the Action feedback refers to
* @return the built string
* @throws JSONException
*/
public static String deploymentActionInProgressFeedback(final String id) throws JSONException {
return deploymentActionFeedback(id, "proceeding");
}
/**
* builds a certain json string for a action feedback.
*
* @param id
* of the action the feedback refers to
* @param execution
* see ExecutionStatus
* @return the build json string
* @throws JSONException
*/
public static String deploymentActionFeedback(final String id, final String execution) throws JSONException {
return deploymentActionFeedback(id, execution, "none", null,
Arrays.asList(RandomStringUtils.randomAlphanumeric(1000)));
}
public static String deploymentActionFeedback(final String id, final String execution, final String message)
throws JSONException {
return deploymentActionFeedback(id, execution, "none", null, Arrays.asList(message));
}
public static String deploymentActionFeedback(final String id, final String execution, final String finished,
final String message) throws JSONException {
return deploymentActionFeedback(id, execution, finished, null, message);
}
public static String deploymentActionFeedback(final String id, final String execution, final String finished,
final Integer code, final String message) throws JSONException {
return deploymentActionFeedback(id, execution, finished, code, Arrays.asList(message));
}
public static String deploymentActionFeedback(final String id, final String execution, final String finished,
final Integer code, final Collection<String> messages) throws JSONException {
final JSONObject statusJson = new JSONObject().put("execution", execution).put("result",
new JSONObject().put("finished", finished).put("progress", new JSONObject().put("cnt", 2).put("of", 5)))
.put("details", new JSONArray(messages));
if (code != null) {
statusJson.put("code", code);
}
return new JSONObject().put("id", id).put("status", statusJson).toString();
}
/**
* Build an invalid request body with missing result for feedback message.
*
* @param id
* id of the action
* @param execution
* the execution
* @param message
* the message
* @return a invalid request body
* @throws JSONException
*/
public static String missingResultInFeedback(final String id, final String execution, final String message)
throws JSONException {
return new JSONObject().put("id", id).put("time", "20140511T121314")
.put("status",
new JSONObject().put("execution", execution).put("details", new JSONArray().put(message)))
.toString();
}
/**
* Build an invalid request body with missing finished result for feedback
* message.
*
* @param id
* id of the action
* @param execution
* the execution
* @param message
* the message
* @return a invalid request body
* @throws JSONException
*/
public static String missingFinishedResultInFeedback(final String id, final String execution, final String message)
throws JSONException {
return new JSONObject().put("id", id).put("time", "20140511T121314")
.put("status", new JSONObject().put("execution", execution).put("result", new JSONObject())
.put("details", new JSONArray().put(message)))
.toString();
}
public static String distributionSetTypes(final List<DistributionSetType> types) throws JSONException {
final JSONArray result = new JSONArray();
@@ -545,15 +642,27 @@ public abstract class JsonBuilder {
return json.toString();
}
public static String cancelActionFeedback(final String id, final String execution) throws JSONException {
return cancelActionFeedback(id, execution, null, RandomStringUtils.randomAlphanumeric(1000));
}
public static String cancelActionFeedback(final String id, final String execution, final String message)
throws JSONException {
return new JSONObject().put("id", id)
.put("status",
new JSONObject().put("execution", execution)
.put("result", new JSONObject().put("finished", "success"))
.put("details", new JSONArray().put(message)))
.toString();
return cancelActionFeedback(id, execution, null, message);
}
public static String cancelActionFeedback(final String id, final String execution, final Integer code,
final String message) throws JSONException {
final JSONObject statusJson = new JSONObject().put("execution", execution)
.put("result", new JSONObject().put("finished", "success"))
.put("details", new JSONArray().put(message));
if (code != null) {
statusJson.put("code", code);
}
return new JSONObject().put("id", id).put("status", statusJson).toString();
}
public static JSONObject configData(final Map<String, String> attributes) throws JSONException {

View File

@@ -20,6 +20,8 @@ final class DdiApiModelProperties {
static final String TARGET_EXEC_STATUS = "status of the action execution";
static final String TARGET_EXEC_STATUS_CODE = "optional individual status code";
static final String TARGET_RESULT_VALUE = "result of the action execution";
static final String TARGET_RESULT_DETAILS = "List of details message information";

View File

@@ -185,17 +185,17 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
final Action cancelAction = deploymentManagement.cancelAction(actionId);
final DdiStatus ddiStatus = new DdiStatus(DdiStatus.ExecutionStatus.CLOSED,
new DdiResult(DdiResult.FinalResult.SUCCESS, new DdiProgress(2, 5)), List.of("Some feedback"));
new DdiResult(DdiResult.FinalResult.SUCCESS, new DdiProgress(2, 5)), null, List.of("Some feedback"));
final DdiActionFeedback feedback = new DdiActionFeedback(Instant.now().toString(), ddiStatus);
mockMvc.perform(post(
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/" + DdiRestConstants.CANCEL_ACTION
+ "/{actionId}/feedback",
tenantAware.getCurrentTenant(), target.getControllerId(), cancelAction.getId()).content(
objectMapper.writeValueAsString(feedback))
tenantAware.getCurrentTenant(), target.getControllerId(), cancelAction.getId())
.content(objectMapper.writeValueAsString(feedback))
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andDo(this.document.document(
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andDo(this.document
.document(
pathParameters(parameterWithName("tenant").description(ApiModelPropertiesGeneric.TENANT),
parameterWithName("controllerId").description(DdiApiModelProperties.CONTROLLER_ID),
parameterWithName("actionId").description(DdiApiModelProperties.ACTION_ID_CANCELED)),
@@ -206,6 +206,8 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
optionalRequestFieldWithPath("time")
.description(DdiApiModelProperties.FEEDBACK_ACTION_TIME),
requestFieldWithPath("status").description(DdiApiModelProperties.TARGET_STATUS),
requestFieldWithPath("status.code")
.description(DdiApiModelProperties.TARGET_EXEC_STATUS_CODE),
requestFieldWithPath("status.execution")
.description(DdiApiModelProperties.TARGET_EXEC_STATUS).type("enum")
.attributes(key("value").value(
@@ -396,17 +398,14 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
final Long actionId = getFirstAssignedActionId(assignDistributionSet(set.getId(), target.getControllerId()));
final DdiStatus ddiStatus = new DdiStatus(DdiStatus.ExecutionStatus.CLOSED,
new DdiResult(DdiResult.FinalResult.SUCCESS, new DdiProgress(2, 5)), List.of("Feedback message"));
new DdiResult(DdiResult.FinalResult.SUCCESS, new DdiProgress(2, 5)), 200, List.of("Feedback message"));
final DdiActionFeedback feedback = new DdiActionFeedback(Instant.now().toString(), ddiStatus);
mockMvc.perform(post(
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/" + DdiRestConstants.DEPLOYMENT_BASE_ACTION
+ "/{actionId}/feedback",
tenantAware.getCurrentTenant(), target.getControllerId(), actionId)
.content(objectMapper.writeValueAsString(feedback))
mockMvc.perform(post(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
+ DdiRestConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}/feedback", tenantAware.getCurrentTenant(),
target.getControllerId(), actionId).content(objectMapper.writeValueAsString(feedback))
.contentType(MediaType.APPLICATION_JSON_UTF8).accept(MediaTypes.HAL_JSON_VALUE))
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andDo(this.document.document(
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andDo(this.document.document(
pathParameters(parameterWithName("tenant").description(ApiModelPropertiesGeneric.TENANT),
parameterWithName("controllerId").description(DdiApiModelProperties.CONTROLLER_ID),
parameterWithName("actionId").description(DdiApiModelProperties.ACTION_ID)),
@@ -418,6 +417,8 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
optionalRequestFieldWithPath("time")
.description(DdiApiModelProperties.FEEDBACK_ACTION_TIME),
requestFieldWithPath("status").description(DdiApiModelProperties.TARGET_STATUS),
requestFieldWithPath("status.code")
.description(DdiApiModelProperties.TARGET_EXEC_STATUS_CODE),
requestFieldWithPath("status.execution")
.description(DdiApiModelProperties.TARGET_EXEC_STATUS).type("enum")
.attributes(key("value").value(

View File

@@ -251,6 +251,11 @@ public abstract class AbstractApiRestDocumentation extends AbstractRestIntegrati
.addUpdateActionStatus(entityFactory.actionStatus().create(action.getId()).status(Status.FINISHED));
}
protected void provideCodeFeedback(final Action action, final int code) {
controllerManagement.addUpdateActionStatus(
entityFactory.actionStatus().create(action.getId()).code(code).status(Status.RUNNING));
}
protected Target createTargetByGivenNameWithAttributes(final String name, final DistributionSet distributionSet) {
return createTargetByGivenNameWithAttributes(name, true, false, distributionSet);
}

View File

@@ -150,6 +150,8 @@ public final class MgmtApiModelProperties {
public static final String ACTION_STATUS_REPORTED_AT = "Time at which the status was reported (server time).";
public static final String ACTION_STATUS_CODE = "(Optional) Code provided by the device related to the status.";
public static final String ACTION_STATUS_LIST = "List of action status.";
public static final String ACTION_EXECUTION_STATUS = "Status of action.";

View File

@@ -462,7 +462,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio
@Test
@Description("Handles the GET request of retrieving a specific action on a specific target. Required Permission: READ_TARGET.")
public void getStatusFromAction() throws Exception {
final Action action = generateActionForTarget(targetId);
final Action action = generateActionForTarget(targetId, false);
provideCodeFeedback(action, 200);
mockMvc.perform(
get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + MgmtRestConstants.TARGET_V1_ACTIONS
@@ -481,7 +482,10 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio
.description(MgmtApiModelProperties.ACTION_STATUS_MESSAGES).type("String"),
fieldWithPath("content[].reportedAt")
.description(MgmtApiModelProperties.ACTION_STATUS_REPORTED_AT).type("String"),
fieldWithPath("content[].type").description(MgmtApiModelProperties.ACTION_STATUS_TYPE)
optionalRequestFieldWithPath("content[].code")
.description(MgmtApiModelProperties.ACTION_STATUS_CODE).type("Integer"),
fieldWithPath(
"content[].type").description(MgmtApiModelProperties.ACTION_STATUS_TYPE)
.attributes(key("value").value(
"['finished', 'error', 'warning', 'pending', 'running', 'canceled', 'retrieved', 'canceling']")))));
}