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

@@ -14,6 +14,7 @@ import java.util.Optional;
import javax.validation.constraints.NotNull;
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.EntityNotFoundException;
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.SoftwareModuleType;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.data.domain.Page;
@@ -69,7 +71,6 @@ public interface DeploymentManagement {
DistributionSetAssignmentResult assignDistributionSet(@NotNull Long dsID, @NotNull ActionType actionType,
long forcedTimestamp, @NotEmpty Collection<String> controllerIDs);
/**
* method assigns the {@link DistributionSet} to all {@link Target}s by
* their IDs with a specific {@link ActionType} and {@code forcetime}.
@@ -116,6 +117,38 @@ public interface DeploymentManagement {
DistributionSetAssignmentResult assignDistributionSet(@NotNull Long dsID,
@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
* 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.TargetManagement;
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.EntityNotFoundException;
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.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.ConcurrencyFailureException;
@@ -99,51 +95,70 @@ public class JpaDeploymentManagement implements DeploymentManagement {
*/
private static final int ACTION_PAGE_LIMIT = 1000;
@Autowired
private EntityManager entityManager;
private final 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
private ActionRepository actionRepository;
JpaDeploymentManagement(final EntityManager entityManager, 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) {
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
private DistributionSetRepository distributionSetRepository;
@Autowired
private TargetRepository targetRepository;
@Autowired
private ActionStatusRepository actionStatusRepository;
@Autowired
private TargetManagement targetManagement;
@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
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public DistributionSetAssignmentResult offlineAssignedDistributionSet(final Long dsID,
final Collection<String> controllerIDs) {
return assignDistributionSetToTargets(dsID,
controllerIDs.stream()
.map(controllerId -> new TargetWithActionType(controllerId, ActionType.FORCED, -1))
.collect(Collectors.toList()),
null, offlineDsAssignmentStrategy);
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
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()
.map(t -> new TargetWithActionType(t, actionType, forcedTimestamp)).collect(Collectors.toList()), null);
return assignDistributionSetToTargets(dsID,
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,
final Collection<TargetWithActionType> targets) {
return assignDistributionSetToTargets(dsID, targets, null);
return assignDistributionSetToTargets(dsID, targets, null, onlineDsAssignmentStrategy);
}
@Override
@@ -164,12 +179,22 @@ public class JpaDeploymentManagement implements DeploymentManagement {
public DistributionSetAssignmentResult assignDistributionSet(final Long dsID,
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
* 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
* 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
* @param actionMessage
* an optional message to be written into the action status
* @param offline
* to <code>true</code> in offline case
* @return the assignment result
*
* @throw IncompleteDistributionSetException if mandatory
@@ -184,7 +211,8 @@ public class JpaDeploymentManagement implements DeploymentManagement {
* {@link DistributionSetType}.
*/
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);
if (set == null) {
@@ -209,10 +237,7 @@ public class JpaDeploymentManagement implements DeploymentManagement {
// maximum 1000 elements, so we need to split the entries here and
// execute multiple statements we take the target only into account if
// the requested operation is no duplicate of a previous one
final List<JpaTarget> targets = Lists.partition(controllerIDs, Constants.MAX_ENTRIES_IN_STATEMENT).stream()
.map(ids -> targetRepository
.findAll(TargetSpecifications.hasControllerIdAndAssignedDistributionSetIdNot(ids, set.getId())))
.flatMap(List::stream).collect(Collectors.toList());
final List<JpaTarget> targets = assignmentStrategy.findTargetsForAssignment(controllerIDs, set.getId());
if (targets.isEmpty()) {
// 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
// because for targets which we have changed to canceling we don't want
// to publish the new action update event.
final Set<Long> targetIdsCancellList = targetIds.stream().map(this::overrideObsoleteUpdateActions)
.flatMap(Collection::stream).collect(Collectors.toSet());
final Set<Long> targetIdsCancellList = assignmentStrategy.findTargetIdsToCancel(targetIds);
// cancel all scheduled actions which are in-active, these actions were
// not active before and the manual assignment which has been done
@@ -245,24 +269,26 @@ public class JpaDeploymentManagement implements DeploymentManagement {
currentUser = null;
}
targetIds.forEach(tIds -> targetRepository.setAssignedDistributionSetAndUpdateStatus(TargetUpdateStatus.PENDING,
set, System.currentTimeMillis(), currentUser, tIds));
assignmentStrategy.updateTargetStatus(set, targetIds, currentUser);
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()));
// create initial action status when action is created so we remember
// the initial running status because we will change the status
// of the action itself and with this action status we have a nicer
// 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
entityManager.detach(set);
// detaching as the entity has been updated by the JPQL query above
targets.forEach(entityManager::detach);
sendAssignmentEvents(targets, targetIdsCancellList, targetIdsToActions);
assignmentStrategy.sendAssignmentEvents(targets, targetIdsCancellList, targetIdsToActions);
return new DistributionSetAssignmentResult(
targets.stream().map(Target::getControllerId).collect(Collectors.toList()), targets.size(),
@@ -270,77 +296,6 @@ public class JpaDeploymentManagement implements DeploymentManagement {
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
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
@@ -363,7 +318,7 @@ public class JpaDeploymentManagement implements DeploymentManagement {
actionStatusRepository.save(new JpaActionStatus(action, Status.CANCELING, System.currentTimeMillis(),
RepositoryConstants.SERVER_MESSAGE_PREFIX + "manual cancelation requested"));
final Action saveAction = actionRepository.save(action);
cancelAssignDistributionSetEvent(action.getTarget(), action.getId());
onlineDsAssignmentStrategy.cancelAssignDistributionSetEvent(action.getTarget(), action.getId());
return saveAction;
} 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
@Transactional(isolation = Isolation.READ_COMMITTED)
@Retryable(include = {
@@ -471,14 +412,14 @@ public class JpaDeploymentManagement implements DeploymentManagement {
}
// check if we need to override running update actions
final List<Long> overrideObsoleteUpdateActions = overrideObsoleteUpdateActions(
Collections.singletonList(action.getTarget().getId()));
final List<Long> overrideObsoleteUpdateActions = onlineDsAssignmentStrategy
.overrideObsoleteUpdateActions(Collections.singletonList(action.getTarget().getId()));
action.setActive(true);
action.setStatus(Status.RUNNING);
final JpaAction savedAction = actionRepository.save(action);
setRunningActionStatus(savedAction, null);
actionStatusRepository.save(onlineDsAssignmentStrategy.createActionStatus(savedAction, null));
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) {
final JpaActionStatus actionStatus = new JpaActionStatus();
actionStatus.setAction(action);
@@ -683,4 +612,5 @@ public class JpaDeploymentManagement implements DeploymentManagement {
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.configuration.MultiTenantJpaTransactionManager;
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.EntityInterceptorHolder;
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.Profile;
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.EnableJpaRepositories;
import org.springframework.integration.support.locks.LockRegistry;
@@ -496,8 +498,16 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
*/
@Bean
@ConditionalOnMissingBean
DeploymentManagement deploymentManagement() {
return new JpaDeploymentManagement();
DeploymentManagement deploymentManagement(final EntityManager entityManager,
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("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.
*

View File

@@ -97,6 +97,19 @@ public final class TargetSpecifications {
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
* target is overdue if it did not respond during the configured

View File

@@ -428,6 +428,38 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
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
* {@link TargetRepository#assignDistributionSet(DistributionSet, Iterable)}