diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java index 0816a1d7b..19b326d80 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpConfiguration.java @@ -354,10 +354,11 @@ public class AmqpConfiguration { final AmqpMessageSenderService amqpSenderService, final ArtifactUrlHandler artifactUrlHandler, final SystemSecurityContext systemSecurityContext, final SystemManagement systemManagement, final TargetManagement targetManagement, final DistributionSetManagement distributionSetManagement, - final SoftwareModuleManagement softwareModuleManagement, final DeploymentManagement deploymentManagement) { + final SoftwareModuleManagement softwareModuleManagement, final DeploymentManagement deploymentManagement, + final TenantConfigurationManagement tenantConfigurationManagement) { return new AmqpMessageDispatcherService(rabbitTemplate, amqpSenderService, artifactUrlHandler, systemSecurityContext, systemManagement, targetManagement, serviceMatcher, distributionSetManagement, - softwareModuleManagement, deploymentManagement); + softwareModuleManagement, deploymentManagement, tenantConfigurationManagement); } private static Map getTTLMaxArgsAuthenticationQueue() { diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java index 69d3af4bd..83a45ef8d 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherService.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.amqp; import static org.eclipse.hawkbit.repository.RepositoryConstants.MAX_ACTION_COUNT; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.BATCH_ASSIGNMENTS_ENABLED; import java.net.URI; import java.util.Collections; @@ -32,16 +33,19 @@ import org.eclipse.hawkbit.dmf.amqp.api.MessageType; import org.eclipse.hawkbit.dmf.json.model.DmfActionRequest; import org.eclipse.hawkbit.dmf.json.model.DmfArtifact; import org.eclipse.hawkbit.dmf.json.model.DmfArtifactHash; +import org.eclipse.hawkbit.dmf.json.model.DmfBatchDownloadAndUpdateRequest; import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest; import org.eclipse.hawkbit.dmf.json.model.DmfMetadata; import org.eclipse.hawkbit.dmf.json.model.DmfMultiActionRequest; import org.eclipse.hawkbit.dmf.json.model.DmfSoftwareModule; +import org.eclipse.hawkbit.dmf.json.model.DmfTarget; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.TenantConfigurationManagement; import org.eclipse.hawkbit.repository.event.remote.MultiActionEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; @@ -89,6 +93,7 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { private final DistributionSetManagement distributionSetManagement; private final DeploymentManagement deploymentManagement; private final SoftwareModuleManagement softwareModuleManagement; + private final TenantConfigurationManagement tenantConfigurationManagement; /** * Constructor. @@ -110,13 +115,17 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { * cluster node * @param distributionSetManagement * to retrieve modules + * @param tenantConfigurationManagement + * to access tenant configuration + * */ protected AmqpMessageDispatcherService(final RabbitTemplate rabbitTemplate, - final AmqpMessageSenderService amqpSenderService, final ArtifactUrlHandler artifactUrlHandler, - final SystemSecurityContext systemSecurityContext, final SystemManagement systemManagement, - final TargetManagement targetManagement, final ServiceMatcher serviceMatcher, - final DistributionSetManagement distributionSetManagement, - final SoftwareModuleManagement softwareModuleManagement, final DeploymentManagement deploymentManagement) { + final AmqpMessageSenderService amqpSenderService, final ArtifactUrlHandler artifactUrlHandler, + final SystemSecurityContext systemSecurityContext, final SystemManagement systemManagement, + final TargetManagement targetManagement, final ServiceMatcher serviceMatcher, + final DistributionSetManagement distributionSetManagement, + final SoftwareModuleManagement softwareModuleManagement, final DeploymentManagement deploymentManagement, + final TenantConfigurationManagement tenantConfigurationManagement) { super(rabbitTemplate); this.artifactUrlHandler = artifactUrlHandler; this.amqpSenderService = amqpSenderService; @@ -127,6 +136,7 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { this.distributionSetManagement = distributionSetManagement; this.softwareModuleManagement = softwareModuleManagement; this.deploymentManagement = deploymentManagement; + this.tenantConfigurationManagement = tenantConfigurationManagement; } /** @@ -182,8 +192,13 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { distributionSetManagement.get(assignedEvent.getDistributionSetId()).ifPresent(ds -> { final Map> softwareModules = getSoftwareModulesWithMetadata( ds); - targets.forEach(target -> sendUpdateMessageToTarget( - assignedEvent.getActions().get(target.getControllerId()), target, softwareModules)); + + if (!targets.isEmpty() && isBatchAssignmentsEnabled()) { + sendUpdateMessageToTargets(assignedEvent.getActions(), targets, softwareModules); + } else { + targets.forEach(target -> sendUpdateMessageToTarget( + assignedEvent.getActions().get(target.getControllerId()), target, softwareModules)); + } }); } @@ -500,4 +515,59 @@ public class AmqpMessageDispatcherService extends BaseAmqpService { PageRequest.of(0, RepositoryConstants.MAX_META_DATA_COUNT), module.getId()).getContent(); } + private void sendUpdateMessageToTargets(final Map actions, final List targets, + final Map> modules) { + + List dmfTargets = targets.stream().filter(target -> IpUtil.isAmqpUri(target.getAddress())) + .map(t -> convertToDmfTarget(t, actions.get(t.getControllerId()).getId())).collect(Collectors.toList()); + + final DmfBatchDownloadAndUpdateRequest batchRequest = new DmfBatchDownloadAndUpdateRequest(); + batchRequest.setTimestamp(System.currentTimeMillis()); + batchRequest.addTargets(dmfTargets); + + //due to the fact that all targets in a batch use the same set of software modules we don't generate + // target-specific urls + Target firstTarget = targets.get(0); + if (modules != null) { + modules.entrySet().forEach(entry -> + batchRequest.addSoftwareModule(convertToAmqpSoftwareModule(firstTarget, entry))); + } + + // we use only the first action when constructing message as Tenant and action type are the same + // since all actions have the same trigger + final ActionProperties firstAction = actions.values().iterator().next(); + final Message message = getMessageConverter().toMessage(batchRequest, + createMessagePropertiesBatch(firstAction.getTenant(), getBatchEventTopicForAction(firstAction))); + amqpSenderService.sendMessage(message, firstTarget.getAddress()); + } + + protected DmfTarget convertToDmfTarget(final Target target, final Long actionId) { + DmfTarget dmfTarget = new DmfTarget(); + dmfTarget.setActionId(actionId); + dmfTarget.setControllerId(target.getControllerId()); + dmfTarget.setTargetSecurityToken(systemSecurityContext.runAsSystem(target::getSecurityToken)); + return dmfTarget; + } + + private static MessageProperties createMessagePropertiesBatch(final String tenant, final EventTopic topic) { + final MessageProperties messageProperties = new MessageProperties(); + messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON); + messageProperties.setHeader(MessageHeaderKey.CONTENT_TYPE, MessageProperties.CONTENT_TYPE_JSON); + messageProperties.setHeader(MessageHeaderKey.TENANT, tenant); + + messageProperties.setHeader(MessageHeaderKey.TOPIC, topic); + messageProperties.setHeader(MessageHeaderKey.TYPE, MessageType.EVENT); + return messageProperties; + } + + public boolean isBatchAssignmentsEnabled() { + return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement + .getConfigurationValue(BATCH_ASSIGNMENTS_ENABLED, Boolean.class).getValue()); + } + + private static EventTopic getBatchEventTopicForAction(final ActionProperties action) { + return (Action.ActionType.DOWNLOAD_ONLY == action.getActionType() || !action.isMaintenanceWindowAvailable()) + ? EventTopic.BATCH_DOWNLOAD + : EventTopic.BATCH_DOWNLOAD_AND_INSTALL; + } } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java index 7e12d189a..2a9587b33 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java @@ -111,7 +111,7 @@ class AmqpMessageDispatcherServiceTest extends AbstractIntegrationTest { amqpMessageDispatcherService = new AmqpMessageDispatcherService(rabbitTemplate, senderService, artifactUrlHandlerMock, systemSecurityContext, systemManagement, targetManagement, serviceMatcher, - distributionSetManagement, softwareModuleManagement, deploymentManagement); + distributionSetManagement, softwareModuleManagement, deploymentManagement, tenantConfigurationManagement); } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java index 4a9d0b5d2..684eac692 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java @@ -12,7 +12,9 @@ import static org.assertj.core.api.Assertions.assertThat; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -32,6 +34,7 @@ import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate; import org.eclipse.hawkbit.dmf.json.model.DmfCreateThing; import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest; import org.eclipse.hawkbit.dmf.json.model.DmfMetadata; +import org.eclipse.hawkbit.dmf.json.model.DmfSoftwareModule; import org.eclipse.hawkbit.integration.listener.DeadletterListener; import org.eclipse.hawkbit.integration.listener.ReplyToListener; import org.eclipse.hawkbit.matcher.SoftwareModuleJsonMatcher; @@ -95,7 +98,7 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt getDmfClient().setExchange(AmqpSettings.DMF_EXCHANGE); } - private T waitUntilIsPresent(final Callable> callable) { + protected T waitUntilIsPresent(final Callable> callable) { createConditionFactory() .until(() -> WithSpringAuthorityRule.runAsPrivileged(() -> callable.call().isPresent())); @@ -194,10 +197,7 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt protected void assertDmfDownloadAndUpdateRequest(final DmfDownloadAndUpdateRequest request, final Set softwareModules, final String controllerId) { - assertThat(softwareModules) - .is(new HamcrestCondition<>(SoftwareModuleJsonMatcher.containsExactly(request.getSoftwareModules()))); - request.getSoftwareModules().forEach(dmfModule -> assertThat(dmfModule.getMetadata()).containsExactly( - new DmfMetadata(TestdataFactory.VISIBLE_SM_MD_KEY, TestdataFactory.VISIBLE_SM_MD_VALUE))); + assertSoftwareModules(softwareModules, request.getSoftwareModules()); final Target updatedTarget = waitUntilIsPresent(() -> targetManagement.getByControllerID(controllerId)); assertThat(updatedTarget).isNotNull(); assertThat(updatedTarget.getSecurityToken()).isEqualTo(request.getTargetSecurityToken()); @@ -445,4 +445,11 @@ public abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpInt return distributionSet; } + protected void assertSoftwareModules(final Set expectedSoftwareModules, + final List softwareModules) { + assertThat(expectedSoftwareModules) + .is(new HamcrestCondition<>(SoftwareModuleJsonMatcher.containsExactly(softwareModules))); + softwareModules.forEach(dmfModule -> assertThat(dmfModule.getMetadata()).containsExactly( + new DmfMetadata(TestdataFactory.VISIBLE_SM_MD_KEY, TestdataFactory.VISIBLE_SM_MD_VALUE))); + } } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java index cb4cba8ad..062eaffa1 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java @@ -9,9 +9,13 @@ package org.eclipse.hawkbit.integration; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.eclipse.hawkbit.dmf.amqp.api.EventTopic.BATCH_DOWNLOAD; +import static org.eclipse.hawkbit.dmf.amqp.api.EventTopic.BATCH_DOWNLOAD_AND_INSTALL; import static org.eclipse.hawkbit.dmf.amqp.api.EventTopic.DOWNLOAD; import static org.eclipse.hawkbit.dmf.amqp.api.MessageType.EVENT; import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY; +import static org.eclipse.hawkbit.repository.model.Action.ActionType.FORCED; import java.util.AbstractMap.SimpleEntry; import java.util.Arrays; @@ -29,10 +33,12 @@ import org.eclipse.hawkbit.dmf.amqp.api.EventTopic; import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; import org.eclipse.hawkbit.dmf.json.model.DmfActionRequest; import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus; +import org.eclipse.hawkbit.dmf.json.model.DmfBatchDownloadAndUpdateRequest; import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest; import org.eclipse.hawkbit.dmf.json.model.DmfMultiActionRequest; import org.eclipse.hawkbit.dmf.json.model.DmfMultiActionRequest.DmfMultiActionElement; import org.eclipse.hawkbit.dmf.json.model.DmfSoftwareModule; +import org.eclipse.hawkbit.dmf.json.model.DmfTarget; import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.event.remote.MultiActionAssignEvent; import org.eclipse.hawkbit.repository.event.remote.MultiActionCancelEvent; @@ -53,6 +59,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedE import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TenantConfigurationCreatedEvent; +import org.eclipse.hawkbit.repository.exception.TenantConfigurationValueChangeNotAllowedException; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -66,6 +73,8 @@ import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.Mockito; import org.springframework.amqp.core.Message; @@ -603,4 +612,87 @@ public class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpSer || multiActionElement.getTopic().equals(EventTopic.DOWNLOAD_AND_INSTALL); } + @Test + @Description("Verify payload of batch assignment download and install message.") + public void assertBatchAssignmentsDownloadAndInstall() { + assertBatchAssignmentsMessagePayload(BATCH_DOWNLOAD_AND_INSTALL); + } + + @Test + @Description("Verify payload of batch assignments download only message.") + public void assertBatchAssignmentsDownloadOnly() { + assertBatchAssignmentsMessagePayload(BATCH_DOWNLOAD); + } + + @Test + @Description("Verify that batch and multi-assignments can't be activated at the same time.") + void assertBatchAndMultiAssignmentsNotCompatible() { + enableBatchAssignments(); + assertThatExceptionOfType(TenantConfigurationValueChangeNotAllowedException.class) + .isThrownBy(() -> enableMultiAssignments()); + disableBatchAssignments(); + + enableMultiAssignments(); + assertThatExceptionOfType(TenantConfigurationValueChangeNotAllowedException.class) + .isThrownBy(() -> enableBatchAssignments()); + } + + @ParameterizedTest + @EnumSource(names = { "BATCH_DOWNLOAD_AND_INSTALL", "BATCH_DOWNLOAD" }) + @Description("Verify payload of batch assignments.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 3), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 3), @Expect(type = ActionUpdatedEvent.class, count = 0), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3), + @Expect(type = SoftwareModuleUpdatedEvent.class, count = 6), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 3), @Expect(type = TargetPollEvent.class, count = 3), + @Expect(type = TenantConfigurationCreatedEvent.class, count = 1) }) + void assertBatchAssignmentsMessagePayload(final EventTopic topic) { + enableBatchAssignments(); + + final List targets = Arrays.asList("batchCtrlID1", "batchCtrlID2", "batchCtrlID3"); + for (int i = 0; i < targets.size(); i++) { + registerAndAssertTargetWithExistingTenant(targets.get(i), i+1); + } + + final DistributionSet ds = testdataFactory.createDistributionSet(); + testdataFactory.addSoftwareModuleMetadata(ds); + + assignDistributionSet(ds.getId(), targets, topic == BATCH_DOWNLOAD?DOWNLOAD_ONLY:FORCED); + + waitUntilEventMessagesAreDispatchedToTarget(topic); + + final Message message = replyToListener.getLatestEventMessage(topic); + final Map headers = message.getMessageProperties().getHeaders(); + assertThat(headers).containsEntry("type", EVENT.toString()); + assertThat(headers).containsEntry("topic",topic.toString()); + + final DmfBatchDownloadAndUpdateRequest batchRequest = (DmfBatchDownloadAndUpdateRequest) getDmfClient() + .getMessageConverter().fromMessage(message); + + assertThat(batchRequest).isExactlyInstanceOf(DmfBatchDownloadAndUpdateRequest.class); + assertDmfBatchDownloadAndUpdateRequest(batchRequest, ds.getModules(), targets); + } + + protected void assertDmfBatchDownloadAndUpdateRequest(final DmfBatchDownloadAndUpdateRequest request, + final Set softwareModules, + final List controllerIds) { + assertSoftwareModules(softwareModules, request.getSoftwareModules()); + + List tokens = controllerIds.stream().map(controllerId -> { + final Optional target = controllerManagement.getByControllerId(controllerId); + assertThat(target).isPresent(); + return target.get().getSecurityToken(); + }).collect(Collectors.toList()); + + + List requestTargets = request.getTargets(); + + assertThat(requestTargets).hasSameSizeAs(controllerIds); + requestTargets.forEach(requestTarget -> { + assertThat(requestTarget).isNotNull(); + assertThat(tokens.contains(requestTarget.getTargetSecurityToken())); + }); + } } diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/EventTopic.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/EventTopic.java index 480a20302..668f9d4b7 100644 --- a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/EventTopic.java +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/amqp/api/EventTopic.java @@ -46,6 +46,16 @@ public enum EventTopic { /** * Topic to send multiple actions to the device. */ - MULTI_ACTION + MULTI_ACTION, + + /** + * Topic when sending a download only action to multiple devices. + */ + BATCH_DOWNLOAD, + + /** + * Topic when sending a download and install action to multiple devices. + */ + BATCH_DOWNLOAD_AND_INSTALL } diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfBatchDownloadAndUpdateRequest.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfBatchDownloadAndUpdateRequest.java new file mode 100644 index 000000000..3280a4ccb --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfBatchDownloadAndUpdateRequest.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2022 Bosch.IO 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.dmf.json.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * JSON representation of batch download and update request. + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class DmfBatchDownloadAndUpdateRequest { + + @JsonProperty + private Long timestamp; + + @JsonProperty + private List targets; + + @JsonProperty + private List softwareModules; + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(final Long timestamp) { + this.timestamp = timestamp; + } + + public List getSoftwareModules() { + if (softwareModules == null) { + return Collections.emptyList(); + } + + return Collections.unmodifiableList(softwareModules); + } + + /** + * Add a Software module. + * + * @param createSoftwareModule + * the module + */ + public void addSoftwareModule(final DmfSoftwareModule createSoftwareModule) { + if (softwareModules == null) { + softwareModules = new ArrayList<>(); + } + + softwareModules.add(createSoftwareModule); + } + + public List getTargets() { + if (targets == null) { + return Collections.emptyList(); + } + + return Collections.unmodifiableList(targets); + } + + + /** + * Add a Target. + * + * @param target + * the target + */ + public void addTarget(final DmfTarget target) { + if (targets == null) { + targets = new ArrayList<>(); + } + + targets.add(target); + } + + /** + * Add multiple Targets. + * + * @param targets + * the target + */ + public void addTargets(final List targets) { + if (this.targets == null) { + this.targets = new ArrayList<>(); + } + this.targets.addAll(targets); + } +} diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfTarget.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfTarget.java new file mode 100644 index 000000000..4f29b5741 --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfTarget.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2022 Bosch.IO 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.dmf.json.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + + +/** + * Json representation of Target used in batch download and update request. + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class DmfTarget { + + @JsonProperty + private Long actionId; + + @JsonProperty + private String controllerId; + + @JsonProperty + private String targetSecurityToken; + + public Long getActionId() { + return actionId; + } + + public void setActionId(final Long actionId) { + this.actionId = actionId; + } + + public String getControllerId() { + return controllerId; + } + + public void setControllerId(final String controllerId) { + this.controllerId = controllerId; + } + + public String getTargetSecurityToken() { + return targetSecurityToken; + } + + public void setTargetSecurityToken(final String targetSecurityToken) { + this.targetSecurityToken = targetSecurityToken; + } + + @Override + public String toString() { + return String.format( + "DmfTarget [actionId=%d controllerId='%s']", + actionId, controllerId); + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java index 628b67f91..c49cd65d9 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java @@ -147,6 +147,11 @@ public class TenantConfigurationProperties { */ public static final String MULTI_ASSIGNMENTS_ENABLED = "multi.assignments.enabled"; + /** + * Switch to enable/disable the batch-assignment feature. + */ + public static final String BATCH_ASSIGNMENTS_ENABLED = "batch.assignments.enabled"; + private String keyName; private String defaultValue = ""; private Class dataType = String.class; diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties b/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties index 8b8d99808..912c27ee7 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties +++ b/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties @@ -96,5 +96,10 @@ hawkbit.server.tenant.configuration.multi-assignments-enabled.defaultValue=false hawkbit.server.tenant.configuration.multi-assignments-enabled.dataType=java.lang.Boolean hawkbit.server.tenant.configuration.multi-assignments-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator +hawkbit.server.tenant.configuration.batch-assignments-enabled.keyName=batch.assignments.enabled +hawkbit.server.tenant.configuration.batch-assignments-enabled.defaultValue=false +hawkbit.server.tenant.configuration.batch-assignments-enabled.dataType=java.lang.Boolean +hawkbit.server.tenant.configuration.batch-assignments-enabled.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationBooleanValidator + # Default tenant configuration - END diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantConfigurationManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantConfigurationManagement.java index 22eec469d..4c453bf6c 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantConfigurationManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantConfigurationManagement.java @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.repository.jpa; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.BATCH_ASSIGNMENTS_ENABLED; import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED; import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED; @@ -177,9 +178,9 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana * Asserts that the requested configuration value change is allowed. Throws * a {@link TenantConfigurationValueChangeNotAllowedException} otherwise. * - * @param configurationKeyName + * @param key * The configuration key. - * @param tenantConfiguration + * @param valueChange * The configuration to be validated. * * @throws TenantConfigurationValueChangeNotAllowedException @@ -188,6 +189,7 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana private void assertValueChangeIsAllowed(final String key, final JpaTenantConfiguration valueChange) { assertMultiAssignmentsValueChange(key, valueChange); assertAutoCloseValueChange(key, valueChange); + assertBatchAssignmentValueChange(key, valueChange); } @SuppressWarnings("squid:S1172") @@ -201,11 +203,30 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana } } - private static void assertMultiAssignmentsValueChange(final String key, final JpaTenantConfiguration valueChange) { + private void assertMultiAssignmentsValueChange(final String key, final JpaTenantConfiguration valueChange) { if (MULTI_ASSIGNMENTS_ENABLED.equals(key) && !Boolean.parseBoolean(valueChange.getValue())) { LOG.debug("The Multi-Assignments '{}' feature cannot be disabled.", key); throw new TenantConfigurationValueChangeNotAllowedException(); } + if (MULTI_ASSIGNMENTS_ENABLED.equals(key) && Boolean.parseBoolean(valueChange.getValue())) { + JpaTenantConfiguration batchConfig = tenantConfigurationRepository.findByKey(BATCH_ASSIGNMENTS_ENABLED); + if (batchConfig!=null && Boolean.parseBoolean(batchConfig.getValue())) { + LOG.debug("The Multi-Assignments '{}' feature cannot be enabled as it contradicts with " + + "The Batch-Assignments feature, which is already enabled .", key); + throw new TenantConfigurationValueChangeNotAllowedException(); + } + } + } + + private void assertBatchAssignmentValueChange(final String key, final JpaTenantConfiguration valueChange) { + if (BATCH_ASSIGNMENTS_ENABLED.equals(key) && Boolean.parseBoolean(valueChange.getValue())) { + JpaTenantConfiguration multiConfig = tenantConfigurationRepository.findByKey(MULTI_ASSIGNMENTS_ENABLED); + if (multiConfig != null && Boolean.parseBoolean(multiConfig.getValue())) { + LOG.debug("The Batch-Assignments '{}' feature cannot be enabled as it contradicts with " + + "The Multi-Assignments feature, which is already enabled .", key); + throw new TenantConfigurationValueChangeNotAllowedException(); + } + } } @Override diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java index 762168f7c..dcb2ac0a9 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java @@ -465,4 +465,12 @@ public abstract class AbstractIntegrationTest { protected static Comparator controllerIdComparator() { return (o1, o2) -> o1.getControllerId().equals(o2.getControllerId()) ? 0 : 1; } + + protected void enableBatchAssignments() { + tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.BATCH_ASSIGNMENTS_ENABLED, true); + } + + protected void disableBatchAssignments() { + tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.BATCH_ASSIGNMENTS_ENABLED, false); + } } diff --git a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java index 6c61ac1d0..442d1d908 100644 --- a/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java +++ b/hawkbit-rest/hawkbit-rest-docs/src/test/java/org/eclipse/hawkbit/rest/mgmt/documentation/TenantResourceDocumentationTest.java @@ -89,6 +89,8 @@ public class TenantResourceDocumentationTest extends AbstractApiRestDocumentatio "the expiry time in milliseconds that needs to elapse before an action may be cleaned up."); CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED, "if multiple distribution sets can be assigned to the same targets."); + CONFIG_ITEM_DESCRIPTIONS.put(TenantConfigurationKey.BATCH_ASSIGNMENTS_ENABLED, + "if distribution set can be assigned to multiple targets in a single batch message."); } @Autowired