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) final ActionStatusCreate actionStatus = entityFactory.actionStatus().create(action.getId()).status(status)
.messages(messages); .messages(messages);
actionUpdateStatus.getCode().ifPresent(code -> {
actionStatus.code(code);
actionStatus.message("Device reported status code: " + code);
});
final Action updatedAction = (Status.CANCELED == status) final Action updatedAction = (Status.CANCELED == status)
? controllerManagement.addCancelActionStatus(actionStatus) ? controllerManagement.addCancelActionStatus(actionStatus)
: controllerManagement.addUpdateActionStatus(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.UpdateMode;
import org.eclipse.hawkbit.repository.builder.ActionStatusBuilder; import org.eclipse.hawkbit.repository.builder.ActionStatusBuilder;
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; 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.jpa.model.helper.SecurityTokenGeneratorHolder;
import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.Action.Status;
@@ -543,7 +545,7 @@ public class AmqpMessageHandlerServiceTest {
} }
@Test @Test
@Description("Tests TODO") @Description("Test next update is provided on finished action")
public void lookupNextUpdateActionAfterFinished() throws IllegalAccessException { public void lookupNextUpdateActionAfterFinished() throws IllegalAccessException {
// Mock // Mock
@@ -578,6 +580,42 @@ public class AmqpMessageHandlerServiceTest {
assertThat(actionProperties.getId()).as("event has wrong action id").isEqualTo(22L); 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) { private DmfActionUpdateStatus createActionUpdateStatus(final DmfActionStatus status) {
return createActionUpdateStatus(status, 2L); return createActionUpdateStatus(status, 2L);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ package org.eclipse.hawkbit.repository.jpa.model;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import javax.persistence.CollectionTable; import javax.persistence.CollectionTable;
import javax.persistence.Column; 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) @Column(name = "detail_message", length = MESSAGE_ENTRY_LENGTH, nullable = false, updatable = false)
private List<String> messages; private List<String> messages;
@Column(name = "code", nullable = true, updatable = false)
private Integer code;
/** /**
* Creates a new {@link ActionStatus} object. * Creates a new {@link ActionStatus} object.
* *
@@ -184,4 +188,11 @@ public class JpaActionStatus extends AbstractJpaTenantAwareBaseEntity implements
this.status = status; 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.IncompleteDistributionSetException;
import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException; import org.eclipse.hawkbit.repository.exception.InvalidDistributionSetException;
import org.eclipse.hawkbit.repository.exception.MultiAssignmentIsNotEnabledException; 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.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus; import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; 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.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@@ -33,6 +35,8 @@ public class DdiStatus {
@Valid @Valid
private final DdiResult result; private final DdiResult result;
private final Integer code;
private final List<String> details; private final List<String> details;
/** /**
@@ -42,14 +46,18 @@ public class DdiStatus {
* status * status
* @param result * @param result
* information * information
* @param code
* as optional code (can be null)
* @param details * @param details
* as optional addition * as optional addition
*/ */
@JsonCreator @JsonCreator
public DdiStatus(@JsonProperty("execution") final ExecutionStatus execution, 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.execution = execution;
this.result = result; this.result = result;
this.code = code;
this.details = details; this.details = details;
} }
@@ -69,6 +77,10 @@ public class DdiStatus {
return Collections.unmodifiableList(details); return Collections.unmodifiableList(details);
} }
public Integer getCode() {
return code;
}
/** /**
* The element status contains information about the execution of the * The element status contains information about the execution of the
* operation. * operation.
@@ -129,7 +141,8 @@ public class DdiStatus {
@Override @Override
public String toString() { 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 { public void shouldSerializeAndDeserializeObject() throws IOException {
// Setup // Setup
final String time = Instant.now().toString(); 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); final DdiActionFeedback ddiActionFeedback = new DdiActionFeedback(time, ddiStatus);
// Test // Test

View File

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

View File

@@ -324,7 +324,6 @@ public class DdiRootController implements DdiRootControllerRestApi {
final Target target = findTarget(controllerId); final Target target = findTarget(controllerId);
final Action action = findActionForTarget(actionId, target); final Action action = findActionForTarget(actionId, target);
if (!action.isActive()) { if (!action.isActive()) {
LOG.warn("Updating action {} with feedback {} not possible since action not active anymore.", LOG.warn("Updating action {} with feedback {} not possible since action not active anymore.",
action.getId(), feedback.getStatus()); action.getId(), feedback.getStatus());
@@ -340,12 +339,20 @@ public class DdiRootController implements DdiRootControllerRestApi {
private ActionStatusCreate generateUpdateStatus(final DdiActionFeedback feedback, final String controllerId, private ActionStatusCreate generateUpdateStatus(final DdiActionFeedback feedback, final String controllerId,
final Long actionId) { final Long actionId) {
final ActionStatusCreate actionStatusCreate = entityFactory.actionStatus().create(actionId);
final List<String> messages = new ArrayList<>(); final List<String> messages = new ArrayList<>();
if (!CollectionUtils.isEmpty(feedback.getStatus().getDetails())) { if (!CollectionUtils.isEmpty(feedback.getStatus().getDetails())) {
messages.addAll(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; final Status status;
switch (feedback.getStatus().getExecution()) { switch (feedback.getStatus().getExecution()) {
case CANCELED: case CANCELED:
@@ -380,7 +387,7 @@ public class DdiRootController implements DdiRootControllerRestApi {
break; 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) { 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 Target target = findTarget(controllerId);
final Action action = findActionForTarget(actionId, target); final Action action = findActionForTarget(actionId, target);
if (action.isCancelingOrCanceled()) { if (action.isCancelingOrCanceled()) {
final DdiCancel cancel = new DdiCancel(String.valueOf(action.getId()), final DdiCancel cancel = new DdiCancel(String.valueOf(action.getId()),
new DdiCancelActionToStop(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, private static ActionStatusCreate generateActionCancelStatus(final DdiActionFeedback feedback, final Target target,
final Long actionId, final EntityFactory entityFactory) { final Long actionId, final EntityFactory entityFactory) {
final ActionStatusCreate actionStatusCreate = entityFactory.actionStatus().create(actionId);
final List<String> messages = new ArrayList<>(); final List<String> messages = new ArrayList<>();
final Status status; final Status status;
switch (feedback.getStatus().getExecution()) { switch (feedback.getStatus().getExecution()) {
@@ -531,7 +539,13 @@ public class DdiRootController implements DdiRootControllerRestApi {
messages.addAll(feedback.getStatus().getDetails()); 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, protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus, final DdiResult ddiResult,
final List<String> messages) throws JsonProcessingException { 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)); return objectMapper.writeValueAsString(new DdiActionFeedback(Instant.now().toString(), ddiStatus));
} }
protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus, protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus,
final DdiResult.FinalResult finalResult, final List<String> messages) throws JsonProcessingException { 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); messages);
return objectMapper.writeValueAsString(new DdiActionFeedback(Instant.now().toString(), ddiStatus)); 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, final String feedback = getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING, DdiResult.FinalResult.NONE,
messages); null, messages);
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isForbidden()); postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isForbidden());
} }

View File

@@ -35,6 +35,9 @@ public class MgmtActionStatus {
@JsonProperty @JsonProperty
private Long reportedAt; private Long reportedAt;
@JsonProperty
private Integer code;
/** /**
* @return the statusId * @return the statusId
*/ */
@@ -95,4 +98,18 @@ public class MgmtActionStatus {
this.reportedAt = reportedAt; 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.setReportedAt(actionStatus.getCreatedAt());
result.setStatusId(actionStatus.getId()); result.setStatusId(actionStatus.getId());
result.setType(actionStatus.getStatus().name().toLowerCase()); result.setType(actionStatus.getStatus().name().toLowerCase());
actionStatus.getCode().ifPresent(result::setCode);
return result; return result;
} }

View File

@@ -8,11 +8,13 @@
*/ */
package org.eclipse.hawkbit.rest.util; package org.eclipse.hawkbit.rest.util;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroup;
@@ -184,6 +186,101 @@ public abstract class JsonBuilder {
} }
/**
* 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 { public static String distributionSetTypes(final List<DistributionSetType> types) throws JSONException {
final JSONArray result = new JSONArray(); final JSONArray result = new JSONArray();
@@ -545,15 +642,27 @@ public abstract class JsonBuilder {
return json.toString(); 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) public static String cancelActionFeedback(final String id, final String execution, final String message)
throws JSONException { throws JSONException {
return new JSONObject().put("id", id) return cancelActionFeedback(id, execution, null, message);
.put("status", }
new JSONObject().put("execution", execution)
.put("result", new JSONObject().put("finished", "success"))
.put("details", new JSONArray().put(message)))
.toString();
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 { 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 = "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_VALUE = "result of the action execution";
static final String TARGET_RESULT_DETAILS = "List of details message information"; 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 Action cancelAction = deploymentManagement.cancelAction(actionId);
final DdiStatus ddiStatus = new DdiStatus(DdiStatus.ExecutionStatus.CLOSED, 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); final DdiActionFeedback feedback = new DdiActionFeedback(Instant.now().toString(), ddiStatus);
mockMvc.perform(post( mockMvc.perform(post(
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/" + DdiRestConstants.CANCEL_ACTION DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/" + DdiRestConstants.CANCEL_ACTION
+ "/{actionId}/feedback", + "/{actionId}/feedback",
tenantAware.getCurrentTenant(), target.getControllerId(), cancelAction.getId()).content( tenantAware.getCurrentTenant(), target.getControllerId(), cancelAction.getId())
objectMapper.writeValueAsString(feedback)) .content(objectMapper.writeValueAsString(feedback))
.contentType(MediaType.APPLICATION_JSON_UTF8)) .contentType(MediaType.APPLICATION_JSON_UTF8))
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andDo(this.document
.andDo(this.document.document( .document(
pathParameters(parameterWithName("tenant").description(ApiModelPropertiesGeneric.TENANT), pathParameters(parameterWithName("tenant").description(ApiModelPropertiesGeneric.TENANT),
parameterWithName("controllerId").description(DdiApiModelProperties.CONTROLLER_ID), parameterWithName("controllerId").description(DdiApiModelProperties.CONTROLLER_ID),
parameterWithName("actionId").description(DdiApiModelProperties.ACTION_ID_CANCELED)), parameterWithName("actionId").description(DdiApiModelProperties.ACTION_ID_CANCELED)),
@@ -206,6 +206,8 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
optionalRequestFieldWithPath("time") optionalRequestFieldWithPath("time")
.description(DdiApiModelProperties.FEEDBACK_ACTION_TIME), .description(DdiApiModelProperties.FEEDBACK_ACTION_TIME),
requestFieldWithPath("status").description(DdiApiModelProperties.TARGET_STATUS), requestFieldWithPath("status").description(DdiApiModelProperties.TARGET_STATUS),
requestFieldWithPath("status.code")
.description(DdiApiModelProperties.TARGET_EXEC_STATUS_CODE),
requestFieldWithPath("status.execution") requestFieldWithPath("status.execution")
.description(DdiApiModelProperties.TARGET_EXEC_STATUS).type("enum") .description(DdiApiModelProperties.TARGET_EXEC_STATUS).type("enum")
.attributes(key("value").value( .attributes(key("value").value(
@@ -396,17 +398,14 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
final Long actionId = getFirstAssignedActionId(assignDistributionSet(set.getId(), target.getControllerId())); final Long actionId = getFirstAssignedActionId(assignDistributionSet(set.getId(), target.getControllerId()));
final DdiStatus ddiStatus = new DdiStatus(DdiStatus.ExecutionStatus.CLOSED, 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); final DdiActionFeedback feedback = new DdiActionFeedback(Instant.now().toString(), ddiStatus);
mockMvc.perform(post( mockMvc.perform(post(DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/" + DdiRestConstants.DEPLOYMENT_BASE_ACTION + DdiRestConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}/feedback", tenantAware.getCurrentTenant(),
+ "/{actionId}/feedback", target.getControllerId(), actionId).content(objectMapper.writeValueAsString(feedback))
tenantAware.getCurrentTenant(), target.getControllerId(), actionId)
.content(objectMapper.writeValueAsString(feedback))
.contentType(MediaType.APPLICATION_JSON_UTF8).accept(MediaTypes.HAL_JSON_VALUE)) .contentType(MediaType.APPLICATION_JSON_UTF8).accept(MediaTypes.HAL_JSON_VALUE))
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()).andDo(this.document.document(
.andDo(this.document.document(
pathParameters(parameterWithName("tenant").description(ApiModelPropertiesGeneric.TENANT), pathParameters(parameterWithName("tenant").description(ApiModelPropertiesGeneric.TENANT),
parameterWithName("controllerId").description(DdiApiModelProperties.CONTROLLER_ID), parameterWithName("controllerId").description(DdiApiModelProperties.CONTROLLER_ID),
parameterWithName("actionId").description(DdiApiModelProperties.ACTION_ID)), parameterWithName("actionId").description(DdiApiModelProperties.ACTION_ID)),
@@ -418,6 +417,8 @@ public class RootControllerDocumentationTest extends AbstractApiRestDocumentatio
optionalRequestFieldWithPath("time") optionalRequestFieldWithPath("time")
.description(DdiApiModelProperties.FEEDBACK_ACTION_TIME), .description(DdiApiModelProperties.FEEDBACK_ACTION_TIME),
requestFieldWithPath("status").description(DdiApiModelProperties.TARGET_STATUS), requestFieldWithPath("status").description(DdiApiModelProperties.TARGET_STATUS),
requestFieldWithPath("status.code")
.description(DdiApiModelProperties.TARGET_EXEC_STATUS_CODE),
requestFieldWithPath("status.execution") requestFieldWithPath("status.execution")
.description(DdiApiModelProperties.TARGET_EXEC_STATUS).type("enum") .description(DdiApiModelProperties.TARGET_EXEC_STATUS).type("enum")
.attributes(key("value").value( .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)); .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) { protected Target createTargetByGivenNameWithAttributes(final String name, final DistributionSet distributionSet) {
return createTargetByGivenNameWithAttributes(name, true, false, 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_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_STATUS_LIST = "List of action status.";
public static final String ACTION_EXECUTION_STATUS = "Status of action."; public static final String ACTION_EXECUTION_STATUS = "Status of action.";

View File

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