Introduce inital draft of hawkBit SDK (#1638)
Intends to provide a Java SDK facilitating: * development of back-end integrations using mgmt api (including UI-s) * development of java based high-end devices (which could run Spring apps) to communicate with hawkBit via DDI API * implementation of demo/test cases using device & management SDK Status: initial draft - Feign client did & management API - done - Hal/HATEAOS Support - works (including in non-web apps) - device communication works when no software updates (e.g. pulling software base) - demo for single and multiple devices simulation (including management API uses) - TODO - fix software update flows - TODO - provide more integration points for developers to interact with device SDK Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com>
This commit is contained in:
48
hawkbit-sdk/hawkbit-sdk-demo/pom.xml
Normal file
48
hawkbit-sdk/hawkbit-sdk-demo/pom.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<!--
|
||||
|
||||
Copyright (c) 2023 Bosch.IO GmbH and others
|
||||
|
||||
This program and the accompanying materials are made
|
||||
available under the terms of the Eclipse Public License 2.0
|
||||
which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
|
||||
SPDX-License-Identifier: EPL-2.0
|
||||
|
||||
-->
|
||||
<project
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-sdk</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-sdk-test</artifactId>
|
||||
<name>hawkBit :: SDK :: Test / Example</name>
|
||||
<description>Test / Example of how SDK could be used to for devices and for Mgmt API access</description>
|
||||
|
||||
<properties>
|
||||
<spring-shell.version>3.1.5</spring-shell.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-sdk-device</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-mgmt-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.shell</groupId>
|
||||
<artifactId>spring-shell-starter</artifactId>
|
||||
<version>${spring-shell.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Bosch.IO GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.sdk.demo;
|
||||
|
||||
import feign.FeignException;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTenantManagementRestApi;
|
||||
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
||||
import org.eclipse.hawkbit.sdk.Tenant;
|
||||
import org.eclipse.hawkbit.sdk.device.DdiController;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Abstract class representing DDI device connecting directly to hawkVit.
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Slf4j
|
||||
public class SetupHelper {
|
||||
|
||||
private static final String AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED = "authentication.gatewaytoken.enabled";
|
||||
|
||||
/**
|
||||
* Gateway token value.
|
||||
*/
|
||||
private static final String AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY = "authentication.gatewaytoken.key";
|
||||
private static final String AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED = "authentication.targettoken.enabled";
|
||||
|
||||
// if gateway toke is configured then the gateway auth is enabled key is set
|
||||
// so all devices use gateway token authentication
|
||||
// otherwise target token authentication is enabled. Then all devices shall be registerd
|
||||
// and the target token shall be set to the one from the DDI controller instance
|
||||
public static void setupTargetAuthentication(final HawkbitClient hawkbitClient, final Tenant tenant) {
|
||||
final MgmtTenantManagementRestApi mgmtTenantManagementRestApi =
|
||||
hawkbitClient.mgmtService(MgmtTenantManagementRestApi.class, tenant);
|
||||
if (ObjectUtils.isEmpty(tenant.getGatewayToken())) {
|
||||
if (!((Boolean)mgmtTenantManagementRestApi
|
||||
.getTenantConfigurationValue(AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED)
|
||||
.getBody().getValue())) {
|
||||
mgmtTenantManagementRestApi.updateTenantConfiguration(
|
||||
Map.of(AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, true)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!((Boolean)mgmtTenantManagementRestApi
|
||||
.getTenantConfigurationValue(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED)
|
||||
.getBody().getValue())) {
|
||||
mgmtTenantManagementRestApi.updateTenantConfiguration(
|
||||
Map.of(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, true)
|
||||
);
|
||||
}
|
||||
if (!tenant.getGatewayToken().equals(
|
||||
mgmtTenantManagementRestApi
|
||||
.getTenantConfigurationValue(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY)
|
||||
.getBody().getValue())) {
|
||||
mgmtTenantManagementRestApi.updateTenantConfiguration(
|
||||
Map.of(AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, tenant.getGatewayToken())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns target token
|
||||
public static String setupTargetToken(
|
||||
final String controllerId, String securityTargetToken,
|
||||
final HawkbitClient hawkbitClient, final Tenant tenant) {
|
||||
if (ObjectUtils.isEmpty(tenant.getGatewayToken())) {
|
||||
final MgmtTargetRestApi mgmtTargetRestApi = hawkbitClient.mgmtService(MgmtTargetRestApi.class, tenant);
|
||||
try {
|
||||
// test if target exist, if not - throws 404
|
||||
final MgmtTarget target = mgmtTargetRestApi.getTarget(controllerId).getBody();
|
||||
if (ObjectUtils.isEmpty(securityTargetToken)) {
|
||||
if (ObjectUtils.isEmpty(target.getSecurityToken())) {
|
||||
// generate random to set to tha existing target without configured security token
|
||||
securityTargetToken = randomToken();
|
||||
mgmtTargetRestApi.updateTarget(controllerId,
|
||||
new MgmtTargetRequestBody().setSecurityToken(securityTargetToken));
|
||||
}
|
||||
} else if (!securityTargetToken.equals(target.getSecurityToken())){
|
||||
// update target's with the security token (since it doesn't match)
|
||||
mgmtTargetRestApi.updateTarget(controllerId,
|
||||
new MgmtTargetRequestBody().setSecurityToken(securityTargetToken));
|
||||
}
|
||||
} catch (final FeignException.NotFound e) {
|
||||
if (ObjectUtils.isEmpty(securityTargetToken)) {
|
||||
securityTargetToken = randomToken();
|
||||
}
|
||||
// create target with the security token
|
||||
mgmtTargetRestApi.createTargets(List.of(
|
||||
new MgmtTargetRequestBody()
|
||||
.setControllerId(controllerId)
|
||||
.setSecurityToken(securityTargetToken)));
|
||||
}
|
||||
}
|
||||
|
||||
return securityTargetToken;
|
||||
}
|
||||
|
||||
private static final Random RND = new Random();
|
||||
public static String randomToken() {
|
||||
final byte[] rnd = new byte[24];
|
||||
RND.nextBytes(rnd);
|
||||
return Base64.getEncoder().encodeToString(rnd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Bosch.IO GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.sdk.demo.device;
|
||||
|
||||
import feign.Client;
|
||||
import feign.Contract;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.sdk.Controller;
|
||||
import org.eclipse.hawkbit.sdk.HawkbitServer;
|
||||
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
||||
import org.eclipse.hawkbit.sdk.HawkbitSDKConfigurtion;
|
||||
import org.eclipse.hawkbit.sdk.Tenant;
|
||||
import org.eclipse.hawkbit.sdk.demo.SetupHelper;
|
||||
import org.eclipse.hawkbit.sdk.device.DdiController;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* Abstract class representing DDI device connecting directly to hawkVit.
|
||||
*/
|
||||
@Slf4j
|
||||
@SpringBootApplication
|
||||
@Import({ HawkbitSDKConfigurtion.class})
|
||||
public class DeviceApp {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DeviceApp.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
HawkbitClient hawkbitClient(
|
||||
final HawkbitServer hawkBitServer,
|
||||
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
|
||||
return new HawkbitClient(hawkBitServer, client, encoder, decoder, contract);
|
||||
}
|
||||
|
||||
@Bean
|
||||
DdiController device(
|
||||
@Value("${hawkbit.device:controller-default}") final String controllerId,
|
||||
@Value("${hawkbit.device.securityToken:}") final String securityToken,
|
||||
final Tenant defaultTenant, final HawkbitClient hawkbitClient) {
|
||||
return new DdiController(
|
||||
defaultTenant,
|
||||
Controller.builder()
|
||||
.controllerId(controllerId)
|
||||
.securityToken(ObjectUtils.isEmpty(securityToken) ?
|
||||
(ObjectUtils.isEmpty(defaultTenant.getGatewayToken()) ? SetupHelper.randomToken() : securityToken) :
|
||||
securityToken)
|
||||
.build(),
|
||||
hawkbitClient).setOverridePollMillis(10_000);
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class Shell {
|
||||
|
||||
private final Tenant tenant;
|
||||
private final DdiController device;
|
||||
private final HawkbitClient hawkbitClient;
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
Shell(final Tenant tenant, final DdiController device, final HawkbitClient hawkbitClient) {
|
||||
this.tenant = tenant;
|
||||
this.device = device;
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
}
|
||||
|
||||
@ShellMethod(key = "setup")
|
||||
public void setup() {
|
||||
SetupHelper.setupTargetAuthentication(hawkbitClient, tenant);
|
||||
SetupHelper.setupTargetToken(
|
||||
device.getControllerId(), device.getTargetSecurityToken(), hawkbitClient, tenant);
|
||||
}
|
||||
|
||||
@ShellMethod(key = "start")
|
||||
public void start() {
|
||||
device.start(scheduler);
|
||||
}
|
||||
|
||||
@ShellMethod(key = "stop")
|
||||
public void stop() {
|
||||
device.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Bosch.IO GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.sdk.demo.multidevice;
|
||||
|
||||
import feign.Client;
|
||||
import feign.Contract;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.sdk.Controller;
|
||||
import org.eclipse.hawkbit.sdk.HawkbitServer;
|
||||
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
||||
import org.eclipse.hawkbit.sdk.HawkbitSDKConfigurtion;
|
||||
import org.eclipse.hawkbit.sdk.Tenant;
|
||||
import org.eclipse.hawkbit.sdk.demo.SetupHelper;
|
||||
import org.eclipse.hawkbit.sdk.device.DdiController;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.standard.ShellOption;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* Abstract class representing DDI device connecting directly to hawkVit.
|
||||
*/
|
||||
@Slf4j
|
||||
@SpringBootApplication
|
||||
@Import({ HawkbitSDKConfigurtion.class})
|
||||
public class MultiDeviceApp {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MultiDeviceApp.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
HawkbitClient hawkbitClient(
|
||||
final HawkbitServer hawkBitServer,
|
||||
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
|
||||
return new HawkbitClient(hawkBitServer, client, encoder, decoder, contract);
|
||||
}
|
||||
|
||||
@ShellComponent
|
||||
public static class Shell {
|
||||
|
||||
private final Tenant tenant;
|
||||
private final HawkbitClient hawkbitClient;
|
||||
private final Map<String, DdiController> devices = new ConcurrentHashMap<>();
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
private boolean setup;
|
||||
|
||||
Shell(final Tenant tenant, final HawkbitClient hawkbitClient) {
|
||||
this.tenant = tenant;
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
}
|
||||
|
||||
@ShellMethod(key = "setup")
|
||||
public void setup() {
|
||||
SetupHelper.setupTargetAuthentication(hawkbitClient, tenant);
|
||||
setup = true;
|
||||
}
|
||||
|
||||
@ShellMethod(key = "start-one")
|
||||
public void startOne(@ShellOption("--id") final String controllerId) {
|
||||
DdiController device = devices.get(controllerId);
|
||||
final String securityTargetToken;
|
||||
if (setup) {
|
||||
securityTargetToken = SetupHelper.setupTargetToken(
|
||||
controllerId, null, hawkbitClient, tenant);
|
||||
} else {
|
||||
securityTargetToken = null;
|
||||
}
|
||||
if (device == null) {
|
||||
device = new DdiController(tenant,
|
||||
Controller.builder()
|
||||
.controllerId(controllerId)
|
||||
.securityToken(securityTargetToken)
|
||||
.build(),
|
||||
hawkbitClient).setOverridePollMillis(10_000);
|
||||
final DdiController oldDevice = devices.putIfAbsent(controllerId, device);
|
||||
if (oldDevice != null) {
|
||||
device = oldDevice; // reuse existing
|
||||
}
|
||||
}
|
||||
|
||||
device.start(scheduler);
|
||||
}
|
||||
|
||||
@ShellMethod(key = "stop-one")
|
||||
public void stopOne(@ShellOption("--id") final String controllerId) {
|
||||
final DdiController device = devices.get(controllerId);
|
||||
if (device == null) {
|
||||
System.out.println("ERROR: controller with id " + controllerId + " not found!");
|
||||
} else {
|
||||
device.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@ShellMethod(key = "start")
|
||||
public void start(
|
||||
@ShellOption(value = "--prefix", defaultValue = "") final String prefix,
|
||||
@ShellOption(value = "--offset", defaultValue = "0") final int offset,
|
||||
@ShellOption(value = "--count") final int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
startOne(String.format(prefix + "%03d", offset + i));
|
||||
}
|
||||
}
|
||||
|
||||
@ShellMethod(key = "stop")
|
||||
public void stop(
|
||||
@ShellOption(value = "--prefix", defaultValue = "") final String prefix,
|
||||
@ShellOption(value = "--offset", defaultValue = "0") final int offset,
|
||||
@ShellOption(value = "--count") final int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
stopOne(String.format(prefix + "%03d", offset + i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2023 Bosch.IO GmbH and others
|
||||
#
|
||||
# This program and the accompanying materials are made
|
||||
# available under the terms of the Eclipse Public License 2.0
|
||||
# which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
#
|
||||
# SPDX-License-Identifier: EPL-2.0
|
||||
#
|
||||
|
||||
spring.main.web-application-type=none
|
||||
spring.cloud.openfeign.httpclient.hc5.enabled=true
|
||||
|
||||
logging.level.org.eclipse.hawkbit=DEBUG
|
||||
|
||||
Reference in New Issue
Block a user