Feature offline deployments (#563)

* Repository support offline deployments.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Add offline assignment to Management API.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* DsAssignmentStrategy introduced.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Fixed JavaDoc.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Readibility improved.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>
This commit is contained in:
Kai Zimmermann
2017-07-27 17:28:56 +02:00
committed by GitHub
parent 09b24fa97d
commit e2ca4cf840
15 changed files with 693 additions and 190 deletions

View File

@@ -219,6 +219,9 @@ public interface MgmtDistributionSetRestApi {
* @param targetIds * @param targetIds
* the IDs of the target which should get assigned to the * the IDs of the target which should get assigned to the
* distribution set given in the response body * distribution set given in the response body
* @param offline
* to <code>true</code> if update was executed offline, i.e. not
* managed by hawkBit.
* @return status OK if the assignment of the targets was successful and a * @return status OK if the assignment of the targets was successful and a
* complex return body which contains information about the assigned * complex return body which contains information about the assigned
* targets and the already assigned targets counters * targets and the already assigned targets counters
@@ -227,7 +230,9 @@ public interface MgmtDistributionSetRestApi {
MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE, MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE }) MediaType.APPLICATION_JSON_VALUE })
ResponseEntity<MgmtTargetAssignmentResponseBody> createAssignedTarget( ResponseEntity<MgmtTargetAssignmentResponseBody> createAssignedTarget(
@PathVariable("distributionSetId") Long distributionSetId, List<MgmtTargetAssignmentRequestBody> targetIds); @PathVariable("distributionSetId") Long distributionSetId,
final List<MgmtTargetAssignmentRequestBody> targetIds,
@RequestParam(value = "offline", required = false) boolean offline);
/** /**
* Gets a paged list of meta data for a distribution set. * Gets a paged list of meta data for a distribution set.

View File

@@ -15,6 +15,7 @@ import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssigment; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssigment;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAttributes; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAttributes;
@@ -245,6 +246,7 @@ public interface MgmtTargetRestApi {
* *
* @param controllerId * @param controllerId
* the ID of the target to retrieve the assigned distribution * the ID of the target to retrieve the assigned distribution
*
* @return the assigned distribution set with status OK, if none is assigned * @return the assigned distribution set with status OK, if none is assigned
* than {@code null} content (e.g. "{}") * than {@code null} content (e.g. "{}")
*/ */
@@ -259,13 +261,20 @@ public interface MgmtTargetRestApi {
* of the target to change * of the target to change
* @param dsId * @param dsId
* of the distributionset that is to be assigned * of the distributionset that is to be assigned
* @return http status * @param offline
* to <code>true</code> if update was executed offline, i.e. not
* managed by hawkBit.
*
* @return status OK if the assignment of the targets was successful and a
* complex return body which contains information about the assigned
* targets and the already assigned targets counters
*/ */
@RequestMapping(method = RequestMethod.POST, value = "/{controllerId}/assignedDS", consumes = { @RequestMapping(method = RequestMethod.POST, value = "/{controllerId}/assignedDS", consumes = {
MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE, MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE }) MediaType.APPLICATION_JSON_VALUE })
ResponseEntity<Void> postAssignedDistributionSet(@PathVariable("controllerId") String controllerId, ResponseEntity<MgmtTargetAssignmentResponseBody> postAssignedDistributionSet(
MgmtDistributionSetAssigment dsId); @PathVariable("controllerId") String controllerId, MgmtDistributionSetAssigment dsId,
@RequestParam(value = "offline", required = false) boolean offline);
/** /**
* Handles the GET request of retrieving the installed distribution set of * Handles the GET request of retrieving the installed distribution set of

View File

@@ -35,7 +35,6 @@ import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; import org.eclipse.hawkbit.repository.model.DistributionSetMetadata;
import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.Target;
@@ -229,16 +228,22 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
@Override @Override
public ResponseEntity<MgmtTargetAssignmentResponseBody> createAssignedTarget( public ResponseEntity<MgmtTargetAssignmentResponseBody> createAssignedTarget(
@PathVariable("distributionSetId") final Long distributionSetId, @PathVariable("distributionSetId") final Long distributionSetId,
@RequestBody final List<MgmtTargetAssignmentRequestBody> targetIds) { @RequestBody final List<MgmtTargetAssignmentRequestBody> assignments,
@RequestParam(value = "offline", required = false) final boolean offline) {
final DistributionSetAssignmentResult assignDistributionSet = this.deployManagament.assignDistributionSet( if (offline) {
return ResponseEntity.ok(MgmtDistributionSetMapper
.toResponse(this.deployManagament.offlineAssignedDistributionSet(distributionSetId, assignments
.stream().map(MgmtTargetAssignmentRequestBody::getId).collect(Collectors.toList()))));
}
return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(this.deployManagament.assignDistributionSet(
distributionSetId, distributionSetId,
targetIds.stream() assignments.stream()
.map(t -> new TargetWithActionType(t.getId(), .map(assignment -> new TargetWithActionType(assignment.getId(),
MgmtRestModelMapper.convertActionType(t.getType()), t.getForcetime())) MgmtRestModelMapper.convertActionType(assignment.getType()), assignment.getForcetime()))
.collect(Collectors.toList())); .collect(Collectors.toList()))));
return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(assignDistributionSet));
} }
@Override @Override

View File

@@ -21,6 +21,7 @@ import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssigment; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtDistributionSetAssigment;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAttributes; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAttributes;
@@ -253,16 +254,21 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
} }
@Override @Override
public ResponseEntity<Void> postAssignedDistributionSet(@PathVariable("controllerId") final String controllerId, public ResponseEntity<MgmtTargetAssignmentResponseBody> postAssignedDistributionSet(
@RequestBody final MgmtDistributionSetAssigment dsId) { @PathVariable("controllerId") final String controllerId,
@RequestBody final MgmtDistributionSetAssigment dsId,
@RequestParam(value = "offline", required = false) final boolean offline) {
if (offline) {
return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(
deploymentManagement.offlineAssignedDistributionSet(dsId.getId(), Arrays.asList(controllerId))));
}
findTargetWithExceptionIfNotFound(controllerId); findTargetWithExceptionIfNotFound(controllerId);
final ActionType type = (dsId.getType() != null) ? MgmtRestModelMapper.convertActionType(dsId.getType()) final ActionType type = (dsId.getType() != null) ? MgmtRestModelMapper.convertActionType(dsId.getType())
: ActionType.FORCED; : ActionType.FORCED;
this.deploymentManagement.assignDistributionSet(dsId.getId(), type, dsId.getForcetime(), return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(deploymentManagement
Arrays.asList(controllerId)); .assignDistributionSet(dsId.getId(), type, dsId.getForcetime(), Arrays.asList(controllerId))));
return ResponseEntity.ok().build();
} }
@Override @Override

View File

@@ -205,30 +205,49 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr
@Test @Test
@Description("Ensures that multi target assignment through API is reflected by the repository.") @Description("Ensures that multi target assignment through API is reflected by the repository.")
public void assignMultipleTargetsToDistributionSet() throws Exception { public void assignMultipleTargetsToDistributionSet() throws Exception {
// prepare distribution set final DistributionSet createdDs = testdataFactory.createDistributionSet();
final Set<DistributionSet> createDistributionSetsAlphabetical = createDistributionSetsAlphabetical(1); final List<Target> targets = testdataFactory.createTargets(5);
final DistributionSet createdDs = createDistributionSetsAlphabetical.iterator().next();
// prepare targets
final String[] knownTargetIds = new String[] { "1", "2", "3", "4", "5" };
final JSONArray list = new JSONArray(); final JSONArray list = new JSONArray();
for (final String targetId : knownTargetIds) { targets.forEach(target -> list.put(new JSONObject().put("id", target.getControllerId())));
testdataFactory.createTarget(targetId);
list.put(new JSONObject().put("id", Long.valueOf(targetId)));
}
// assign already one target to DS // assign already one target to DS
assignDistributionSet(createdDs.getId(), knownTargetIds[0]); assignDistributionSet(createdDs.getId(), targets.get(0).getControllerId());
mvc.perform(post( mvc.perform(post(
MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + createdDs.getId() + "/assignedTargets") MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + createdDs.getId() + "/assignedTargets")
.contentType(MediaType.APPLICATION_JSON).content(list.toString())) .contentType(MediaType.APPLICATION_JSON).content(list.toString()))
.andExpect(status().isOk()).andExpect(jsonPath("$.assigned", equalTo(knownTargetIds.length - 1))) .andExpect(status().isOk()).andExpect(jsonPath("$.assigned", equalTo(targets.size() - 1)))
.andExpect(jsonPath("$.alreadyAssigned", equalTo(1))) .andExpect(jsonPath("$.alreadyAssigned", equalTo(1)))
.andExpect(jsonPath("$.total", equalTo(knownTargetIds.length))); .andExpect(jsonPath("$.total", equalTo(targets.size())));
assertThat(targetManagement.findTargetByAssignedDistributionSet(createdDs.getId(), PAGE).getContent()) assertThat(targetManagement.findTargetByAssignedDistributionSet(createdDs.getId(), PAGE).getContent())
.as("Five targets in repository have DS assigned").hasSize(5); .as("Five targets in repository have DS assigned").hasSize(5);
} }
@Test
@Description("Ensures that offline reported multi target assignment through API is reflected by the repository.")
public void offlineAssignmentOfMultipleTargetsToDistributionSet() throws Exception {
final DistributionSet createdDs = testdataFactory.createDistributionSet();
final List<Target> targets = testdataFactory.createTargets(5);
final JSONArray list = new JSONArray();
targets.forEach(target -> list.put(new JSONObject().put("id", target.getControllerId())));
// assign already one target to DS
assignDistributionSet(createdDs.getId(), targets.get(0).getControllerId());
mvc.perform(post(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + createdDs.getId()
+ "/assignedTargets?offline=true").contentType(MediaType.APPLICATION_JSON).content(list.toString()))
.andExpect(status().isOk()).andExpect(jsonPath("$.assigned", equalTo(targets.size() - 1)))
.andExpect(jsonPath("$.alreadyAssigned", equalTo(1)))
.andExpect(jsonPath("$.total", equalTo(targets.size())));
assertThat(targetManagement.findTargetByAssignedDistributionSet(createdDs.getId(), PAGE).getContent())
.as("Five targets in repository have DS assigned").hasSize(5);
assertThat(targetManagement.findTargetByInstalledDistributionSet(createdDs.getId(), PAGE).getContent())
.hasSize(4);
}
@Test @Test
@Description("Ensures that assigned targets of DS are returned as reflected by the repository.") @Description("Ensures that assigned targets of DS are returned as reflected by the repository.")
public void getAssignedTargetsOfDistributionSet() throws Exception { public void getAssignedTargetsOfDistributionSet() throws Exception {

View File

@@ -47,6 +47,7 @@ import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.test.util.WithUser; import org.eclipse.hawkbit.repository.test.util.WithUser;
import org.eclipse.hawkbit.rest.exception.MessageNotReadableException; import org.eclipse.hawkbit.rest.exception.MessageNotReadableException;
import org.eclipse.hawkbit.rest.json.model.ExceptionInfo; import org.eclipse.hawkbit.rest.json.model.ExceptionInfo;
@@ -1145,7 +1146,9 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest
mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + "/assignedDS") mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + "/assignedDS")
.content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andExpect(jsonPath("assigned", equalTo(1))).andExpect(jsonPath("alreadyAssigned", equalTo(0)))
.andExpect(jsonPath("total", equalTo(1)));
assertThat(deploymentManagement.getAssignedDistributionSet(target.getControllerId()).get()).isEqualTo(set); assertThat(deploymentManagement.getAssignedDistributionSet(target.getControllerId()).get()).isEqualTo(set);
target = targetManagement.findTargetByControllerID(target.getControllerId()).get(); target = targetManagement.findTargetByControllerID(target.getControllerId()).get();
@@ -1153,7 +1156,41 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest
// repeating DS assignment leads again to OK // repeating DS assignment leads again to OK
mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + "/assignedDS") mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + "/assignedDS")
.content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andExpect(jsonPath("assigned", equalTo(0))).andExpect(jsonPath("alreadyAssigned", equalTo(1)))
.andExpect(jsonPath("total", equalTo(1)));
// ...but does not change the target
assertThat(targetManagement.findTargetByControllerID(target.getControllerId()).get()).isEqualTo(target);
}
@Test
@Description("Verfies that an offline DS to target assignment is reflected by the repository and that repeating "
+ "the assignment does not change the target.")
public void offlineAssignDistributionSetToTarget() throws Exception {
Target target = testdataFactory.createTarget();
final DistributionSet set = testdataFactory.createDistributionSet("one");
mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId()
+ "/assignedDS?offline=true").content("{\"id\":" + set.getId() + "}")
.contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andExpect(jsonPath("assigned", equalTo(1))).andExpect(jsonPath("alreadyAssigned", equalTo(0)))
.andExpect(jsonPath("total", equalTo(1)));
assertThat(deploymentManagement.getAssignedDistributionSet(target.getControllerId()).get()).isEqualTo(set);
assertThat(deploymentManagement.getInstalledDistributionSet(target.getControllerId()).get()).isEqualTo(set);
target = targetManagement.findTargetByControllerID(target.getControllerId()).get();
assertThat(target.getUpdateStatus()).isEqualTo(TargetUpdateStatus.IN_SYNC);
// repeating DS assignment leads again to OK
mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId()
+ "/assignedDS?offline=true").content("{\"id\":" + set.getId() + "}")
.contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
.andExpect(jsonPath("assigned", equalTo(0))).andExpect(jsonPath("alreadyAssigned", equalTo(1)))
.andExpect(jsonPath("total", equalTo(1)));
// ...but does not change the target // ...but does not change the target
assertThat(targetManagement.findTargetByControllerID(target.getControllerId()).get()).isEqualTo(target); assertThat(targetManagement.findTargetByControllerID(target.getControllerId()).get()).isEqualTo(target);

View File

@@ -14,6 +14,7 @@ import java.util.Optional;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException;
@@ -28,6 +29,7 @@ import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@@ -69,7 +71,6 @@ public interface DeploymentManagement {
DistributionSetAssignmentResult assignDistributionSet(@NotNull Long dsID, @NotNull ActionType actionType, DistributionSetAssignmentResult assignDistributionSet(@NotNull Long dsID, @NotNull ActionType actionType,
long forcedTimestamp, @NotEmpty Collection<String> controllerIDs); long forcedTimestamp, @NotEmpty Collection<String> controllerIDs);
/** /**
* method assigns the {@link DistributionSet} to all {@link Target}s by * method assigns the {@link DistributionSet} to all {@link Target}s by
* their IDs with a specific {@link ActionType} and {@code forcetime}. * their IDs with a specific {@link ActionType} and {@code forcetime}.
@@ -116,6 +117,38 @@ public interface DeploymentManagement {
DistributionSetAssignmentResult assignDistributionSet(@NotNull Long dsID, DistributionSetAssignmentResult assignDistributionSet(@NotNull Long dsID,
@NotEmpty Collection<TargetWithActionType> targets, String actionMessage); @NotEmpty Collection<TargetWithActionType> targets, String actionMessage);
/**
* Method registers an "offline" assignment, i.e. adds a completed action
* for the given {@link DistributionSet} to the given {@link Target}s.
*
* The handling differs to hawkBit managed updates my means that:<br/>
*
* <ol type="A">
* <li>it ignores targets completely that are in
* {@link TargetUpdateStatus#PENDING}.</li>
* <li>it creates completed actions.</li>
* <li>sets both installed and assigned DS on the target and switches the
* status to {@link TargetUpdateStatus#IN_SYNC}.</li>
* <li>does not send a {@link TargetAssignDistributionSetEvent}.</li>
* </ol>
*
* @param dsID
* the ID of the distribution set that was assigned
* @param controllerIDs
* a list of IDs of the targets that where assigned
* @return the assignment result
*
* @throws IncompleteDistributionSetException
* if mandatory {@link SoftwareModuleType} are not assigned as
* defined by the {@link DistributionSetType}.
*
* @throws EntityNotFoundException
* if either provided {@link DistributionSet} or {@link Target}s
* do not exist
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
DistributionSetAssignmentResult offlineAssignedDistributionSet(Long dsID, Collection<String> controllerIDs);
/** /**
* Cancels given {@link Action} for given {@link Target}. The method will * Cancels given {@link Action} for given {@link Target}. The method will
* immediately add a {@link Status#CANCELED} status to the action. However, * immediately add a {@link Status#CANCELED} status to the action. However,

View File

@@ -0,0 +1,188 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.jpa;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.CancelTargetAssignmentEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
/**
* {@link DistributionSet} to {@link Target} assignment strategy as utility for
* {@link JpaDeploymentManagement}.
*
*/
public abstract class AbstractDsAssignmentStrategy {
protected final TargetRepository targetRepository;
protected final AfterTransactionCommitExecutor afterCommit;
protected final ApplicationEventPublisher eventPublisher;
protected final ApplicationContext applicationContext;
private final ActionRepository actionRepository;
private final ActionStatusRepository actionStatusRepository;
AbstractDsAssignmentStrategy(final TargetRepository targetRepository,
final AfterTransactionCommitExecutor afterCommit, final ApplicationEventPublisher eventPublisher,
final ApplicationContext applicationContext, final ActionRepository actionRepository,
final ActionStatusRepository actionStatusRepository) {
this.targetRepository = targetRepository;
this.afterCommit = afterCommit;
this.eventPublisher = eventPublisher;
this.applicationContext = applicationContext;
this.actionRepository = actionRepository;
this.actionStatusRepository = actionStatusRepository;
}
/**
* Find targets to be considered for assignment.
*
* @param controllerIDs
* as provided by repository caller
* @param distributionSetId
* to assign
* @return list of targets up to {@link Constants#MAX_ENTRIES_IN_STATEMENT}
*/
abstract List<JpaTarget> findTargetsForAssignment(final List<String> controllerIDs, final long distributionSetId);
/**
* Update status and DS fields of given target.
*
* @param distributionSet
* to set
* @param targetIds
* to change
* @param currentUser
* for auditing
*/
abstract void updateTargetStatus(final JpaDistributionSet distributionSet, final List<List<Long>> targetIds,
final String currentUser);
/**
* Cancels actions that can be canceled (i.e.
* {@link DistributionSet#isRequiredMigrationStep() is <code>false</code>})
* as a result of the new assignment and returns all {@link Target}s where
* such actions existed.
*
* @param targetIds
* to cancel actions for
* @return {@link Set} of {@link Target#getId()}s
*/
abstract Set<Long> findTargetIdsToCancel(List<List<Long>> targetIds);
/**
* Handles event sending related to the assignment.
*
* @param targets
* to send events for
* @param targetIdsCancelList
* targets where an action was canceled
* @param controllerIdsToActions
* mapping of {@link Target#getControllerId()} to new
* {@link Action} that was created as part of the assignment.
*/
abstract void sendAssignmentEvents(final List<JpaTarget> targets, final Set<Long> targetIdsCancelList,
final Map<String, JpaAction> controllerIdsToActions);
protected void sendTargetAssignDistributionSetEvent(final Action action) {
afterCommit.afterCommit(() -> eventPublisher
.publishEvent(new TargetAssignDistributionSetEvent(action, applicationContext.getId())));
}
protected void sendTargetUpdatedEvent(final JpaTarget target) {
afterCommit.afterCommit(
() -> eventPublisher.publishEvent(new TargetUpdatedEvent(target, applicationContext.getId())));
}
/**
* Removes {@link Action}s that are no longer necessary and sends
* cancellations to the controller.
*
* @param targetsIds
* to override {@link Action}s
*/
protected List<Long> overrideObsoleteUpdateActions(final Collection<Long> targetsIds) {
// Figure out if there are potential target/action combinations that
// need to be considered for cancellation
final List<JpaAction> activeActions = actionRepository
.findByActiveAndTargetIdInAndActionStatusNotEqualToAndDistributionSetRequiredMigrationStep(targetsIds,
Action.Status.CANCELING);
return activeActions.stream().map(action -> {
action.setStatus(Status.CANCELING);
// document that the status has been retrieved
actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELING, System.currentTimeMillis(),
RepositoryConstants.SERVER_MESSAGE_PREFIX + "cancel obsolete action due to new update"));
actionRepository.save(action);
cancelAssignDistributionSetEvent(action.getTarget(), action.getId());
return action.getTarget().getId();
}).collect(Collectors.toList());
}
/**
* Sends the {@link CancelTargetAssignmentEvent} for a specific target to
* the eventPublisher.
*
* @param target
* the Target which has been assigned to a distribution set
* @param actionId
* the action id of the assignment
*/
void cancelAssignDistributionSetEvent(final Target target, final Long actionId) {
afterCommit.afterCommit(() -> eventPublisher
.publishEvent(new CancelTargetAssignmentEvent(target, actionId, applicationContext.getId())));
}
JpaAction createTargetAction(final Map<String, TargetWithActionType> targetsWithActionMap, final JpaTarget target,
final JpaDistributionSet set) {
final JpaAction actionForTarget = new JpaAction();
final TargetWithActionType targetWithActionType = targetsWithActionMap.get(target.getControllerId());
actionForTarget.setActionType(targetWithActionType.getActionType());
actionForTarget.setForcedTime(targetWithActionType.getForceTime());
actionForTarget.setActive(true);
actionForTarget.setTarget(target);
actionForTarget.setDistributionSet(set);
return actionForTarget;
}
JpaActionStatus createActionStatus(final JpaAction action, final String actionMessage) {
final JpaActionStatus actionStatus = new JpaActionStatus();
actionStatus.setAction(action);
actionStatus.setOccurredAt(action.getCreatedAt());
if (actionMessage != null) {
actionStatus.addMessage(actionMessage);
}
return actionStatus;
}
}

View File

@@ -31,8 +31,6 @@ import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.CancelTargetAssignmentEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.ForceQuitActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.ForceQuitActionNotAllowedException;
@@ -47,7 +45,6 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.Action.Status;
@@ -62,7 +59,6 @@ import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.ConcurrencyFailureException; import org.springframework.dao.ConcurrencyFailureException;
@@ -99,51 +95,70 @@ public class JpaDeploymentManagement implements DeploymentManagement {
*/ */
private static final int ACTION_PAGE_LIMIT = 1000; private static final int ACTION_PAGE_LIMIT = 1000;
@Autowired private final EntityManager entityManager;
private EntityManager entityManager; private final ActionRepository actionRepository;
private final DistributionSetRepository distributionSetRepository;
private final TargetRepository targetRepository;
private final ActionStatusRepository actionStatusRepository;
private final TargetManagement targetManagement;
private final AuditorAware<String> auditorProvider;
private final ApplicationEventPublisher eventPublisher;
private final ApplicationContext applicationContext;
private final AfterTransactionCommitExecutor afterCommit;
private final VirtualPropertyReplacer virtualPropertyReplacer;
private final PlatformTransactionManager txManager;
private final OnlineDsAssignmentStrategy onlineDsAssignmentStrategy;
private final OfflineDsAssignmentStrategy offlineDsAssignmentStrategy;
@Autowired JpaDeploymentManagement(final EntityManager entityManager, final ActionRepository actionRepository,
private ActionRepository actionRepository; final DistributionSetRepository distributionSetRepository, final TargetRepository targetRepository,
final ActionStatusRepository actionStatusRepository, final TargetManagement targetManagement,
final AuditorAware<String> auditorProvider, final ApplicationEventPublisher eventPublisher,
final ApplicationContext applicationContext, final AfterTransactionCommitExecutor afterCommit,
final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager) {
this.entityManager = entityManager;
this.actionRepository = actionRepository;
this.distributionSetRepository = distributionSetRepository;
this.targetRepository = targetRepository;
this.actionStatusRepository = actionStatusRepository;
this.targetManagement = targetManagement;
this.auditorProvider = auditorProvider;
this.eventPublisher = eventPublisher;
this.applicationContext = applicationContext;
this.afterCommit = afterCommit;
this.virtualPropertyReplacer = virtualPropertyReplacer;
this.txManager = txManager;
onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisher,
applicationContext, actionRepository, actionStatusRepository);
offlineDsAssignmentStrategy = new OfflineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisher,
applicationContext, actionRepository, actionStatusRepository);
}
@Autowired @Override
private DistributionSetRepository distributionSetRepository; @Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
@Autowired ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
private TargetRepository targetRepository; public DistributionSetAssignmentResult offlineAssignedDistributionSet(final Long dsID,
final Collection<String> controllerIDs) {
@Autowired return assignDistributionSetToTargets(dsID,
private ActionStatusRepository actionStatusRepository; controllerIDs.stream()
.map(controllerId -> new TargetWithActionType(controllerId, ActionType.FORCED, -1))
@Autowired .collect(Collectors.toList()),
private TargetManagement targetManagement; null, offlineDsAssignmentStrategy);
}
@Autowired
private AuditorAware<String> auditorProvider;
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private AfterTransactionCommitExecutor afterCommit;
@Autowired
private VirtualPropertyReplacer virtualPropertyReplacer;
@Autowired
private PlatformTransactionManager txManager;
@Override @Override
@Transactional(isolation = Isolation.READ_COMMITTED) @Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = { @Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public DistributionSetAssignmentResult assignDistributionSet(final Long dsID, final ActionType actionType, public DistributionSetAssignmentResult assignDistributionSet(final Long dsID, final ActionType actionType,
final long forcedTimestamp, final Collection<String> targetIDs) { final long forcedTimestamp, final Collection<String> controllerIDs) {
return assignDistributionSetToTargets(dsID, targetIDs.stream() return assignDistributionSetToTargets(dsID,
.map(t -> new TargetWithActionType(t, actionType, forcedTimestamp)).collect(Collectors.toList()), null); controllerIDs.stream()
.map(controllerId -> new TargetWithActionType(controllerId, actionType, forcedTimestamp))
.collect(Collectors.toList()),
null, onlineDsAssignmentStrategy);
} }
@@ -154,7 +169,7 @@ public class JpaDeploymentManagement implements DeploymentManagement {
public DistributionSetAssignmentResult assignDistributionSet(final Long dsID, public DistributionSetAssignmentResult assignDistributionSet(final Long dsID,
final Collection<TargetWithActionType> targets) { final Collection<TargetWithActionType> targets) {
return assignDistributionSetToTargets(dsID, targets, null); return assignDistributionSetToTargets(dsID, targets, null, onlineDsAssignmentStrategy);
} }
@Override @Override
@@ -164,12 +179,22 @@ public class JpaDeploymentManagement implements DeploymentManagement {
public DistributionSetAssignmentResult assignDistributionSet(final Long dsID, public DistributionSetAssignmentResult assignDistributionSet(final Long dsID,
final Collection<TargetWithActionType> targets, final String actionMessage) { final Collection<TargetWithActionType> targets, final String actionMessage) {
return assignDistributionSetToTargets(dsID, targets, actionMessage); return assignDistributionSetToTargets(dsID, targets, actionMessage, onlineDsAssignmentStrategy);
} }
/** /**
* method assigns the {@link DistributionSet} to all {@link Target}s by * method assigns the {@link DistributionSet} to all {@link Target}s by
* their IDs with a specific {@link ActionType} and {@code forcetime}. * their IDs with a specific {@link ActionType} and {@code forcetime}.
*
*
* In case the update was executed offline (i.e. not managed by hawkBit) the
* handling differs my means that:<br/>
* A. it ignores targets completely that are in
* {@link TargetUpdateStatus#PENDING}.<br/>
* B. it created completed actions.<br/>
* C. sets both installed and assigned DS on the target and switches the
* status to {@link TargetUpdateStatus#IN_SYNC} <br/>
* D. does not send a {@link TargetAssignDistributionSetEvent}.<br/>
* *
* @param dsID * @param dsID
* the ID of the distribution set to assign * the ID of the distribution set to assign
@@ -177,6 +202,8 @@ public class JpaDeploymentManagement implements DeploymentManagement {
* a list of all targets and their action type * a list of all targets and their action type
* @param actionMessage * @param actionMessage
* an optional message to be written into the action status * an optional message to be written into the action status
* @param offline
* to <code>true</code> in offline case
* @return the assignment result * @return the assignment result
* *
* @throw IncompleteDistributionSetException if mandatory * @throw IncompleteDistributionSetException if mandatory
@@ -184,7 +211,8 @@ public class JpaDeploymentManagement implements DeploymentManagement {
* {@link DistributionSetType}. * {@link DistributionSetType}.
*/ */
private DistributionSetAssignmentResult assignDistributionSetToTargets(final Long dsID, private DistributionSetAssignmentResult assignDistributionSetToTargets(final Long dsID,
final Collection<TargetWithActionType> targetsWithActionType, final String actionMessage) { final Collection<TargetWithActionType> targetsWithActionType, final String actionMessage,
final AbstractDsAssignmentStrategy assignmentStrategy) {
final JpaDistributionSet set = distributionSetRepository.findOne(dsID); final JpaDistributionSet set = distributionSetRepository.findOne(dsID);
if (set == null) { if (set == null) {
@@ -209,10 +237,7 @@ public class JpaDeploymentManagement implements DeploymentManagement {
// maximum 1000 elements, so we need to split the entries here and // maximum 1000 elements, so we need to split the entries here and
// execute multiple statements we take the target only into account if // execute multiple statements we take the target only into account if
// the requested operation is no duplicate of a previous one // the requested operation is no duplicate of a previous one
final List<JpaTarget> targets = Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream() final List<JpaTarget> targets = assignmentStrategy.findTargetsForAssignment(controllerIDs, set.getId());
.map(ids -> targetRepository
.findAll(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, set.getId())))
.flatMap(List::stream).collect(Collectors.toList());
if (targets.isEmpty()) { if (targets.isEmpty()) {
// detaching as it is not necessary to persist the set itself // detaching as it is not necessary to persist the set itself
@@ -229,8 +254,7 @@ public class JpaDeploymentManagement implements DeploymentManagement {
// need to remember which one we have been switched to canceling state // need to remember which one we have been switched to canceling state
// because for targets which we have changed to canceling we don't want // because for targets which we have changed to canceling we don't want
// to publish the new action update event. // to publish the new action update event.
final Set<Long> targetIdsCancellList = targetIds.stream().map(this::overrideObsoleteUpdateActions) final Set<Long> targetIdsCancellList = assignmentStrategy.findTargetIdsToCancel(targetIds);
.flatMap(Collection::stream).collect(Collectors.toSet());
// cancel all scheduled actions which are in-active, these actions were // cancel all scheduled actions which are in-active, these actions were
// not active before and the manual assignment which has been done // not active before and the manual assignment which has been done
@@ -245,24 +269,26 @@ public class JpaDeploymentManagement implements DeploymentManagement {
currentUser = null; currentUser = null;
} }
targetIds.forEach(tIds -> targetRepository.setAssignedDistributionSetAndUpdateStatus(TargetUpdateStatus.PENDING, assignmentStrategy.updateTargetStatus(set, targetIds, currentUser);
set, System.currentTimeMillis(), currentUser, tIds));
final Map<String, JpaAction> targetIdsToActions = targets.stream() final Map<String, JpaAction> targetIdsToActions = targets.stream()
.map(t -> actionRepository.save(createTargetAction(targetsWithActionMap, t, set))) .map(t -> actionRepository.save(assignmentStrategy.createTargetAction(targetsWithActionMap, t, set)))
.collect(Collectors.toMap(a -> a.getTarget().getControllerId(), Function.identity())); .collect(Collectors.toMap(a -> a.getTarget().getControllerId(), Function.identity()));
// create initial action status when action is created so we remember // create initial action status when action is created so we remember
// the initial running status because we will change the status // the initial running status because we will change the status
// of the action itself and with this action status we have a nicer // of the action itself and with this action status we have a nicer
// action history. // action history.
targetIdsToActions.values().forEach(action -> setRunningActionStatus(action, actionMessage)); actionStatusRepository.save(targetIdsToActions.values().stream()
.map(action -> assignmentStrategy.createActionStatus(action, actionMessage))
.collect(Collectors.toList()));
// detaching as it is not necessary to persist the set itself // detaching as it is not necessary to persist the set itself
entityManager.detach(set); entityManager.detach(set);
// detaching as the entity has been updated by the JPQL query above // detaching as the entity has been updated by the JPQL query above
targets.forEach(entityManager::detach); targets.forEach(entityManager::detach);
sendAssignmentEvents(targets, targetIdsCancellList, targetIdsToActions); assignmentStrategy.sendAssignmentEvents(targets, targetIdsCancellList, targetIdsToActions);
return new DistributionSetAssignmentResult( return new DistributionSetAssignmentResult(
targets.stream().map(Target::getControllerId).collect(Collectors.toList()), targets.size(), targets.stream().map(Target::getControllerId).collect(Collectors.toList()), targets.size(),
@@ -270,77 +296,6 @@ public class JpaDeploymentManagement implements DeploymentManagement {
targetManagement); targetManagement);
} }
private void sendAssignmentEvents(final List<JpaTarget> targets, final Set<Long> targetIdsCancellList,
final Map<String, JpaAction> targetIdsToActions) {
targets.forEach(target -> {
sendTargetUpdatedEvent(target);
if (targetIdsCancellList.contains(target.getId())) {
return;
}
sendTargetAssignDistributionSetEvent(targetIdsToActions.get(target.getControllerId()));
});
}
private static JpaAction createTargetAction(final Map<String, TargetWithActionType> targetsWithActionMap,
final JpaTarget target, final JpaDistributionSet set) {
final JpaAction actionForTarget = new JpaAction();
final TargetWithActionType targetWithActionType = targetsWithActionMap.get(target.getControllerId());
actionForTarget.setActionType(targetWithActionType.getActionType());
actionForTarget.setForcedTime(targetWithActionType.getForceTime());
actionForTarget.setActive(true);
actionForTarget.setStatus(Status.RUNNING);
actionForTarget.setTarget(target);
actionForTarget.setDistributionSet(set);
return actionForTarget;
}
private void sendTargetAssignDistributionSetEvent(final Action action) {
afterCommit.afterCommit(() -> eventPublisher
.publishEvent(new TargetAssignDistributionSetEvent(action, applicationContext.getId())));
}
private void sendTargetUpdatedEvent(final JpaTarget target) {
// Update is not available in the object as the update was executed
// through JQL
target.setUpdateStatus(TargetUpdateStatus.PENDING);
afterCommit.afterCommit(
() -> eventPublisher.publishEvent(new TargetUpdatedEvent(target, applicationContext.getId())));
}
/**
* Removes {@link Action}s that are no longer necessary and sends
* cancellations to the controller.
*
* @param targetsIds
* to override {@link Action}s
*/
private List<Long> overrideObsoleteUpdateActions(final Collection<Long> targetsIds) {
// Figure out if there are potential target/action combinations that
// need to be considered for cancellation
final List<JpaAction> activeActions = actionRepository
.findByActiveAndTargetIdInAndActionStatusNotEqualToAndDistributionSetRequiredMigrationStep(targetsIds,
Action.Status.CANCELING);
return activeActions.stream().map(action -> {
action.setStatus(Status.CANCELING);
// document that the status has been retrieved
actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELING, System.currentTimeMillis(),
RepositoryConstants.SERVER_MESSAGE_PREFIX + "cancel obsolete action due to new update"));
actionRepository.save(action);
cancelAssignDistributionSetEvent(action.getTarget(), action.getId());
return action.getTarget().getId();
}).collect(Collectors.toList());
}
@Override @Override
@Transactional(isolation = Isolation.READ_COMMITTED) @Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = { @Retryable(include = {
@@ -363,7 +318,7 @@ public class JpaDeploymentManagement implements DeploymentManagement {
actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELING, System.currentTimeMillis(), actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELING, System.currentTimeMillis(),
RepositoryConstants.SERVER_MESSAGE_PREFIX + "manual cancelation requested")); RepositoryConstants.SERVER_MESSAGE_PREFIX + "manual cancelation requested"));
final Action saveAction = actionRepository.save(action); final Action saveAction = actionRepository.save(action);
cancelAssignDistributionSetEvent(action.getTarget(), action.getId()); onlineDsAssignmentStrategy.cancelAssignDistributionSetEvent(action.getTarget(), action.getId());
return saveAction; return saveAction;
} else { } else {
@@ -371,20 +326,6 @@ public class JpaDeploymentManagement implements DeploymentManagement {
} }
} }
/**
* Sends the {@link CancelTargetAssignmentEvent} for a specific target to
* the eventPublisher.
*
* @param target
* the Target which has been assigned to a distribution set
* @param actionId
* the action id of the assignment
*/
private void cancelAssignDistributionSetEvent(final Target target, final Long actionId) {
afterCommit.afterCommit(() -> eventPublisher
.publishEvent(new CancelTargetAssignmentEvent(target, actionId, applicationContext.getId())));
}
@Override @Override
@Transactional(isolation = Isolation.READ_COMMITTED) @Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = { @Retryable(include = {
@@ -471,14 +412,14 @@ public class JpaDeploymentManagement implements DeploymentManagement {
} }
// check if we need to override running update actions // check if we need to override running update actions
final List<Long> overrideObsoleteUpdateActions = overrideObsoleteUpdateActions( final List<Long> overrideObsoleteUpdateActions = onlineDsAssignmentStrategy
Collections.singletonList(action.getTarget().getId())); .overrideObsoleteUpdateActions(Collections.singletonList(action.getTarget().getId()));
action.setActive(true); action.setActive(true);
action.setStatus(Status.RUNNING); action.setStatus(Status.RUNNING);
final JpaAction savedAction = actionRepository.save(action); final JpaAction savedAction = actionRepository.save(action);
setRunningActionStatus(savedAction, null); actionStatusRepository.save(onlineDsAssignmentStrategy.createActionStatus(savedAction, null));
target = (JpaTarget) entityManager.merge(savedAction.getTarget()); target = (JpaTarget) entityManager.merge(savedAction.getTarget());
@@ -494,18 +435,6 @@ public class JpaDeploymentManagement implements DeploymentManagement {
} }
} }
private void setRunningActionStatus(final JpaAction action, final String actionMessage) {
final JpaActionStatus actionStatus = new JpaActionStatus();
actionStatus.setAction(action);
actionStatus.setOccurredAt(action.getCreatedAt());
actionStatus.setStatus(Status.RUNNING);
if (actionMessage != null) {
actionStatus.addMessage(actionMessage);
}
actionStatusRepository.save(actionStatus);
}
private void setSkipActionStatus(final JpaAction action) { private void setSkipActionStatus(final JpaAction action) {
final JpaActionStatus actionStatus = new JpaActionStatus(); final JpaActionStatus actionStatus = new JpaActionStatus();
actionStatus.setAction(action); actionStatus.setAction(action);
@@ -683,4 +612,5 @@ public class JpaDeploymentManagement implements DeploymentManagement {
return distributionSetRepository.findInstalledAtTarget(controllerId); return distributionSetRepository.findInstalledAtTarget(controllerId);
} }
} }

View File

@@ -0,0 +1,96 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.jpa;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import com.google.common.collect.Lists;
/**
* AbstractDsAssignmentStrategy for offline assignments, i.e. not managed by
* hawkBit.
*
*/
public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
OfflineDsAssignmentStrategy(final TargetRepository targetRepository,
final AfterTransactionCommitExecutor afterCommit, final ApplicationEventPublisher eventPublisher,
final ApplicationContext applicationContext, final ActionRepository actionRepository,
final ActionStatusRepository actionStatusRepository) {
super(targetRepository, afterCommit, eventPublisher, applicationContext, actionRepository,
actionStatusRepository);
}
@Override
void sendAssignmentEvents(final List<JpaTarget> targets, final Set<Long> targetIdsCancellList,
final Map<String, JpaAction> targetIdsToActions) {
targets.forEach(target -> {
target.setUpdateStatus(TargetUpdateStatus.IN_SYNC);
sendTargetUpdatedEvent(target);
});
}
@Override
public List<JpaTarget> findTargetsForAssignment(final List<String> controllerIDs, final long setId) {
return Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream()
.map(ids -> targetRepository.findAll(SpecificationsBuilder.combineWithAnd(
Arrays.asList(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, setId),
TargetSpecifications.notEqualToTargetUpdateStatus(TargetUpdateStatus.PENDING)))))
.flatMap(List::stream).collect(Collectors.toList());
}
@Override
public Set<Long> findTargetIdsToCancel(final List<List<Long>> targetIds) {
return Collections.emptySet();
}
@Override
void updateTargetStatus(final JpaDistributionSet set, final List<List<Long>> targetIds, final String currentUser) {
targetIds.forEach(tIds -> targetRepository.setAssignedAndInstalledDistributionSetAndUpdateStatus(
TargetUpdateStatus.IN_SYNC, set, System.currentTimeMillis(), currentUser, tIds));
}
@Override
protected JpaAction createTargetAction(final Map<String, TargetWithActionType> targetsWithActionMap,
final JpaTarget target, final JpaDistributionSet set) {
final JpaAction result = super.createTargetAction(targetsWithActionMap, target, set);
result.setStatus(Status.FINISHED);
return result;
}
@Override
protected JpaActionStatus createActionStatus(final JpaAction action, final String actionMessage) {
final JpaActionStatus result = super.createActionStatus(action, actionMessage);
result.setStatus(Status.FINISHED);
result.addMessage(RepositoryConstants.SERVER_MESSAGE_PREFIX + "Action reported as offline deployment");
return result;
}
}

View File

@@ -0,0 +1,97 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.jpa;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import com.google.common.collect.Lists;
/**
* AbstractDsAssignmentStrategy for online assignments, i.e. managed by hawkBit.
*
*/
public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy {
OnlineDsAssignmentStrategy(final TargetRepository targetRepository,
final AfterTransactionCommitExecutor afterCommit, final ApplicationEventPublisher eventPublisher,
final ApplicationContext applicationContext, final ActionRepository actionRepository,
final ActionStatusRepository actionStatusRepository) {
super(targetRepository, afterCommit, eventPublisher, applicationContext, actionRepository,
actionStatusRepository);
}
@Override
void sendAssignmentEvents(final List<JpaTarget> targets, final Set<Long> targetIdsCancellList,
final Map<String, JpaAction> targetIdsToActions) {
targets.forEach(target -> {
target.setUpdateStatus(TargetUpdateStatus.PENDING);
sendTargetUpdatedEvent(target);
if (targetIdsCancellList.contains(target.getId())) {
return;
}
sendTargetAssignDistributionSetEvent(targetIdsToActions.get(target.getControllerId()));
});
}
@Override
List<JpaTarget> findTargetsForAssignment(final List<String> controllerIDs, final long setId) {
return Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream()
.map(ids -> targetRepository
.findAll(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, setId)))
.flatMap(List::stream).collect(Collectors.toList());
}
@Override
Set<Long> findTargetIdsToCancel(final List<List<Long>> targetIds) {
return targetIds.stream().map(this::overrideObsoleteUpdateActions).flatMap(Collection::stream)
.collect(Collectors.toSet());
}
@Override
void updateTargetStatus(final JpaDistributionSet set, final List<List<Long>> targetIds, final String currentUser) {
targetIds.forEach(tIds -> targetRepository.setAssignedDistributionSetAndUpdateStatus(TargetUpdateStatus.PENDING,
set, System.currentTimeMillis(), currentUser, tIds));
}
@Override
JpaAction createTargetAction(final Map<String, TargetWithActionType> targetsWithActionMap, final JpaTarget target,
final JpaDistributionSet set) {
final JpaAction result = super.createTargetAction(targetsWithActionMap, target, set);
result.setStatus(Status.RUNNING);
return result;
}
@Override
JpaActionStatus createActionStatus(final JpaAction action, final String actionMessage) {
final JpaActionStatus result = super.createActionStatus(action, actionMessage);
result.setStatus(Status.RUNNING);
return result;
}
}

View File

@@ -50,6 +50,7 @@ import org.eclipse.hawkbit.repository.jpa.builder.JpaSoftwareModuleBuilder;
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryBuilder; import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryBuilder;
import org.eclipse.hawkbit.repository.jpa.configuration.MultiTenantJpaTransactionManager; import org.eclipse.hawkbit.repository.jpa.configuration.MultiTenantJpaTransactionManager;
import org.eclipse.hawkbit.repository.jpa.event.JpaEventEntityManager; import org.eclipse.hawkbit.repository.jpa.event.JpaEventEntityManager;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.model.helper.AfterTransactionCommitExecutorHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.AfterTransactionCommitExecutorHolder;
import org.eclipse.hawkbit.repository.jpa.model.helper.EntityInterceptorHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.EntityInterceptorHolder;
import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.SecurityTokenGeneratorHolder;
@@ -87,6 +88,7 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource; import org.springframework.context.annotation.PropertySource;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.integration.support.locks.LockRegistry; import org.springframework.integration.support.locks.LockRegistry;
@@ -496,8 +498,16 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
*/ */
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
DeploymentManagement deploymentManagement() { DeploymentManagement deploymentManagement(final EntityManager entityManager,
return new JpaDeploymentManagement(); final ActionRepository actionRepository, final DistributionSetRepository distributionSetRepository,
final TargetRepository targetRepository, final ActionStatusRepository actionStatusRepository,
final TargetManagement targetManagement, final AuditorAware<String> auditorProvider,
final ApplicationEventPublisher eventPublisher, final ApplicationContext applicationContext,
final AfterTransactionCommitExecutor afterCommit, final VirtualPropertyReplacer virtualPropertyReplacer,
final PlatformTransactionManager txManager) {
return new JpaDeploymentManagement(entityManager, actionRepository, distributionSetRepository, targetRepository,
actionStatusRepository, targetManagement, auditorProvider, eventPublisher, applicationContext,
afterCommit, virtualPropertyReplacer, txManager);
} }
/** /**

View File

@@ -58,6 +58,29 @@ public interface TargetRepository extends BaseEntityRepository<JpaTarget, Long>,
@Param("set") JpaDistributionSet set, @Param("lastModifiedAt") Long modifiedAt, @Param("set") JpaDistributionSet set, @Param("lastModifiedAt") Long modifiedAt,
@Param("lastModifiedBy") String modifiedBy, @Param("targets") Collection<Long> targets); @Param("lastModifiedBy") String modifiedBy, @Param("targets") Collection<Long> targets);
/**
* Sets {@link JpaTarget#getAssignedDistributionSet()},
* {@link JpaTarget#getInstalledDistributionSet()} and
* {@link JpaTarget#getInstallationDate()}
*
* @param set
* to use
* @param status
* to set
* @param modifiedAt
* current time
* @param modifiedBy
* current auditor
* @param targets
* to update
*/
@Modifying
@Transactional
@Query("UPDATE JpaTarget t SET t.assignedDistributionSet = :set, t.installedDistributionSet = :set, t.installationDate = :lastModifiedAt, t.lastModifiedAt = :lastModifiedAt, t.lastModifiedBy = :lastModifiedBy, t.updateStatus = :status WHERE t.id IN :targets")
void setAssignedAndInstalledDistributionSetAndUpdateStatus(@Param("status") TargetUpdateStatus status,
@Param("set") JpaDistributionSet set, @Param("lastModifiedAt") Long modifiedAt,
@Param("lastModifiedBy") String modifiedBy, @Param("targets") Collection<Long> targets);
/** /**
* Loads {@link Target} by given ID. * Loads {@link Target} by given ID.
* *

View File

@@ -97,6 +97,19 @@ public final class TargetSpecifications {
return (targetRoot, query, cb) -> targetRoot.get(JpaTarget_.updateStatus).in(updateStatus); return (targetRoot, query, cb) -> targetRoot.get(JpaTarget_.updateStatus).in(updateStatus);
} }
/**
* {@link Specification} for retrieving {@link Target}s by "not equal to
* given {@link TargetUpdateStatus}".
*
* @param updateStatus
* to be filtered on
*
* @return the {@link Target} {@link Specification}
*/
public static Specification<JpaTarget> notEqualToTargetUpdateStatus(final TargetUpdateStatus updateStatus) {
return (targetRoot, query, cb) -> cb.not(cb.equal(targetRoot.get(JpaTarget_.updateStatus), updateStatus));
}
/** /**
* {@link Specification} for retrieving {@link Target}s that are overdue. A * {@link Specification} for retrieving {@link Target}s that are overdue. A
* target is overdue if it did not respond during the configured * target is overdue if it did not respond during the configured

View File

@@ -428,6 +428,38 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
return action; return action;
} }
@Test
@Description("Simple offline deployment of a distribution set to a list of targets. Verifies that offline assigment "
+ "is correctly executed for targets that do not have a running update already. Those are ignored.")
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 20),
@Expect(type = TargetUpdatedEvent.class, count = 20), @Expect(type = ActionCreatedEvent.class, count = 20),
@Expect(type = DistributionSetCreatedEvent.class, count = 2),
@Expect(type = SoftwareModuleCreatedEvent.class, count = 6),
@Expect(type = TargetAssignDistributionSetEvent.class, count = 10) })
public void assignedDistributionSet() {
final List<String> controllerIds = testdataFactory.createTargets(10).stream().map(Target::getControllerId)
.collect(Collectors.toList());
final List<Target> onlineAssignedTargets = testdataFactory.createTargets(10, "2");
controllerIds.addAll(onlineAssignedTargets.stream().map(Target::getControllerId).collect(Collectors.toList()));
final DistributionSet ds = testdataFactory.createDistributionSet();
assignDistributionSet(testdataFactory.createDistributionSet("2"), onlineAssignedTargets);
final long current = System.currentTimeMillis();
final List<Target> targets = deploymentManagement.offlineAssignedDistributionSet(ds.getId(), controllerIds)
.getAssignedEntity();
assertThat(actionRepository.count()).isEqualTo(20);
assertThat(targetManagement.findTargetByInstalledDistributionSet(ds.getId(), PAGE).getContent())
.containsAll(targets).hasSize(10)
.containsAll(targetManagement.findTargetByAssignedDistributionSet(ds.getId(), PAGE))
.as("InstallationDate set").allMatch(target -> target.getInstallationDate() >= current)
.as("TargetUpdateStatus IN_SYNC")
.allMatch(target -> TargetUpdateStatus.IN_SYNC.equals(target.getUpdateStatus()))
.as("InstallationDate equal to LastModifiedAt")
.allMatch(target -> target.getLastModifiedAt().equals(target.getInstallationDate()));
}
/** /**
* test a simple deployment by calling the * test a simple deployment by calling the
* {@link TargetRepository#assignDistributionSet(DistributionSet, Iterable)} * {@link TargetRepository#assignDistributionSet(DistributionSet, Iterable)}