Code format hawkbit (#1948)

Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-11-05 11:41:56 +02:00
committed by GitHub
parent 3e469fa58c
commit d842bc2aaa
108 changed files with 17957 additions and 12571 deletions

View File

@@ -9,6 +9,17 @@
*/
package org.eclipse.hawkbit.sdk.device;
import java.time.LocalTime;
import java.time.temporal.ChronoField;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@@ -28,17 +39,6 @@ import org.springframework.hateoas.Link;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.time.LocalTime;
import java.time.temporal.ChronoField;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Class representing DDI device connecting directly to hawkBit.
*/
@@ -81,10 +81,10 @@ public class DdiController {
* @param tenant the tenant of the device belongs to
* @param controller the controller
* @param hawkbitClient a factory for creating to {@link DdiRootControllerRestApi} (and used)
* for communication to hawkBit
* for communication to hawkBit
*/
public DdiController(final Tenant tenant, final Controller controller,
final UpdateHandler updateHandler, final HawkbitClient hawkbitClient) {
final UpdateHandler updateHandler, final HawkbitClient hawkbitClient) {
this.tenantId = tenant.getTenantId();
gatewayToken = tenant.getGatewayToken();
downloadAuthenticationEnabled = tenant.isDownloadAuthenticationEnabled();
@@ -112,58 +112,88 @@ public class DdiController {
currentActionId = null;
}
public void updateAttribute(final String mode, final String key, final String value) {
final DdiUpdateMode updateMode = switch (mode.toLowerCase()) {
case "replace" -> DdiUpdateMode.REPLACE;
case "remove" -> DdiUpdateMode.REMOVE;
default -> DdiUpdateMode.MERGE;
};
final DdiConfigData configData = new DdiConfigData(Collections.singletonMap(key, value), updateMode);
getDdiApi().putConfigData(configData, getTenantId(), getControllerId());
}
void sendFeedback(final UpdateStatus updateStatus) {
log.debug(LOG_PREFIX + "Send feedback {} -> {}", getTenantId(), getControllerId(), currentActionId, updateStatus);
try {
getDdiApi().postDeploymentBaseActionFeedback(updateStatus.feedback(), getTenantId(), getControllerId(),
currentActionId);
} catch (final RuntimeException e) {
log.error(LOG_PREFIX + "Failed to send feedback {} -> {}", getTenantId(), getControllerId(),
currentActionId, updateStatus, e);
}
if (updateStatus.status() == UpdateStatus.Status.SUCCESSFUL ||
updateStatus.status() == UpdateStatus.Status.FAILURE) {
lastActionId = currentActionId;
currentActionId = null;
}
}
private void poll() {
log.debug(LOG_PREFIX + " Polling ...", tenantId, controllerId);
Optional.ofNullable(executorService).ifPresent(executor ->
getControllerBase().ifPresentOrElse(
controllerBase -> {
final Optional<Link> confirmationBaseLink = getRequiredLink(controllerBase, CONFIRMATION_BASE_LINK);
if (confirmationBaseLink.isPresent()) {
final long actionId = getActionId(confirmationBaseLink.get());
log.info(LOG_PREFIX + "Confirmation is required for action {}!", getTenantId(),
getControllerId(), actionId);
// TODO - confirmation handler
sendConfirmationFeedback(actionId);
executor.schedule(this::poll, IMMEDIATE_MS, TimeUnit.MILLISECONDS);
} else {
getRequiredLink(controllerBase, DEPLOYMENT_BASE_LINK).flatMap(this::getActionWithDeployment).ifPresentOrElse(actionWithDeployment -> {
final long actionId = actionWithDeployment.getKey();
if (currentActionId == null) {
if (lastActionId != null && lastActionId == actionId) {
log.info(LOG_PREFIX + "Still receive the last action {}",
getTenantId(), getControllerId(), actionId);
return;
}
getControllerBase().ifPresentOrElse(
controllerBase -> {
final Optional<Link> confirmationBaseLink = getRequiredLink(controllerBase, CONFIRMATION_BASE_LINK);
if (confirmationBaseLink.isPresent()) {
final long actionId = getActionId(confirmationBaseLink.get());
log.info(LOG_PREFIX + "Confirmation is required for action {}!", getTenantId(),
getControllerId(), actionId);
// TODO - confirmation handler
sendConfirmationFeedback(actionId);
executor.schedule(this::poll, IMMEDIATE_MS, TimeUnit.MILLISECONDS);
} else {
getRequiredLink(controllerBase, DEPLOYMENT_BASE_LINK).flatMap(this::getActionWithDeployment)
.ifPresentOrElse(actionWithDeployment -> {
final long actionId = actionWithDeployment.getKey();
if (currentActionId == null) {
if (lastActionId != null && lastActionId == actionId) {
log.info(LOG_PREFIX + "Still receive the last action {}",
getTenantId(), getControllerId(), actionId);
return;
}
log.info(LOG_PREFIX + "Process action {}", getTenantId(), getControllerId(),
actionId);
final DdiDeployment deployment = actionWithDeployment.getValue().getDeployment();
final DdiDeployment.HandlingType updateType = deployment.getUpdate();
final List<DdiChunk> modules = deployment.getChunks();
log.info(LOG_PREFIX + "Process action {}", getTenantId(), getControllerId(),
actionId);
final DdiDeployment deployment = actionWithDeployment.getValue().getDeployment();
final DdiDeployment.HandlingType updateType = deployment.getUpdate();
final List<DdiChunk> modules = deployment.getChunks();
currentActionId = actionId;
executor.submit(
updateHandler.getUpdateProcessor(this, updateType, modules));
} else if (currentActionId != actionId) {
// TODO - cancel and start new one?
log.info(LOG_PREFIX + "Action {} is canceled while in process (new {})!", getTenantId(),
getControllerId(), currentActionId, actionId);
} // else same action - already processing
}, () -> {
if (currentActionId != null) {
// TODO - cancel current?
log.info(LOG_PREFIX + "Action {} is canceled while in process (not returned)!", getTenantId(),
getControllerId(), getCurrentActionId());
}
});
executor.schedule(this::poll, getPollMillis(controllerBase), TimeUnit.MILLISECONDS);
currentActionId = actionId;
executor.submit(
updateHandler.getUpdateProcessor(this, updateType, modules));
} else if (currentActionId != actionId) {
// TODO - cancel and start new one?
log.info(LOG_PREFIX + "Action {} is canceled while in process (new {})!", getTenantId(),
getControllerId(), currentActionId, actionId);
} // else same action - already processing
}, () -> {
if (currentActionId != null) {
// TODO - cancel current?
log.info(LOG_PREFIX + "Action {} is canceled while in process (not returned)!", getTenantId(),
getControllerId(), getCurrentActionId());
}
});
executor.schedule(this::poll, getPollMillis(controllerBase), TimeUnit.MILLISECONDS);
}
},
() -> {
// error has occurred or no controller base hasn't been acquired
executor.schedule(this::poll, DEFAULT_POLL_MS, TimeUnit.MILLISECONDS);
}
},
() -> {
// error has occurred or no controller base hasn't been acquired
executor.schedule(this::poll, DEFAULT_POLL_MS, TimeUnit.MILLISECONDS);
}
));
));
}
private Optional<DdiControllerBase> getControllerBase() {
@@ -211,42 +241,14 @@ public class DdiController {
final ResponseEntity<DdiDeploymentBase> action = getDdiApi()
.getControllerDeploymentBaseAction(getTenantId(), getControllerId(), actionId, -1, null);
if (action.getStatusCode() != HttpStatus.OK) {
log.warn(LOG_PREFIX + "Fail to get deployment action: {} -> {}", getTenantId(), getControllerId(), actionId, action.getStatusCode());
log.warn(LOG_PREFIX + "Fail to get deployment action: {} -> {}", getTenantId(), getControllerId(), actionId,
action.getStatusCode());
return Optional.empty();
}
return Optional.ofNullable(action.getBody() == null ? null : new AbstractMap.SimpleEntry<>(actionId, action.getBody()));
}
public void updateAttribute(final String mode, final String key, final String value) {
final DdiUpdateMode updateMode = switch (mode.toLowerCase()) {
case "replace" -> DdiUpdateMode.REPLACE;
case "remove" -> DdiUpdateMode.REMOVE;
default -> DdiUpdateMode.MERGE;
};
final DdiConfigData configData = new DdiConfigData(Collections.singletonMap(key, value), updateMode);
getDdiApi().putConfigData(configData, getTenantId(), getControllerId());
}
void sendFeedback(final UpdateStatus updateStatus) {
log.debug(LOG_PREFIX + "Send feedback {} -> {}", getTenantId(), getControllerId(), currentActionId, updateStatus);
try {
getDdiApi().postDeploymentBaseActionFeedback(updateStatus.feedback(), getTenantId(), getControllerId(),
currentActionId);
} catch (final RuntimeException e) {
log.error(LOG_PREFIX + "Failed to send feedback {} -> {}", getTenantId(), getControllerId(),
currentActionId, updateStatus, e);
}
if (updateStatus.status() == UpdateStatus.Status.SUCCESSFUL ||
updateStatus.status() == UpdateStatus.Status.FAILURE) {
lastActionId = currentActionId;
currentActionId = null;
}
}
private void sendConfirmationFeedback(final long actionId) {
final DdiConfirmationFeedback ddiConfirmationFeedback = new DdiConfirmationFeedback(
DdiConfirmationFeedback.Confirmation.CONFIRMED, 0, Collections.singletonList(

View File

@@ -9,15 +9,15 @@
*/
package org.eclipse.hawkbit.sdk.device;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import lombok.Getter;
import org.eclipse.hawkbit.sdk.Controller;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.Tenant;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* An in-memory simulated DDI Tenant to hold the controller twins in
* memory and be able to retrieve them again.

View File

@@ -9,6 +9,22 @@
*/
package org.eclipse.hawkbit.sdk.device;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
@@ -28,22 +44,6 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Update handler provide plug-in endpoint allowing for customization of the update processing.
*/
@@ -67,20 +67,16 @@ public interface UpdateHandler {
@Slf4j
class UpdateProcessor implements Runnable {
protected final Map<String, Path> downloads = new HashMap<>();
private static final String LOG_PREFIX = "[{}:{}] ";
private static final String DOWNLOAD_LOG_MESSAGE = "Download ";
private static final String EXPECTED = "(Expected: ";
private static final String BUT_GOT_LOG_MESSAGE = " but got: ";
private static final int MINIMUM_TOKEN_LENGTH_FOR_HINT = 6;
private final DdiController ddiController;
private final DdiDeployment.HandlingType updateType;
private final List<DdiChunk> modules;
private final ArtifactHandler artifactHandler;
protected final Map<String, Path> downloads = new HashMap<>();
public UpdateProcessor(
final DdiController ddiController,
@@ -186,101 +182,6 @@ public interface UpdateHandler {
log.debug(LOG_PREFIX + "Cleaned up", ddiController.getTenantId(), ddiController.getControllerId());
}
private void handleArtifact(
final String targetToken, final String gatewayToken,
final List<UpdateStatus> status, final DdiArtifact artifact) {
artifact.getLink("download").ifPresentOrElse(
// HTTPS
link -> status.add(downloadUrl(link.getHref(), gatewayToken, targetToken,
artifact.getHashes(), artifact.getSize())),
// HTTP
() -> status.add(downloadUrl(
artifact.getLink("download-http")
.map(Link::getHref)
.orElseThrow(() -> new IllegalArgumentException("Nor https nor http found!")),
gatewayToken, targetToken,
artifact.getHashes(), artifact.getSize()))
);
}
private UpdateStatus downloadUrl(
final String url, final String gatewayToken, final String targetToken,
final DdiArtifactHash hash, final long size) {
if (log.isDebugEnabled()) {
log.debug(LOG_PREFIX + "Downloading {} with token {}, expected hash {} and size {}",
ddiController.getTenantId(), ddiController.getControllerId(), url,
hideTokenDetails(targetToken), hash, size);
}
try {
return readAndCheckDownloadUrl(url, gatewayToken, targetToken, hash, size);
} catch (final IOException | KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
log.error(LOG_PREFIX + "Failed to download {}",
ddiController.getTenantId(), ddiController.getControllerId(), url, e);
return new UpdateStatus(
UpdateStatus.Status.FAILURE,
List.of("Failed to download " + url + ": " + e.getMessage()));
}
}
private UpdateStatus readAndCheckDownloadUrl(final String url, final String gatewayToken,
final String targetToken, final DdiArtifactHash hash, final long size)
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException {
final Validator sizeValidator = sizeValidator(size);
final Validator hashValidator = hashValidator(hash);
final ArtifactHandler.DownloadHandler downloadHandler = artifactHandler.getDownloadHandler(url);
try (final CloseableHttpClient httpclient = createHttpClientThatAcceptsAllServerCerts()) {
final HttpGet request = new HttpGet(url);
if (StringUtils.hasLength(targetToken)) {
request.addHeader(HttpHeaders.AUTHORIZATION, "TargetToken " + targetToken);
} else if (StringUtils.hasLength(gatewayToken)) {
request.addHeader(HttpHeaders.AUTHORIZATION, "GatewayToken " + gatewayToken);
}
return httpclient.execute(request, response -> {
try {
if (response.getCode() != HttpStatus.OK.value()) {
throw new IllegalStateException("Unexpected status code: " + response.getCode());
}
if (response.getEntity().getContentLength() != size) {
throw new IllegalArgumentException("Wrong content length " + EXPECTED + size + BUT_GOT_LOG_MESSAGE + response.getEntity()
.getContentLength() + ")!");
}
final byte[] buff = new byte[32 * 1024];
try (final InputStream is = response.getEntity().getContent()) {
for (int read; (read = is.read(buff)) != -1; ) {
sizeValidator.read(buff, read);
hashValidator.read(buff, read);
downloadHandler.read(buff, 0, read);
}
}
sizeValidator.validate();
hashValidator.validate();
final String message = "Downloaded " + url + " (" + size + " bytes)";
log.debug(LOG_PREFIX + message, ddiController.getTenantId(), ddiController.getControllerId());
downloadHandler.finished(ArtifactHandler.DownloadHandler.Status.SUCCESS);
downloadHandler.download().ifPresent(path -> downloads.put(url, path));
return new UpdateStatus(UpdateStatus.Status.SUCCESSFUL, List.of(message));
} catch (final Exception e) {
final String message = e.getMessage();
if (log.isTraceEnabled()) {
log.error(LOG_PREFIX + DOWNLOAD_LOG_MESSAGE + url + " failed: " + message,
ddiController.getTenantId(), ddiController.getControllerId(), e);
} else {
log.error(LOG_PREFIX + DOWNLOAD_LOG_MESSAGE + url + " failed: " + message,
ddiController.getTenantId(), ddiController.getControllerId());
}
downloadHandler.finished(ArtifactHandler.DownloadHandler.Status.ERROR);
return new UpdateStatus(UpdateStatus.Status.FAILURE, List.of(message));
}
});
}
}
private static String hideTokenDetails(final String targetToken) {
if (targetToken == null) {
return "<NULL!>";
@@ -314,14 +215,6 @@ public interface UpdateHandler {
.build();
}
private interface Validator {
void read(final byte[] buff, final int len);
void validate();
}
private static Validator sizeValidator(final long size) {
return new Validator() {
@@ -384,6 +277,7 @@ public interface UpdateHandler {
}
return new Validator() {
@Override
public void read(final byte[] buff, final int len) {
hashValidators.forEach(hashValidator -> hashValidator.update(buff, len));
@@ -395,5 +289,108 @@ public interface UpdateHandler {
}
};
}
private void handleArtifact(
final String targetToken, final String gatewayToken,
final List<UpdateStatus> status, final DdiArtifact artifact) {
artifact.getLink("download").ifPresentOrElse(
// HTTPS
link -> status.add(downloadUrl(link.getHref(), gatewayToken, targetToken,
artifact.getHashes(), artifact.getSize())),
// HTTP
() -> status.add(downloadUrl(
artifact.getLink("download-http")
.map(Link::getHref)
.orElseThrow(() -> new IllegalArgumentException("Nor https nor http found!")),
gatewayToken, targetToken,
artifact.getHashes(), artifact.getSize()))
);
}
private UpdateStatus downloadUrl(
final String url, final String gatewayToken, final String targetToken,
final DdiArtifactHash hash, final long size) {
if (log.isDebugEnabled()) {
log.debug(LOG_PREFIX + "Downloading {} with token {}, expected hash {} and size {}",
ddiController.getTenantId(), ddiController.getControllerId(), url,
hideTokenDetails(targetToken), hash, size);
}
try {
return readAndCheckDownloadUrl(url, gatewayToken, targetToken, hash, size);
} catch (final IOException | KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
log.error(LOG_PREFIX + "Failed to download {}",
ddiController.getTenantId(), ddiController.getControllerId(), url, e);
return new UpdateStatus(
UpdateStatus.Status.FAILURE,
List.of("Failed to download " + url + ": " + e.getMessage()));
}
}
private UpdateStatus readAndCheckDownloadUrl(final String url, final String gatewayToken,
final String targetToken, final DdiArtifactHash hash, final long size)
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException {
final Validator sizeValidator = sizeValidator(size);
final Validator hashValidator = hashValidator(hash);
final ArtifactHandler.DownloadHandler downloadHandler = artifactHandler.getDownloadHandler(url);
try (final CloseableHttpClient httpclient = createHttpClientThatAcceptsAllServerCerts()) {
final HttpGet request = new HttpGet(url);
if (StringUtils.hasLength(targetToken)) {
request.addHeader(HttpHeaders.AUTHORIZATION, "TargetToken " + targetToken);
} else if (StringUtils.hasLength(gatewayToken)) {
request.addHeader(HttpHeaders.AUTHORIZATION, "GatewayToken " + gatewayToken);
}
return httpclient.execute(request, response -> {
try {
if (response.getCode() != HttpStatus.OK.value()) {
throw new IllegalStateException("Unexpected status code: " + response.getCode());
}
if (response.getEntity().getContentLength() != size) {
throw new IllegalArgumentException(
"Wrong content length " + EXPECTED + size + BUT_GOT_LOG_MESSAGE + response.getEntity()
.getContentLength() + ")!");
}
final byte[] buff = new byte[32 * 1024];
try (final InputStream is = response.getEntity().getContent()) {
for (int read; (read = is.read(buff)) != -1; ) {
sizeValidator.read(buff, read);
hashValidator.read(buff, read);
downloadHandler.read(buff, 0, read);
}
}
sizeValidator.validate();
hashValidator.validate();
final String message = "Downloaded " + url + " (" + size + " bytes)";
log.debug(LOG_PREFIX + message, ddiController.getTenantId(), ddiController.getControllerId());
downloadHandler.finished(ArtifactHandler.DownloadHandler.Status.SUCCESS);
downloadHandler.download().ifPresent(path -> downloads.put(url, path));
return new UpdateStatus(UpdateStatus.Status.SUCCESSFUL, List.of(message));
} catch (final Exception e) {
final String message = e.getMessage();
if (log.isTraceEnabled()) {
log.error(LOG_PREFIX + DOWNLOAD_LOG_MESSAGE + url + " failed: " + message,
ddiController.getTenantId(), ddiController.getControllerId(), e);
} else {
log.error(LOG_PREFIX + DOWNLOAD_LOG_MESSAGE + url + " failed: " + message,
ddiController.getTenantId(), ddiController.getControllerId());
}
downloadHandler.finished(ArtifactHandler.DownloadHandler.Status.ERROR);
return new UpdateStatus(UpdateStatus.Status.FAILURE, List.of(message));
}
});
}
}
private interface Validator {
void read(final byte[] buff, final int len);
void validate();
}
}
}

View File

@@ -9,14 +9,19 @@
*/
package org.eclipse.hawkbit.sdk.device;
import java.util.List;
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
import java.util.List;
public record UpdateStatus(Status status, List<String> messages) {
DdiActionFeedback feedback() {
return new DdiActionFeedback(null,
new DdiStatus(status.executionStatus, new DdiResult(status.finalResult, null), status.code, messages));
}
/**
* The status to response to the hawkBit update server if an simulated update process should be respond with
* successful or failure update.
@@ -59,9 +64,4 @@ public record UpdateStatus(Status status, List<String> messages) {
this.code = code;
}
}
DdiActionFeedback feedback() {
return new DdiActionFeedback(null,
new DdiStatus(status.executionStatus, new DdiResult(status.finalResult, null), status.code, messages));
}
}