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..89c4c1e1b7 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 @@ -145,8 +145,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..3fb42a40b6 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 @@ -159,10 +159,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 +182,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) {} } }; } @@ -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/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/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..4c20e97b6a 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 @@ -81,7 +81,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 +91,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, @@ -179,8 +131,52 @@ 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() throws InterruptedException { + 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..a66f941922 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 @@ -34,6 +34,8 @@ public interface EdgeRpcClient { void disconnect() throws InterruptedException; + void sendSyncRequestMsg(); + void sendUplinkMsg(UplinkMsg uplinkMsg); void sendDownlinkResponseMsg(DownlinkResponseMsg downlinkResponseMsg); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 560522e021..30279ed10d 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -49,6 +49,7 @@ message ResponseMsg { enum RequestMsgType { CONNECT_RPC_MESSAGE = 0; UPLINK_RPC_MESSAGE = 1; + SYNC_REQUEST_RPC_MESSAGE = 2; } message ConnectRequestMsg { @@ -76,7 +77,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 +172,7 @@ message DeviceUpdateMsg { string name = 6; string type = 7; string label = 8; + string additionalInfo = 9; } message DeviceCredentialsUpdateMsg { @@ -188,6 +192,7 @@ message AssetUpdateMsg { string name = 6; string type = 7; string label = 8; + string additionalInfo = 9; } message EntityViewUpdateMsg { @@ -201,6 +206,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/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é",