diff --git a/application/src/main/data/upgrade/2.6.0/schema_update.cql b/application/src/main/data/upgrade/2.6.0/schema_update.cql index 6ac8f404bd..a6744c52f9 100644 --- a/application/src/main/data/upgrade/2.6.0/schema_update.cql +++ b/application/src/main/data/upgrade/2.6.0/schema_update.cql @@ -57,6 +57,8 @@ CREATE TABLE IF NOT EXISTS thingsboard.edge ( search_text text, routing_key text, secret text, + edge_license_key text, + cloud_endpoint text, configuration text, additional_info text, PRIMARY KEY (id, tenant_id, customer_id, type) diff --git a/application/src/main/data/upgrade/2.6.0/schema_update.sql b/application/src/main/data/upgrade/2.6.0/schema_update.sql index 90660d4026..b3bfd07620 100644 --- a/application/src/main/data/upgrade/2.6.0/schema_update.sql +++ b/application/src/main/data/upgrade/2.6.0/schema_update.sql @@ -25,6 +25,8 @@ CREATE TABLE IF NOT EXISTS edge ( label varchar(255), routing_key varchar(255), secret varchar(255), + edge_license_key varchar(30), + cloud_endpoint varchar(255), search_text varchar(255), tenant_id varchar(31), CONSTRAINT edge_name_unq_key UNIQUE (tenant_id, name), diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 683345fe7f..0972d965ad 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -69,7 +69,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; - protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"}; + protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**"; diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index 057e4a90bf..4c6be42064 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeSearchQuery; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; @@ -408,4 +409,24 @@ public class EdgeController extends BaseController { } } + @RequestMapping(value = "/license/checkInstance", method = RequestMethod.POST) + @ResponseBody + public Object checkInstance(@RequestBody Object request) throws ThingsboardException { + try { + return edgeService.checkInstance(request); + } catch (Exception e) { + throw new ThingsboardException(e, ThingsboardErrorCode.SUBSCRIPTION_VIOLATION); + } + } + + @RequestMapping(value = "/license/activateInstance", params = {"licenseSecret", "releaseDate"}, method = RequestMethod.POST) + @ResponseBody + public Object activateInstance(@RequestParam String licenseSecret, + @RequestParam String releaseDate) throws ThingsboardException { + try { + return edgeService.activateInstance(licenseSecret, releaseDate); + } catch (Exception e) { + throw new ThingsboardException(e, ThingsboardErrorCode.SUBSCRIPTION_VIOLATION); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index e139f9a821..f8026d6278 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -27,6 +27,7 @@ import org.springframework.security.authentication.LockedException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; @@ -66,7 +67,12 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler { response.setContentType(MediaType.APPLICATION_JSON_VALUE); if (exception instanceof ThingsboardException) { - handleThingsboardException((ThingsboardException) exception, response); + ThingsboardException thingsboardException = (ThingsboardException) exception; + if (thingsboardException.getErrorCode() == ThingsboardErrorCode.SUBSCRIPTION_VIOLATION) { + handleSubscriptionException((ThingsboardException) exception, response); + } else { + handleThingsboardException((ThingsboardException) exception, response); + } } else if (exception instanceof TbRateLimitsException) { handleRateLimitException(response, (TbRateLimitsException) exception); } else if (exception instanceof AccessDeniedException) { @@ -126,6 +132,11 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler { ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS)); } + private void handleSubscriptionException(ThingsboardException subscriptionException, HttpServletResponse response) throws IOException { + response.setStatus(HttpStatus.FORBIDDEN.value()); + mapper.writeValue(response.getWriter(), + (new ObjectMapper()).readValue(((HttpClientErrorException) subscriptionException.getCause()).getResponseBodyAsByteArray(), Object.class)); + } private void handleAccessDeniedException(HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index f6359275ea..85ea93bed3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.Resources; import com.google.common.util.concurrent.FutureCallback; import io.grpc.Server; -import io.grpc.ServerBuilder; +import io.grpc.netty.NettyServerBuilder; import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -49,6 +49,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; @Service @Slf4j @@ -68,6 +69,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private String privateKeyResource; @Value("${edges.state.persistToTelemetry:false}") private boolean persistToTelemetry; + @Value("${edges.rpc.client_max_keep_alive_time_sec}") + private int clientMaxKeepAliveTimeSec; @Autowired private EdgeContextComponent ctx; @@ -82,7 +85,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i @PostConstruct public void init() { log.info("Initializing Edge RPC service!"); - ServerBuilder builder = ServerBuilder.forPort(rpcPort).addService(this); + NettyServerBuilder builder = NettyServerBuilder.forPort(rpcPort) + .permitKeepAliveTime(clientMaxKeepAliveTimeSec, TimeUnit.SECONDS) + .addService(this); if (sslEnabled) { try { File certFile = new File(Resources.getResource(certFileResource).toURI()); @@ -145,8 +150,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i executor.submit(() -> { while (!Thread.interrupted()) { try { - for (EdgeGrpcSession session : sessions.values()) { - session.processHandleMessages(); + if (sessions.size() > 0) { + for (EdgeGrpcSession session : sessions.values()) { + session.processHandleMessages(); + } + } else { + log.trace("No sessions available, sleep for the next run"); + try { + Thread.sleep(1000); + } catch (InterruptedException ignore) {} } } catch (Exception e) { log.warn("Failed to process messages handling!", e); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index d56f2b871b..de68eb6e55 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -83,6 +83,7 @@ import org.thingsboard.server.gen.edge.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.DownlinkMsg; import org.thingsboard.server.gen.edge.DownlinkResponseMsg; import org.thingsboard.server.gen.edge.EdgeConfiguration; +import org.thingsboard.server.gen.edge.EdgeUpdateMsg; import org.thingsboard.server.gen.edge.EntityDataProto; import org.thingsboard.server.gen.edge.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.RelationRequestMsg; @@ -159,10 +160,10 @@ public final class EdgeGrpcSession implements Closeable { if (ConnectResponseCode.ACCEPTED != responseMsg.getResponseCode()) { outputStream.onError(new RuntimeException(responseMsg.getErrorMsg())); } - if (ConnectResponseCode.ACCEPTED == responseMsg.getResponseCode()) { - connected = true; - ctx.getSyncEdgeService().sync(edge); - } + } + if (!connected && requestMsg.getMsgType().equals(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE)) { + connected = true; + ctx.getSyncEdgeService().sync(edge); } if (connected) { if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE) && requestMsg.hasUplinkMsg()) { @@ -182,8 +183,12 @@ public final class EdgeGrpcSession implements Closeable { @Override public void onCompleted() { connected = false; - sessionCloseListener.accept(edge.getId()); - outputStream.onCompleted(); + if (edge != null) { + sessionCloseListener.accept(edge.getId()); + } + try { + outputStream.onCompleted(); + } catch (Exception ignored) {} } }; } @@ -240,12 +245,11 @@ public final class EdgeGrpcSession implements Closeable { void onConfigurationUpdate(Edge edge) { try { this.edge = edge; - // TODO: voba - push edge configuration update to edge -// sendResponseMsg(org.thingsboard.server.gen.integration.ResponseMsg.newBuilder() -// .setIntegrationUpdateMsg(IntegrationUpdateMsg.newBuilder() -// .setConfiguration(constructIntegrationConfigProto(configuration, defaultConverterProto, downLinkConverterProto)) -// .build()) -// .build()); + EdgeUpdateMsg edgeConfig = EdgeUpdateMsg.newBuilder() + .setConfiguration(constructEdgeConfigProto(edge)).build(); + outputStream.onNext(ResponseMsg.newBuilder() + .setEdgeUpdateMsg(edgeConfig) + .build()); } catch (Exception e) { log.error("Failed to construct proto objects!", e); } @@ -899,7 +903,7 @@ public final class EdgeGrpcSession implements Closeable { } } if (uplinkMsg.getDeviceRpcCallMsgList() != null && !uplinkMsg.getDeviceRpcCallMsgList().isEmpty()) { - for (DeviceRpcCallMsg deviceRpcCallMsg: uplinkMsg.getDeviceRpcCallMsgList()) { + for (DeviceRpcCallMsg deviceRpcCallMsg : uplinkMsg.getDeviceRpcCallMsgList()) { result.add(ctx.getDeviceProcessor().processDeviceRpcCallResponseMsg(edge.getTenantId(), deviceRpcCallMsg)); } } @@ -948,6 +952,8 @@ public final class EdgeGrpcSession implements Closeable { .setName(edge.getName()) .setRoutingKey(edge.getRoutingKey()) .setType(edge.getType()) + .setEdgeLicenseKey(edge.getEdgeLicenseKey()) + .setCloudEndpoint(edge.getCloudEndpoint()) .setCloudType("CE") .build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java index 694efcd081..c6eb3d32f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java @@ -17,11 +17,10 @@ package org.thingsboard.server.service.edge.rpc.constructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import org.thingsboard.server.gen.edge.AssetUpdateMsg; import org.thingsboard.server.gen.edge.UpdateMsgType; @@ -43,6 +42,9 @@ public class AssetMsgConstructor { builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); } + if (asset.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(asset.getAdditionalInfo())); + } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java index 38905d7529..ffc635075d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.DeviceUpdateMsg; @@ -50,6 +51,9 @@ public class DeviceMsgConstructor { builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); } + if (device.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(device.getAdditionalInfo())); + } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java index c19a84cfcb..abe32dec77 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityViewMsgConstructor.java @@ -19,8 +19,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import org.thingsboard.server.gen.edge.EdgeEntityType; import org.thingsboard.server.gen.edge.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.UpdateMsgType; @@ -54,6 +54,9 @@ public class EntityViewMsgConstructor { builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); } + if (entityView.getAdditionalInfo() != null) { + builder.setAdditionalInfo(JacksonUtil.toString(entityView.getAdditionalInfo())); + } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java index 85ffab4799..0e5b88aa2a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceProcessor.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import org.thingsboard.server.gen.edge.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.DeviceUpdateMsg; @@ -131,6 +132,7 @@ public class DeviceProcessor extends BaseProcessor { device.setName(deviceUpdateMsg.getName()); device.setType(deviceUpdateMsg.getType()); device.setLabel(deviceUpdateMsg.getLabel()); + device.setAdditionalInfo(JacksonUtil.toJsonNode(deviceUpdateMsg.getAdditionalInfo())); deviceService.saveDevice(device); saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, ActionType.CREDENTIALS_REQUEST, deviceId, null); @@ -148,6 +150,7 @@ public class DeviceProcessor extends BaseProcessor { device.setName(deviceUpdateMsg.getName()); device.setType(deviceUpdateMsg.getType()); device.setLabel(deviceUpdateMsg.getLabel()); + device.setAdditionalInfo(JacksonUtil.toJsonNode(deviceUpdateMsg.getAdditionalInfo())); device = deviceService.saveDevice(device); createDeviceCredentials(device); createRelationFromEdge(tenantId, edge.getId(), device.getId()); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index bcf3425f53..4fedc0ce08 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -590,6 +590,7 @@ edges: rpc: enabled: "${EDGES_RPC_ENABLED:false}" port: "${EDGES_RPC_PORT:7070}" + client_max_keep_alive_time_sec: "${EDGES_RPC_CLIENT_MAX_KEEP_ALIVE_TIME_SEC:300}" ssl: # Enable/disable SSL support enabled: "${EDGES_RPC_SSL_ENABLED:false}" diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java index 0b3a86a22e..d44dd867de 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java @@ -510,6 +510,8 @@ public abstract class AbstractControllerTest { edge.setType(type); edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setEdgeLicenseKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setCloudEndpoint("http://localhost:8080"); return edge; } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java index 1f655a2c3c..165f8cecdb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.controller; import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.core.type.TypeReference; import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -90,6 +91,8 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { Assert.assertNotNull(savedEdge.getCustomerId()); Assert.assertEquals(NULL_UUID, savedEdge.getCustomerId().getId()); Assert.assertEquals(edge.getName(), savedEdge.getName()); + Assert.assertTrue(StringUtils.isNoneBlank(savedEdge.getEdgeLicenseKey())); + Assert.assertTrue(StringUtils.isNoneBlank(savedEdge.getCloudEndpoint())); savedEdge.setName("My new edge"); doPost("/api/edge", savedEdge, Edge.class); diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index 9f6d51a601..e1b54b6f84 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -75,10 +75,12 @@ public class EdgeImitator { this::onEdgeUpdate, this::onDownlink, this::onClose); + + edgeRpcClient.sendSyncRequestMsg(); } public void disconnect() throws InterruptedException { - edgeRpcClient.disconnect(); + edgeRpcClient.disconnect(false); } private void onUplinkResponse(UplinkResponseMsg msg) { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java index f09db3e9e1..e29d5f3307 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java @@ -76,4 +76,8 @@ public interface EdgeService { ListenableFuture> findEdgesByTenantIdAndDashboardId(TenantId tenantId, DashboardId dashboardId); ListenableFuture> findRelatedEdgeIdsByEntityId(TenantId tenantId, EntityId entityId); + + Object checkInstance(Object request); + + Object activateInstance(String licenseSecret, String releaseDate); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java index 56990cff44..699fdf954a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -45,6 +45,8 @@ public class Edge extends SearchTextBasedWithAdditionalInfo implements H private String label; private String routingKey; private String secret; + private String edgeLicenseKey; + private String cloudEndpoint; private transient JsonNode configuration; public Edge() { @@ -64,6 +66,8 @@ public class Edge extends SearchTextBasedWithAdditionalInfo implements H this.name = edge.getName(); this.routingKey = edge.getRoutingKey(); this.secret = edge.getSecret(); + this.edgeLicenseKey = edge.getEdgeLicenseKey(); + this.cloudEndpoint = edge.getCloudEndpoint(); this.configuration = edge.getConfiguration(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java index 916a50b26a..d28eea57aa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java @@ -28,7 +28,8 @@ public enum ThingsboardErrorCode { BAD_REQUEST_PARAMS(31), ITEM_NOT_FOUND(32), TOO_MANY_REQUESTS(33), - TOO_MANY_UPDATES(34); + TOO_MANY_UPDATES(34), + SUBSCRIPTION_VIOLATION(40); private int errorCode; diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 57c1cf272e..6232029a14 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -40,6 +40,7 @@ import org.thingsboard.server.gen.edge.UplinkResponseMsg; import javax.net.ssl.SSLException; import java.io.File; import java.net.URISyntaxException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; @@ -54,6 +55,8 @@ public class EdgeGrpcClient implements EdgeRpcClient { private int rpcPort; @Value("${cloud.rpc.timeout}") private int timeoutSecs; + @Value("${cloud.rpc.keep_alive_time_sec}") + private int keepAliveTimeSec; @Value("${cloud.rpc.ssl.enabled}") private boolean sslEnabled; @Value("${cloud.rpc.ssl.cert}") @@ -72,7 +75,9 @@ public class EdgeGrpcClient implements EdgeRpcClient { Consumer onEdgeUpdate, Consumer onDownlink, Consumer onError) { - NettyChannelBuilder builder = NettyChannelBuilder.forAddress(rpcHost, rpcPort).usePlaintext(); + NettyChannelBuilder builder = NettyChannelBuilder.forAddress(rpcHost, rpcPort) + .keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS) + .usePlaintext(); if (sslEnabled) { try { builder.sslContext(GrpcSslContexts.forClient().trustManager(new File(Resources.getResource(certResource).toURI())).build()); @@ -81,7 +86,6 @@ public class EdgeGrpcClient implements EdgeRpcClient { throw new RuntimeException(e); } } - gracefulShutdown(); channel = builder.build(); EdgeRpcServiceGrpc.EdgeRpcServiceStub stub = EdgeRpcServiceGrpc.newStub(channel); log.info("[{}] Sending a connect request to the TB!", edgeKey); @@ -92,53 +96,6 @@ public class EdgeGrpcClient implements EdgeRpcClient { .build()); } - private void gracefulShutdown() { - try { - if (channel != null) { - channel.shutdown().awaitTermination(timeoutSecs, TimeUnit.SECONDS); - } - } catch (InterruptedException e) { - log.debug("Error during shutdown of the previous channel", e); - } - } - - @Override - public void disconnect() throws InterruptedException { - try { - inputStream.onCompleted(); - } catch (Exception e) { - } - if (channel != null) { - channel.shutdown().awaitTermination(timeoutSecs, TimeUnit.SECONDS); - } - } - - @Override - public void sendUplinkMsg(UplinkMsg msg) { - try { - uplinkMsgLock.lock(); - this.inputStream.onNext(RequestMsg.newBuilder() - .setMsgType(RequestMsgType.UPLINK_RPC_MESSAGE) - .setUplinkMsg(msg) - .build()); - } finally { - uplinkMsgLock.unlock(); - } - } - - @Override - public void sendDownlinkResponseMsg(DownlinkResponseMsg downlinkResponseMsg) { - try { - uplinkMsgLock.lock(); - this.inputStream.onNext(RequestMsg.newBuilder() - .setMsgType(RequestMsgType.UPLINK_RPC_MESSAGE) - .setDownlinkResponseMsg(downlinkResponseMsg) - .build()); - } finally { - uplinkMsgLock.unlock(); - } - } - private StreamObserver initOutputStream(String edgeKey, Consumer onUplinkResponse, Consumer onEdgeUpdate, @@ -155,12 +112,15 @@ public class EdgeGrpcClient implements EdgeRpcClient { } else { log.error("[{}] Failed to establish the connection! Code: {}. Error message: {}.", edgeKey, connectResponseMsg.getResponseCode(), connectResponseMsg.getErrorMsg()); try { - EdgeGrpcClient.this.disconnect(); + EdgeGrpcClient.this.disconnect(true); } catch (InterruptedException e) { log.error("[{}] Got interruption during disconnect!", edgeKey, e); } onError.accept(new EdgeConnectionException("Failed to establish the connection! Response code: " + connectResponseMsg.getResponseCode().name())); } + } else if (responseMsg.hasEdgeUpdateMsg()) { + log.debug("[{}] Edge update message received {}", edgeKey, responseMsg.getEdgeUpdateMsg()); + onEdgeUpdate.accept(responseMsg.getEdgeUpdateMsg().getConfiguration()); } else if (responseMsg.hasUplinkResponseMsg()) { log.debug("[{}] Uplink response message received {}", edgeKey, responseMsg.getUplinkResponseMsg()); onUplinkResponse.accept(responseMsg.getUplinkResponseMsg()); @@ -179,8 +139,57 @@ public class EdgeGrpcClient implements EdgeRpcClient { @Override public void onCompleted() { log.debug("[{}] The rpc session was closed!", edgeKey); - onError.accept(new EdgeConnectionException("[" + edgeKey + "] The rpc session was closed!")); } }; } + + @Override + public void disconnect(boolean onError) throws InterruptedException { + if (!onError) { + try { + inputStream.onCompleted(); + } catch (Exception ignored) {} + } + if (channel != null) { + channel.shutdown().awaitTermination(timeoutSecs, TimeUnit.SECONDS); + } + } + + @Override + public void sendUplinkMsg(UplinkMsg msg) { + try { + uplinkMsgLock.lock(); + this.inputStream.onNext(RequestMsg.newBuilder() + .setMsgType(RequestMsgType.UPLINK_RPC_MESSAGE) + .setUplinkMsg(msg) + .build()); + } finally { + uplinkMsgLock.unlock(); + } + } + + @Override + public void sendSyncRequestMsg() { + try { + uplinkMsgLock.lock(); + this.inputStream.onNext(RequestMsg.newBuilder() + .setMsgType(RequestMsgType.SYNC_REQUEST_RPC_MESSAGE) + .build()); + } finally { + uplinkMsgLock.unlock(); + } + } + + @Override + public void sendDownlinkResponseMsg(DownlinkResponseMsg downlinkResponseMsg) { + try { + uplinkMsgLock.lock(); + this.inputStream.onNext(RequestMsg.newBuilder() + .setMsgType(RequestMsgType.UPLINK_RPC_MESSAGE) + .setDownlinkResponseMsg(downlinkResponseMsg) + .build()); + } finally { + uplinkMsgLock.unlock(); + } + } } diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java index c268b2a9e9..7aa017c3db 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java @@ -32,7 +32,9 @@ public interface EdgeRpcClient { Consumer onDownlink, Consumer onError); - void disconnect() throws InterruptedException; + void disconnect(boolean onError) throws InterruptedException; + + void sendSyncRequestMsg(); void sendUplinkMsg(UplinkMsg uplinkMsg); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 560522e021..c2ce170401 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -44,11 +44,17 @@ message ResponseMsg { ConnectResponseMsg connectResponseMsg = 1; UplinkResponseMsg uplinkResponseMsg = 2; DownlinkMsg downlinkMsg = 3; + EdgeUpdateMsg edgeUpdateMsg = 4; } enum RequestMsgType { CONNECT_RPC_MESSAGE = 0; UPLINK_RPC_MESSAGE = 1; + SYNC_REQUEST_RPC_MESSAGE = 2; +} + +message EdgeUpdateMsg { + EdgeConfiguration configuration = 1; } message ConnectRequestMsg { @@ -76,7 +82,9 @@ message EdgeConfiguration { string name = 5; string routingKey = 6; string type = 7; - string cloudType = 8; + string edgeLicenseKey = 8; + string cloudEndpoint = 9; + string cloudType = 10; } enum UpdateMsgType { @@ -169,6 +177,7 @@ message DeviceUpdateMsg { string name = 6; string type = 7; string label = 8; + string additionalInfo = 9; } message DeviceCredentialsUpdateMsg { @@ -188,6 +197,7 @@ message AssetUpdateMsg { string name = 6; string type = 7; string label = 8; + string additionalInfo = 9; } message EntityViewUpdateMsg { @@ -201,6 +211,7 @@ message EntityViewUpdateMsg { int64 entityIdMSB = 8; int64 entityIdLSB = 9; EdgeEntityType entityType = 10; + string additionalInfo = 11; } message AlarmUpdateMsg { diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 2a362e6483..55ff78e61d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -20,13 +20,21 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpHost; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; @@ -48,14 +56,9 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerDao; -import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entity.AbstractEntityService; -import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; @@ -65,10 +68,15 @@ import org.thingsboard.server.dao.tenant.TenantDao; import org.thingsboard.server.dao.user.UserService; import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import java.net.InetSocketAddress; +import java.net.Proxy; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -89,6 +97,10 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; + private RestTemplate restTemplate; + + private static final String EDGE_LICENSE_SERVER_ENDPOINT = "https://license.thingsboard.io"; + @Autowired private EdgeDao edgeDao; @@ -110,6 +122,17 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic @Autowired private RelationService relationService; + @Value("${edges.rpc.enabled:false}") + private boolean edgesRpcEnabled; + + @PostConstruct + public void init() { + super.init(); + if (edgesRpcEnabled) { + initRestTemplate(); + } + } + @Override public Edge findEdgeById(TenantId tenantId, EdgeId edgeId) { log.trace("Executing findEdgeById [{}]", edgeId); @@ -374,6 +397,12 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic if (StringUtils.isEmpty(edge.getRoutingKey())) { throw new DataValidationException("Edge routing key should be specified!"); } + if (StringUtils.isEmpty(edge.getEdgeLicenseKey())) { + throw new DataValidationException("Edge license key should be specified!"); + } + if (StringUtils.isEmpty(edge.getCloudEndpoint())) { + throw new DataValidationException("Cloud endpoint should be specified!"); + } if (edge.getTenantId() == null) { throw new DataValidationException("Edge should be assigned to tenant!"); } else { @@ -475,4 +504,55 @@ public class EdgeServiceImpl extends AbstractEntityService implements EdgeServic }, MoreExecutors.directExecutor()); } + @Override + public Object checkInstance(Object request) { + return this.restTemplate.postForEntity(EDGE_LICENSE_SERVER_ENDPOINT + "/api/license/checkInstance", request, Object.class, new Object[0]); + } + + @Override + public Object activateInstance(String edgeLicenseSecret, String releaseDate) { + Map params = new HashMap(); + params.put("licenseSecret", edgeLicenseSecret); + params.put("releaseDate", releaseDate); + return this.restTemplate.postForEntity(EDGE_LICENSE_SERVER_ENDPOINT + "/api/license/activateInstance?licenseSecret={licenseSecret}&releaseDate={releaseDate}", (Object) null, Object.class, params); + } + + private void initRestTemplate() { + boolean jdkHttpClientEnabled = org.apache.commons.lang3.StringUtils.isNotEmpty(System.getProperty("tb.proxy.jdk")) && System.getProperty("tb.proxy.jdk").equalsIgnoreCase("true"); + boolean systemProxyEnabled = org.apache.commons.lang3.StringUtils.isNotEmpty(System.getProperty("tb.proxy.system")) && System.getProperty("tb.proxy.system").equalsIgnoreCase("true"); + boolean proxyEnabled = org.apache.commons.lang3.StringUtils.isNotEmpty(System.getProperty("tb.proxy.host")) && org.apache.commons.lang3.StringUtils.isNotEmpty(System.getProperty("tb.proxy.port")); + if (jdkHttpClientEnabled) { + log.warn("Going to use plain JDK Http Client!"); + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + if (proxyEnabled) { + log.warn("Going to use Proxy Server: [{}:{}]", System.getProperty("tb.proxy.host"), System.getProperty("tb.proxy.port")); + factory.setProxy(new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(System.getProperty("tb.proxy.host"), Integer.parseInt(System.getProperty("tb.proxy.port"))))); + } + + this.restTemplate = new RestTemplate(new SimpleClientHttpRequestFactory()); + } else { + CloseableHttpClient httpClient; + HttpComponentsClientHttpRequestFactory requestFactory; + if (systemProxyEnabled) { + log.warn("Going to use System Proxy Server!"); + httpClient = HttpClients.createSystem(); + requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpClient(httpClient); + this.restTemplate = new RestTemplate(requestFactory); + } else if (proxyEnabled) { + log.warn("Going to use Proxy Server: [{}:{}]", System.getProperty("tb.proxy.host"), System.getProperty("tb.proxy.port")); + httpClient = HttpClients.custom().setSSLHostnameVerifier(new DefaultHostnameVerifier()).setProxy(new HttpHost(System.getProperty("tb.proxy.host"), Integer.parseInt(System.getProperty("tb.proxy.port")), "https")).build(); + requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpClient(httpClient); + this.restTemplate = new RestTemplate(requestFactory); + } else { + httpClient = HttpClients.custom().setSSLHostnameVerifier(new DefaultHostnameVerifier()).build(); + requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpClient(httpClient); + this.restTemplate = new RestTemplate(requestFactory); + } + } + + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 54a65077c5..688a22675d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -377,6 +377,8 @@ public class ModelConstants { public static final String EDGE_ROUTING_KEY_PROPERTY = "routing_key"; public static final String EDGE_SECRET_PROPERTY = "secret"; + public static final String EDGE_LICENSE_KEY_PROPERTY = "edge_license_key"; + public static final String EDGE_CLOUD_ENDPOINT_KEY_PROPERTY = "cloud_endpoint"; /** * Cassandra edge queue constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EdgeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EdgeEntity.java index 37e2d821ba..5480a9109f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EdgeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EdgeEntity.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.model.nosql; import com.datastax.driver.core.utils.UUIDs; -import com.datastax.driver.mapping.annotations.ClusteringColumn; import com.datastax.driver.mapping.annotations.Column; import com.datastax.driver.mapping.annotations.PartitionKey; import com.datastax.driver.mapping.annotations.Table; @@ -32,14 +31,13 @@ import org.thingsboard.server.dao.model.type.JsonCodec; import java.util.UUID; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TENANT_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ADDITIONAL_INFO_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CLOUD_ENDPOINT_KEY_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_COLUMN_FAMILY_NAME; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CONFIGURATION_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CUSTOMER_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_LABEL_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_LICENSE_KEY_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_NAME_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ROOT_RULE_CHAIN_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ROUTING_KEY_PROPERTY; @@ -84,6 +82,12 @@ public class EdgeEntity implements SearchTextEntity { @Column(name = EDGE_ROUTING_KEY_PROPERTY) private String routingKey; + @Column(name = EDGE_LICENSE_KEY_PROPERTY) + private String edgeLicenseKey; + + @Column(name = EDGE_CLOUD_ENDPOINT_KEY_PROPERTY) + private String cloudEndpoint; + @Column(name = EDGE_SECRET_PROPERTY) private String secret; @@ -125,6 +129,8 @@ public class EdgeEntity implements SearchTextEntity { this.label = edge.getLabel(); this.routingKey = edge.getRoutingKey(); this.secret = edge.getSecret(); + this.edgeLicenseKey = edge.getEdgeLicenseKey(); + this.cloudEndpoint = edge.getCloudEndpoint(); this.configuration = edge.getConfiguration(); this.additionalInfo = edge.getAdditionalInfo(); } @@ -152,6 +158,8 @@ public class EdgeEntity implements SearchTextEntity { edge.setLabel(label); edge.setRoutingKey(routingKey); edge.setSecret(secret); + edge.setEdgeLicenseKey(edgeLicenseKey); + edge.setCloudEndpoint(cloudEndpoint); edge.setConfiguration(configuration); edge.setAdditionalInfo(additionalInfo); return edge; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java index 6ede11a61f..76bd4f2961 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEntity.java @@ -36,9 +36,11 @@ import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CLOUD_ENDPOINT_KEY_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_COLUMN_FAMILY_NAME; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_CUSTOMER_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_LABEL_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_LICENSE_KEY_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_NAME_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ROOT_RULE_CHAIN_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_ROUTING_KEY_PROPERTY; @@ -81,6 +83,12 @@ public class EdgeEntity extends BaseSqlEntity implements SearchTextEntity< @Column(name = EDGE_SECRET_PROPERTY) private String secret; + @Column(name = EDGE_LICENSE_KEY_PROPERTY) + private String edgeLicenseKey; + + @Column(name = EDGE_CLOUD_ENDPOINT_KEY_PROPERTY) + private String cloudEndpoint; + @Type(type = "json") @Column(name = ModelConstants.EDGE_CONFIGURATION_PROPERTY) private JsonNode configuration; @@ -111,6 +119,8 @@ public class EdgeEntity extends BaseSqlEntity implements SearchTextEntity< this.label = edge.getLabel(); this.routingKey = edge.getRoutingKey(); this.secret = edge.getSecret(); + this.edgeLicenseKey = edge.getEdgeLicenseKey(); + this.cloudEndpoint = edge.getCloudEndpoint(); this.configuration = edge.getConfiguration(); this.additionalInfo = edge.getAdditionalInfo(); } @@ -147,6 +157,8 @@ public class EdgeEntity extends BaseSqlEntity implements SearchTextEntity< edge.setLabel(label); edge.setRoutingKey(routingKey); edge.setSecret(secret); + edge.setEdgeLicenseKey(edgeLicenseKey); + edge.setCloudEndpoint(cloudEndpoint); edge.setConfiguration(configuration); edge.setAdditionalInfo(additionalInfo); return edge; diff --git a/dao/src/main/resources/cassandra/schema-entities.cql b/dao/src/main/resources/cassandra/schema-entities.cql index a5518662ae..8ac0ad4956 100644 --- a/dao/src/main/resources/cassandra/schema-entities.cql +++ b/dao/src/main/resources/cassandra/schema-entities.cql @@ -737,6 +737,8 @@ CREATE TABLE IF NOT EXISTS thingsboard.edge ( search_text text, routing_key text, secret text, + edge_license_key text, + cloud_endpoint text, configuration text, additional_info text, PRIMARY KEY (id, tenant_id, customer_id, type) diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index 32855d2df3..8aea89a2b4 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -265,6 +265,8 @@ CREATE TABLE IF NOT EXISTS edge ( label varchar(255), routing_key varchar(255), secret varchar(255), + edge_license_key varchar(30), + cloud_endpoint varchar(255), search_text varchar(255), tenant_id varchar(31), CONSTRAINT edge_name_unq_key UNIQUE (tenant_id, name), diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index d1d33ff71f..4ec76cc845 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -265,6 +265,8 @@ CREATE TABLE IF NOT EXISTS edge ( label varchar(255), routing_key varchar(255), secret varchar(255), + edge_license_key varchar(30), + cloud_endpoint varchar(255), search_text varchar(255), tenant_id varchar(31), CONSTRAINT edge_name_unq_key UNIQUE (tenant_id, name), diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java index 0854ceaae2..af3b4dd291 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java @@ -358,6 +358,8 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { edge.setType("default"); edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setEdgeLicenseKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setCloudEndpoint("http://localhost:8080"); edge = edgeService.saveEdge(edge); try { dashboardService.assignDashboardToEdge(tenantId, dashboard.getId(), edge.getId()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java index 7d31ff4236..8e773d2fab 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java @@ -590,6 +590,8 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { edge.setType(type); edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setEdgeLicenseKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setCloudEndpoint("http://localhost:8080"); return edge; } diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index ab04b16b66..904e662e12 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -2029,9 +2029,9 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } - public Optional setRootRuleChain(RuleChainId ruleChainId, EdgeId edgeId) { + public Optional setRootRuleChain(EdgeId edgeId, RuleChainId ruleChainId) { try { - ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/{ruleChainId}/root", null, RuleChain.class, edgeId.getId(), ruleChainId.getId()); + ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/edge/{edgeId}/{ruleChainId}/root", null, Edge.class, edgeId.getId(), ruleChainId.getId()); return Optional.ofNullable(ruleChain.getBody()); } catch (HttpClientErrorException exception) { if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { @@ -2251,6 +2251,53 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { }, params).getBody(); } + public Optional addDefaultEdgeRuleChain(RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/ruleChain/{ruleChainId}/defaultEdge", null, RuleChain.class, ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public Optional removeDefaultEdgeRuleChain(RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.exchange(baseURL + "/api/ruleChain/{ruleChainId}/defaultEdge", HttpMethod.DELETE, HttpEntity.EMPTY, RuleChain.class, ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public List getDefaultEdgeRuleChains() { + return restTemplate.exchange(baseURL + "/api/ruleChain/defaultEdgeRuleChains", + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public Optional setDefaultRootEdgeRuleChain(RuleChainId ruleChainId) { + try { + ResponseEntity ruleChain = restTemplate.postForEntity(baseURL + "/api/ruleChain/{ruleChainId}/defaultRootEdge", null, RuleChain.class, ruleChainId.getId()); + return Optional.ofNullable(ruleChain.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + public TextPageData getTenantEdges(String type, TextPageLink pageLink) { Map params = new HashMap<>(); params.put("type", type); diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js index d5e4852afd..64610be2f5 100644 --- a/ui/src/app/common/utils.service.js +++ b/ui/src/app/common/utils.service.js @@ -33,7 +33,7 @@ export default angular.module('thingsboard.utils', [thingsboardTypes]) const varsRegex = /\$\{([^}]*)\}/g; /*@ngInject*/ -function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, types) { +function Utils($mdColorPalette, $rootScope, $window, $location, $translate, $q, $timeout, types) { var predefinedFunctions = {}, predefinedFunctionsList = [], @@ -142,6 +142,7 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t guid: guid, cleanCopy: cleanCopy, isLocalUrl: isLocalUrl, + baseUrl: baseUrl, validateDatasources: validateDatasources, createKey: createKey, createAdditionalDataKey: createAdditionalDataKey, @@ -435,6 +436,15 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t } } + function baseUrl() { + var url = $location.protocol() + '://' + $location.host(); + var port = $location.port(); + if (port != 80 && port != 443) { + url += ":" + port; + } + return url; + } + function validateDatasources(datasources) { datasources.forEach(function (datasource) { if (datasource.type === 'device') { diff --git a/ui/src/app/edge/edge-fieldset.tpl.html b/ui/src/app/edge/edge-fieldset.tpl.html index c151c03f10..1595b2aebd 100644 --- a/ui/src/app/edge/edge-fieldset.tpl.html +++ b/ui/src/app/edge/edge-fieldset.tpl.html @@ -83,6 +83,20 @@ + + + +
+
edge.edge-license-key-required
+
+
+ + + +
+
edge.cloud-endpoint-required
+
+
diff --git a/ui/src/app/edge/edge.directive.js b/ui/src/app/edge/edge.directive.js index d96dfa4b4d..98f010e009 100644 --- a/ui/src/app/edge/edge.directive.js +++ b/ui/src/app/edge/edge.directive.js @@ -35,6 +35,7 @@ export default function EdgeDirective($compile, $templateCache, $translate, $mdD if (!scope.edge.id) { scope.edge.routingKey = utils.guid(''); scope.edge.secret = generateSecret(20); + scope.edge.cloudEndpoint = utils.baseUrl(); } if (scope.edge.customerId && scope.edge.customerId.id !== types.id.nullUid) { scope.isAssignedToCustomer = true; diff --git a/ui/src/app/locale/locale.constant-de_DE.json b/ui/src/app/locale/locale.constant-de_DE.json index 1f9bdbae58..f8a1d887ff 100644 --- a/ui/src/app/locale/locale.constant-de_DE.json +++ b/ui/src/app/locale/locale.constant-de_DE.json @@ -747,6 +747,10 @@ "delete-edges-text": "Vorsicht, nach Bestätigung werden alle ausgewählten Rand entfernt und alle zugehörigen Daten werden nicht wiederhergestellt.", "name": "Name", "name-required": "Name ist erforderlich.", + "edge-license-key": "Edge Lizenzschlüssel", + "edge-license-key-required": "Edge Lizenzschlüssel ist erforderlich.", + "cloud-endpoint": "Cloud-Endpunkt", + "cloud-endpoint-required": "Cloud-Endpunkt ist erforderlich.", "description": "Beschreibung", "events": "Ereignisse", "details": "Details", diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 34ef9c98ef..c77192bffb 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -778,6 +778,10 @@ "delete-edges-text": "Be careful, after the confirmation all selected edges will be removed and all related data will become unrecoverable.", "name": "Name", "name-required": "Name is required.", + "edge-license-key": "Edge License Key", + "edge-license-key-required": "Edge License Key is required.", + "cloud-endpoint": "Cloud Endpoint", + "cloud-endpoint-required": "Cloud Endpoint is required.", "description": "Description", "entity-info": "Entity info", "details": "Details", diff --git a/ui/src/app/locale/locale.constant-es_ES.json b/ui/src/app/locale/locale.constant-es_ES.json index de16a8ab99..f8f4e6bbe9 100644 --- a/ui/src/app/locale/locale.constant-es_ES.json +++ b/ui/src/app/locale/locale.constant-es_ES.json @@ -760,6 +760,10 @@ "delete-edges-text": "Tenga cuidado, después de la confirmación se eliminarán todos los bordes seleccionados y todos los datos relacionados se volverán irrecuperables", "name": "Nombre", "name-required": "Se requiere nombre", + "edge-license-key": "Edge Clave de licencia", + "edge-license-key-required": "Se requiere edge clave de licencia", + "cloud-endpoint": "Punto final de la nube", + "cloud-endpoint-required": "Se requiere punto final de la nube", "description": "Descripción", "events": "Eventos", "details": "Detalles", diff --git a/ui/src/app/locale/locale.constant-fr_FR.json b/ui/src/app/locale/locale.constant-fr_FR.json index 444228966f..4058148bbd 100644 --- a/ui/src/app/locale/locale.constant-fr_FR.json +++ b/ui/src/app/locale/locale.constant-fr_FR.json @@ -765,6 +765,10 @@ "delete-edges-text": "Faites attention, après la confirmation, tous les bordures sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", "name": "Nom", "name-required": "Le nom de la bordure est requis", + "edge-license-key": "Edge Clé de licence", + "edge-license-key-required": "La edge clé de licence est requise", + "cloud-endpoint": "Clé de licence", + "cloud-endpoint-required": "La clé de licence est requise", "description": "Dispositifs", "events": "Événements", "details": "Détails de l'entité",