DDI API: Extend API response to report previous update execution status. (#506)
* DDI API: Limit number of feedback messages for an action status. Even though the number of ActionStatus entries are limited to 1000 per action using QuotaManagement, there is no limit on the number of messages that can be sent as part of a single ActionStatus. This allows a controller to potentially send large number of messages for a single action. Limiting the number of allowed messages to 50 using the javax.validation.constraints.Size within DdiStatus. Signed-off-by: Christian Storm <christian.storm@siemens.com> Signed-off-by: Himanshu Kumar Singh <himanshu.singh@siemens.com> Signed-off-by: Raju HS <raju.hs@siemens.com> * DDI API: Extend API response to retrieve controller feedback. (#381) Resolves #381 The mechanism is useful for example, when the client software running on the device loses this information prior to reporting a final execution status such as 'closed' to hawkBit. This may happen, e.g., due to a power cycle or simply a crash. Upon the client software restarting, it installs the same payload again as advertised by hawkBit (as the device has not sent a final update execution status). Instead, if the last feedback sent to hawkBit would be reported back to the device, the client may resume installation. Feedback messages sent as part of POST /{tenant}/controller/v1/{targetid}/deploymentBase/{actionId}/feedback, are sent back to controller as part of response to GET /{tenant}/controller/v1/{targetid}/deploymentBase/{actionId}. Following example illustrates the API changes: 1. After retrieving the action from server, controller starts download and sends a feedback. curl 'http://127.0.0.1:8080/default/controller/v1/1/deploymentBase/1/feedback' -i -X POST -H 'Content-Type: application/json;charset=UTF-8' -H 'Accept: application/hal+json' -d '{ "id" : "1", "time" : "20170406T121500", "status" : { "result" : { "progress" : { "of" : 1, "cnt" : 0 }, "finished" : "none" }, "execution" : "proceeding", "details" : [ "proceeding with download" ] } }' 2. Once artifact download has finished, controller sends another feedback. curl 'http://127.0.0.1:8080/default/controller/v1/1/deploymentBase/1/feedback' -i -X POST -H 'Content-Type: application/json;charset=UTF-8' -H 'Accept: application/hal+json' -d '{ { "id" : "1", "time" : "20170406T123000", "status" : { "result" : { "progress" : { "of" : 1, "cnt" : 0 }, "finished" : "none" }, "execution" : "proceeding", "details" : [ "downloaded artifacts for update" ] } }' 3. If there is a power outage now, the controller can retrieve the messages posted earlier from the action history when it restarts again. curl 'http://127.0.0.1:8080/default/controller/v1/1/deploymentBase/1?c=411599879&actionHistory=-1' -i -H 'Accept: application/hal+json' Response will be like below { "id": "1", "deployment": { "download": "forced", "update": "forced", "chunks": [ { "part": "os", "version": "1", "name": "1", "artifacts": [....], } ], }, "actionHistory": { "status": "RETRIEVED", "messages": [ "downloaded artifacts for update", "proceeding with download" ] } } 4. Based on the feedback messages, controller may be able to skip the download and resume with installation and send additional feedback. curl 'http://127.0.0.1:8080/default/controller/v1/1/deploymentBase/1/feedback' -i -X POST -H 'Content-Type: application/json;charset=UTF-8' -H 'Accept: application/hal+json' -d '{ "id" : "1", "time" : "20170406T121314", "status" : { "result" : { "progress" : { "of" : 1, "cnt" : 0 }, "finished" : "none" }, "execution" : "resumed", "details" : [ "resuming installation based on previous feedback, download skipped" ] } }' Note: The maximum number of messages to be retrieved from the database by a controller is limited to 100. The actionHistory parameter's value has the following meaning: Input Value | Output ----------------------------- <0 | Retrieve maximum allowed number of messages from | action history. unspecified or =0 | Do not retrieve any message (default). >0 | Retrieve specified number of messages, limited by | maximum allowed number. Signed-off-by: Christian Storm <christian.storm@siemens.com> Signed-off-by: Himanshu Kumar Singh <himanshu.singh@siemens.com> Signed-off-by: Raju HS <raju.hs@siemens.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
package org.eclipse.hawkbit.repository;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -317,4 +318,30 @@ public interface ControllerManagement {
|
||||
+ SpringEvalExpressions.IS_SYSTEM_CODE)
|
||||
Optional<Target> findByTargetId(@NotNull Long targetId);
|
||||
|
||||
/**
|
||||
* Retrieves the specified number of messages from action history of the
|
||||
* given {@link Action} based on messageCount. Regardless of the value of
|
||||
* messageCount, in order to restrict resource utilization by controllers,
|
||||
* maximum number of messages that are retrieved from database is limited by
|
||||
* {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}. messageCount <
|
||||
* 0, retrieves the maximum allowed number of action status messages from
|
||||
* history; messageCount = 0, does not retrieve any message; and
|
||||
* messageCount > 0, retrieves the specified number of messages, limited by
|
||||
* maximum allowed number. A controller sends the feedback for an
|
||||
* {@link ActionStatus} as a list of messages; while returning the messages,
|
||||
* even though the messages from multiple {@link ActionStatus} are retrieved
|
||||
* in descending order by the reported time
|
||||
* ({@link ActionStatus#getOccurredAt()}), i.e. latest ActionStatus first,
|
||||
* the sub-ordering of messages from within single {@link ActionStatus} is
|
||||
* unspecified.
|
||||
*
|
||||
* @param actionId
|
||||
* to be filtered on
|
||||
* @param messageCount
|
||||
* is the number of messages to return from history
|
||||
*
|
||||
* @return action history.
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
|
||||
List<String> getActionHistoryMessages(@NotNull Long actionId, final int messageCount);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@ public final class RepositoryConstants {
|
||||
*/
|
||||
public static final int DEFAULT_DS_TYPES_IN_TENANT = 2;
|
||||
|
||||
/**
|
||||
* Maximum number of messages that can be retrieved by a controller for an
|
||||
* {@link Action}.
|
||||
*/
|
||||
public static final int MAX_ACTION_HISTORY_MSG_COUNT = 100;
|
||||
|
||||
private RepositoryConstants() {
|
||||
// Utility class.
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
@@ -63,4 +65,21 @@ public interface ActionStatusRepository
|
||||
*/
|
||||
@EntityGraph(value = "ActionStatus.withMessages", type = EntityGraphType.LOAD)
|
||||
Page<ActionStatus> getByActionId(Pageable pageReq, Long actionId);
|
||||
|
||||
/**
|
||||
* Finds a filtered list of status messages for an action.
|
||||
*
|
||||
* @param pageable
|
||||
* for page configuration
|
||||
* @param actionId
|
||||
* for which to get the status messages
|
||||
* @param filter
|
||||
* is the SQL like pattern to use for filtering out or excluding
|
||||
* the messages
|
||||
*
|
||||
* @return Page with found status messages.
|
||||
*/
|
||||
@Query("SELECT message FROM JpaActionStatus actionstatus JOIN actionstatus.messages message WHERE actionstatus.action.id = :actionId AND message NOT LIKE :filter")
|
||||
Page<String> findMessagesByActionIdAndMessageNotLike(final Pageable pageable, @Param("actionId") Long actionId,
|
||||
@Param("filter") String filter);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
package org.eclipse.hawkbit.repository.jpa;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -59,6 +60,7 @@ import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.dao.ConcurrencyFailureException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
@@ -522,4 +524,25 @@ public class JpaControllerManagement implements ControllerManagement {
|
||||
return actionStatusRepository.findByActionId(pageReq, actionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getActionHistoryMessages(final Long actionId, final int messageCount) {
|
||||
// Just return empty list in case messageCount is zero.
|
||||
if (messageCount == 0) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// For negative and large value of messageCount, limit the number of
|
||||
// messages.
|
||||
int limit = messageCount < 0 || messageCount >= RepositoryConstants.MAX_ACTION_HISTORY_MSG_COUNT
|
||||
? RepositoryConstants.MAX_ACTION_HISTORY_MSG_COUNT : messageCount;
|
||||
|
||||
PageRequest pageable = new PageRequest(0, limit, new Sort(Direction.DESC, "occurredAt"));
|
||||
Page<String> messages = actionStatusRepository.findMessagesByActionIdAndMessageNotLike(pageable, actionId,
|
||||
RepositoryConstants.SERVER_MESSAGE_PREFIX + "%");
|
||||
|
||||
LOG.debug("Retrieved {} message(s) from action history for action {}.", messages.getNumberOfElements(),
|
||||
actionId);
|
||||
|
||||
return messages.getContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import ru.yandex.qatools.allure.annotations.Description;
|
||||
@@ -620,4 +621,25 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest {
|
||||
assertThat(targetManagement.getControllerAttributes(controllerId)).as("Controller Attributes are wrong")
|
||||
.isEqualTo(testData);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test to verify the storage and retrieval of action history.")
|
||||
public void findMessagesByActionStatusId() {
|
||||
final DistributionSet testDs = testdataFactory.createDistributionSet("1");
|
||||
final List<Target> testTarget = testdataFactory.createTargets(1);
|
||||
|
||||
final Long actionId = assignDistributionSet(testDs, testTarget).getActions().get(0);
|
||||
|
||||
controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId)
|
||||
.status(Action.Status.RUNNING).messages(Lists.newArrayList("proceeding message 1")));
|
||||
controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId)
|
||||
.status(Action.Status.RUNNING).messages(Lists.newArrayList("proceeding message 2")));
|
||||
|
||||
final List<String> messages = controllerManagement.getActionHistoryMessages(actionId, 2);
|
||||
|
||||
assertThat(deploymentManagement.findActionStatusByAction(PAGE, actionId).getTotalElements())
|
||||
.as("Two action-states in total").isEqualTo(3L);
|
||||
assertThat(messages.get(0)).as("Message of action-status").isEqualTo("proceeding message 2");
|
||||
assertThat(messages.get(1)).as("Message of action-status").isEqualTo("proceeding message 1");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user