Browse Source

Merge pull request #8757 from thingsboard/fix/snmp

Improvements for SNMP
pull/9217/head
Andrew Shvayka 3 years ago
committed by GitHub
parent
commit
1256f78552
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 49
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  2. 68
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  3. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  4. 4
      application/src/main/resources/thingsboard.yml
  5. 23
      common/cluster-api/src/main/proto/queue.proto
  6. 9
      common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
  7. 6
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java
  8. 4
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/importing/csv/BulkImportColumnType.java
  9. 18
      common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpCommunicationSpec.java
  10. 3
      common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMethod.java
  11. 3
      common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java
  12. 29
      common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/ToServerRpcRequestSnmpCommunicationConfig.java
  13. 32
      common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java
  14. 74
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java
  15. 97
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/PduService.java
  16. 221
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java
  17. 31
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java
  18. 23
      common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java
  19. 43
      common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java
  20. 7
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
  21. 49
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  22. 2
      docker/docker-compose.yml
  23. 1
      docker/tb-snmp-transport.env
  24. 4
      transport/snmp/src/main/resources/tb-snmp-transport.yml
  25. 8
      ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts
  26. 4
      ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts
  27. 12
      ui-ngx/src/app/shared/models/device.models.ts
  28. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json

49
application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java

@ -33,7 +33,10 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode;
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
@ -45,6 +48,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
@ -76,18 +80,18 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
private final Lock findOrCreateDeviceProfileLock = new ReentrantLock();
@Override
protected void setEntityFields(Device entity, Map<BulkImportColumnType, String> fields) {
ObjectNode additionalInfo = getOrCreateAdditionalInfoObj(entity);
protected void setEntityFields(Device device, Map<BulkImportColumnType, String> fields) {
ObjectNode additionalInfo = getOrCreateAdditionalInfoObj(device);
fields.forEach((columnType, value) -> {
switch (columnType) {
case NAME:
entity.setName(value);
device.setName(value);
break;
case TYPE:
entity.setType(value);
device.setType(value);
break;
case LABEL:
entity.setLabel(value);
device.setLabel(value);
break;
case DESCRIPTION:
additionalInfo.set("description", new TextNode(value));
@ -96,16 +100,17 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
additionalInfo.set("gateway", BooleanNode.valueOf(Boolean.parseBoolean(value)));
break;
}
entity.setAdditionalInfo(additionalInfo);
device.setAdditionalInfo(additionalInfo);
});
setUpDeviceConfiguration(device, fields);
}
@Override
@SneakyThrows
protected Device saveEntity(SecurityUser user, Device entity, Map<BulkImportColumnType, String> fields) {
protected Device saveEntity(SecurityUser user, Device device, Map<BulkImportColumnType, String> fields) {
DeviceCredentials deviceCredentials;
try {
deviceCredentials = createDeviceCredentials(entity.getTenantId(), entity.getId(), fields);
deviceCredentials = createDeviceCredentials(device.getTenantId(), device.getId(), fields);
deviceCredentialsService.formatCredentials(deviceCredentials);
} catch (Exception e) {
throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage());
@ -113,15 +118,15 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
DeviceProfile deviceProfile;
if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) {
deviceProfile = setUpLwM2mDeviceProfile(entity.getTenantId(), entity);
} else if (StringUtils.isNotEmpty(entity.getType())) {
deviceProfile = deviceProfileService.findOrCreateDeviceProfile(entity.getTenantId(), entity.getType());
deviceProfile = setUpLwM2mDeviceProfile(device.getTenantId(), device);
} else if (StringUtils.isNotEmpty(device.getType())) {
deviceProfile = deviceProfileService.findOrCreateDeviceProfile(device.getTenantId(), device.getType());
} else {
deviceProfile = deviceProfileService.findDefaultDeviceProfile(entity.getTenantId());
deviceProfile = deviceProfileService.findDefaultDeviceProfile(device.getTenantId());
}
entity.setDeviceProfileId(deviceProfile.getId());
device.setDeviceProfileId(deviceProfile.getId());
return tbDeviceService.saveDeviceWithCredentials(entity, deviceCredentials, user);
return tbDeviceService.saveDeviceWithCredentials(device, deviceCredentials, user);
}
@Override
@ -136,6 +141,22 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
entity.setCustomerId(user.getCustomerId());
}
private void setUpDeviceConfiguration(Device device, Map<BulkImportColumnType, String> fields) {
if (fields.containsKey(BulkImportColumnType.SNMP_HOST)) {
SnmpDeviceTransportConfiguration transportConfiguration = new SnmpDeviceTransportConfiguration();
transportConfiguration.setHost(fields.get(BulkImportColumnType.SNMP_HOST));
transportConfiguration.setPort(Optional.ofNullable(fields.get(BulkImportColumnType.SNMP_PORT))
.map(Integer::parseInt).orElse(161));
transportConfiguration.setProtocolVersion(Optional.ofNullable(fields.get(BulkImportColumnType.SNMP_VERSION))
.map(version -> SnmpProtocolVersion.valueOf(version.toUpperCase())).orElse(SnmpProtocolVersion.V2C));
transportConfiguration.setCommunity(fields.getOrDefault(BulkImportColumnType.SNMP_COMMUNITY_STRING, "public"));
DeviceData deviceData = new DeviceData();
deviceData.setTransportConfiguration(transportConfiguration);
device.setDeviceData(deviceData);
}
}
@SneakyThrows
private DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceId deviceId, Map<BulkImportColumnType, String> fields) {
DeviceCredentials credentials = new DeviceCredentials();

68
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -24,10 +24,14 @@ import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.event.ErrorEvent;
import org.thingsboard.server.common.data.event.Event;
import org.thingsboard.server.common.data.event.LifecycleEvent;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId;
@ -45,7 +49,9 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.EdgeNotificationMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.ErrorEventProto;
import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto;
import org.thingsboard.server.gen.transport.TransportProtos.LifecycleEventProto;
import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionMgrMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmDeleteProto;
@ -274,6 +280,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg();
log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg());
forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback);
} else if (toCoreMsg.hasErrorEventMsg()) {
forwardToEventService(toCoreMsg.getErrorEventMsg(), callback);
} else if (toCoreMsg.hasLifecycleEventMsg()) {
forwardToEventService(toCoreMsg.getLifecycleEventMsg(), callback);
}
} catch (Throwable e) {
log.warn("[{}] Failed to process message: {}", id, msg, e);
@ -519,50 +529,52 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
subscriptionManagerService.cancelSubscription(closeProto.getSessionId(), closeProto.getSubscriptionId(), callback);
} else if (msg.hasTsUpdate()) {
TbTimeSeriesUpdateProto proto = msg.getTsUpdate();
long tenantIdMSB = proto.getTenantIdMSB();
long tenantIdLSB = proto.getTenantIdLSB();
subscriptionManagerService.onTimeSeriesUpdate(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
toTenantId(tenantIdMSB, tenantIdLSB),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
TbSubscriptionUtils.toTsKvEntityList(proto.getDataList()), callback);
} else if (msg.hasAttrUpdate()) {
TbAttributeUpdateProto proto = msg.getAttrUpdate();
subscriptionManagerService.onAttributesUpdate(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getScope(), TbSubscriptionUtils.toAttributeKvList(proto.getDataList()), callback);
} else if (msg.hasAttrDelete()) {
TbAttributeDeleteProto proto = msg.getAttrDelete();
subscriptionManagerService.onAttributesDelete(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getScope(), proto.getKeysList(), proto.getNotifyDevice(), callback);
} else if (msg.hasTsDelete()) {
TbTimeSeriesDeleteProto proto = msg.getTsDelete();
subscriptionManagerService.onTimeSeriesDelete(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getKeysList(), callback);
} else if (msg.hasAlarmUpdate()) {
TbAlarmUpdateProto proto = msg.getAlarmUpdate();
subscriptionManagerService.onAlarmUpdate(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class),
callback);
} else if (msg.hasAlarmDelete()) {
TbAlarmDeleteProto proto = msg.getAlarmDelete();
subscriptionManagerService.onAlarmDeleted(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class), callback);
} else if (msg.hasNotificationUpdate()) {
TransportProtos.NotificationUpdateProto updateProto = msg.getNotificationUpdate();
TenantId tenantId = TenantId.fromUUID(new UUID(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB()));
TenantId tenantId = toTenantId(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB());
UserId recipientId = new UserId(new UUID(updateProto.getRecipientIdMSB(), updateProto.getRecipientIdLSB()));
NotificationUpdate update = JacksonUtil.fromString(updateProto.getUpdate(), NotificationUpdate.class);
subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, callback);
} else if (msg.hasNotificationRequestUpdate()) {
TransportProtos.NotificationRequestUpdateProto updateProto = msg.getNotificationRequestUpdate();
TenantId tenantId = TenantId.fromUUID(new UUID(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB()));
TenantId tenantId = toTenantId(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB());
NotificationRequestUpdate update = JacksonUtil.fromString(updateProto.getUpdate(), NotificationRequestUpdate.class);
subscriptionManagerService.onNotificationRequestUpdate(tenantId, update, callback);
} else {
@ -584,7 +596,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
if (statsEnabled) {
stats.log(deviceActivityMsg);
}
TenantId tenantId = TenantId.fromUUID(new UUID(deviceActivityMsg.getTenantIdMSB(), deviceActivityMsg.getTenantIdLSB()));
TenantId tenantId = toTenantId(deviceActivityMsg.getTenantIdMSB(), deviceActivityMsg.getTenantIdLSB());
DeviceId deviceId = new DeviceId(new UUID(deviceActivityMsg.getDeviceIdMSB(), deviceActivityMsg.getDeviceIdLSB()));
try {
stateService.onDeviceActivity(tenantId, deviceId, deviceActivityMsg.getLastActivityTime());
@ -595,7 +607,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
}
private void forwardToNotificationSchedulerService(TransportProtos.NotificationSchedulerServiceMsg msg, TbCallback callback) {
TenantId tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB()));
TenantId tenantId = toTenantId(msg.getTenantIdMSB(), msg.getTenantIdLSB());
NotificationRequestId notificationRequestId = new NotificationRequestId(new UUID(msg.getRequestIdMSB(), msg.getRequestIdLSB()));
try {
notificationSchedulerService.scheduleNotificationRequest(tenantId, notificationRequestId, msg.getTs());
@ -627,11 +639,47 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
callback.onSuccess();
}
private void forwardToEventService(ErrorEventProto eventProto, TbCallback callback) {
Event event = ErrorEvent.builder()
.tenantId(toTenantId(eventProto.getTenantIdMSB(), eventProto.getTenantIdLSB()))
.entityId(new UUID(eventProto.getEntityIdMSB(), eventProto.getEntityIdLSB()))
.serviceId(eventProto.getServiceId())
.ts(System.currentTimeMillis())
.method(eventProto.getMethod())
.error(eventProto.getError())
.build();
forwardToEventService(event, callback);
}
private void forwardToEventService(LifecycleEventProto eventProto, TbCallback callback) {
Event event = LifecycleEvent.builder()
.tenantId(toTenantId(eventProto.getTenantIdMSB(), eventProto.getTenantIdLSB()))
.entityId(new UUID(eventProto.getEntityIdMSB(), eventProto.getEntityIdLSB()))
.serviceId(eventProto.getServiceId())
.ts(System.currentTimeMillis())
.lcEventType(eventProto.getLcEventType())
.success(eventProto.getSuccess())
.error(StringUtils.isNotEmpty(eventProto.getError()) ? eventProto.getError() : null)
.build();
forwardToEventService(event, callback);
}
private void forwardToEventService(Event event, TbCallback callback) {
DonAsynchron.withCallback(actorContext.getEventService().saveAsync(event),
result -> callback.onSuccess(),
callback::onFailure,
actorContext.getDbCallbackExecutor());
}
private void throwNotHandled(Object msg, TbCallback callback) {
log.warn("Message not handled: {}", msg);
callback.onFailure(new RuntimeException("Message not handled!"));
}
private TenantId toTenantId(long tenantIdMSB, long tenantIdLSB) {
return TenantId.fromUUID(new UUID(tenantIdMSB, tenantIdLSB));
}
@Override
protected void stopMainConsumers() {
if (mainConsumer != null) {

5
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java

@ -22,6 +22,7 @@ import com.google.gson.JsonPrimitive;
import lombok.Data;
import lombok.SneakyThrows;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
@ -57,7 +58,7 @@ import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import org.thingsboard.server.utils.CsvUtils;
import org.thingsboard.server.utils.TypeCastUtil;
import org.thingsboard.server.common.data.util.TypeCastUtil;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
@ -269,7 +270,7 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
if (!entry.getKey().getType().isKv()) {
entityData.getFields().put(entry.getKey().getType(), entry.getValue());
} else {
Map.Entry<DataType, Object> castResult = TypeCastUtil.castValue(entry.getValue());
Pair<DataType, Object> castResult = TypeCastUtil.castValue(entry.getValue());
entityData.getKvs().put(entry.getKey(), new ParsedValue(castResult.getValue(), castResult.getKey()));
}
});

4
application/src/main/resources/thingsboard.yml

@ -991,11 +991,15 @@ transport:
# value: "${LWM2M_PROTOCOL_STAGE_THREAD_COUNT:4}"
snmp:
enabled: "${SNMP_ENABLED:true}"
bind_port: "${SNMP_BIND_PORT:1620}"
response_processing:
# parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices
parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}"
# to configure SNMP to work over UDP or TCP
underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}"
max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}"
response:
ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}"
stats:
enabled: "${TB_TRANSPORT_STATS_ENABLED:true}"
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"

23
common/cluster-api/src/main/proto/queue.proto

@ -975,6 +975,8 @@ message ToCoreMsg {
EdgeNotificationMsgProto edgeNotificationMsg = 5;
DeviceActivityProto deviceActivityMsg = 6;
NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = 7;
LifecycleEventProto lifecycleEventMsg = 8;
ErrorEventProto errorEventMsg = 9;
}
/* High priority messages with low latency are handled by ThingsBoard Core Service separately */
@ -1066,3 +1068,24 @@ message NotificationSchedulerServiceMsg {
message NotificationRuleProcessorMsg {
bytes trigger = 1;
}
message ErrorEventProto {
int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2;
int64 entityIdMSB = 3;
int64 entityIdLSB = 4;
string serviceId = 5;
string method = 6;
string error = 7;
}
message LifecycleEventProto {
int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2;
int64 entityIdMSB = 3;
int64 entityIdLSB = 4;
string serviceId = 5;
string lcEventType = 6;
bool success = 7;
string error = 8;
}

9
common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java

@ -163,6 +163,15 @@ public class StringUtils {
return false;
}
public static boolean equalsAnyIgnoreCase(String string, String... otherStrings) {
for (String otherString : otherStrings) {
if (equalsIgnoreCase(string, otherString)) {
return true;
}
}
return false;
}
public static String substringAfterLast(String str, String sep) {
return org.apache.commons.lang3.StringUtils.substringAfterLast(str, sep);
}

6
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java

@ -18,7 +18,6 @@ package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.transport.snmp.SnmpMapping;
import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig;
import java.util.List;
@ -45,8 +44,7 @@ public class SnmpDeviceProfileTransportConfiguration implements DeviceProfileTra
private boolean isValid() {
return timeoutMs != null && timeoutMs >= 0 && retries != null && retries >= 0
&& communicationConfigs != null
&& communicationConfigs.stream().allMatch(config -> config != null && config.isValid())
&& communicationConfigs.stream().flatMap(config -> config.getAllMappings().stream()).map(SnmpMapping::getOid)
.distinct().count() == communicationConfigs.stream().mapToInt(config -> config.getAllMappings().size()).sum();
&& communicationConfigs.stream().allMatch(config -> config != null && config.isValid());
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/importing/csv/BulkImportColumnType.java

@ -43,6 +43,10 @@ public enum BulkImportColumnType {
LWM2M_SERVER_SECURITY_MODE("securityMode", LwM2MSecurityMode.NO_SEC.name()),
LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID("clientPublicKeyOrId"),
LWM2M_SERVER_CLIENT_SECRET_KEY("clientSecretKey"),
SNMP_HOST,
SNMP_PORT,
SNMP_VERSION,
SNMP_COMMUNITY_STRING,
IS_GATEWAY,
DESCRIPTION,
ROUTING_KEY,

18
common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpCommunicationSpec.java

@ -15,11 +15,21 @@
*/
package org.thingsboard.server.common.data.transport.snmp;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum SnmpCommunicationSpec {
TELEMETRY_QUERYING,
CLIENT_ATTRIBUTES_QUERYING,
SHARED_ATTRIBUTES_SETTING,
TELEMETRY_QUERYING("telemetryQuerying"),
CLIENT_ATTRIBUTES_QUERYING("clientAttributesQuerying"),
SHARED_ATTRIBUTES_SETTING("sharedAttributesSetting"),
TO_DEVICE_RPC_REQUEST("toDeviceRpcRequest"),
TO_SERVER_RPC_REQUEST("toServerRpcRequest");
private final String label;
TO_DEVICE_RPC_REQUEST,
}

3
common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMethod.java

@ -17,7 +17,8 @@ package org.thingsboard.server.common.data.transport.snmp;
public enum SnmpMethod {
GET(-96),
SET(-93);
SET(-93),
TRAP(-89);
// codes taken from org.snmp4j.PDU class
private final int code;

3
common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java

@ -37,7 +37,8 @@ import java.util.List;
@Type(value = TelemetryQueryingSnmpCommunicationConfig.class, name = "TELEMETRY_QUERYING"),
@Type(value = ClientAttributesQueryingSnmpCommunicationConfig.class, name = "CLIENT_ATTRIBUTES_QUERYING"),
@Type(value = SharedAttributesSettingSnmpCommunicationConfig.class, name = "SHARED_ATTRIBUTES_SETTING"),
@Type(value = ToDeviceRpcRequestSnmpCommunicationConfig.class, name = "TO_DEVICE_RPC_REQUEST")
@Type(value = ToDeviceRpcRequestSnmpCommunicationConfig.class, name = "TO_DEVICE_RPC_REQUEST"),
@Type(value = ToServerRpcRequestSnmpCommunicationConfig.class, name = "TO_SERVER_RPC_REQUEST")
})
public interface SnmpCommunicationConfig extends Serializable {

29
common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/ToServerRpcRequestSnmpCommunicationConfig.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.transport.snmp.config;
import org.thingsboard.server.common.data.transport.snmp.SnmpCommunicationSpec;
public class ToServerRpcRequestSnmpCommunicationConfig extends MultipleMappingsSnmpCommunicationConfig {
private static final long serialVersionUID = 4851028734093214L;
@Override
public SnmpCommunicationSpec getSpec() {
return SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST;
}
}

32
application/src/main/java/org/thingsboard/server/utils/TypeCastUtil.java → common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java

@ -13,35 +13,53 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.utils;
package org.thingsboard.server.common.data.util;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.thingsboard.server.common.data.kv.DataType;
import java.math.BigDecimal;
import java.util.Map;
public class TypeCastUtil {
private TypeCastUtil() {}
public static Map.Entry<DataType, Object> castValue(String value) {
public static Pair<DataType, Object> castValue(String value) {
if (isNumber(value)) {
String formattedValue = value.replace(',', '.');
try {
BigDecimal bd = new BigDecimal(formattedValue);
if (bd.stripTrailingZeros().scale() > 0 || isSimpleDouble(formattedValue)) {
if (bd.scale() <= 16) {
return Map.entry(DataType.DOUBLE, bd.doubleValue());
return Pair.of(DataType.DOUBLE, bd.doubleValue());
}
} else {
return Map.entry(DataType.LONG, bd.longValueExact());
return Pair.of(DataType.LONG, bd.longValueExact());
}
} catch (RuntimeException ignored) {}
} else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
return Map.entry(DataType.BOOLEAN, Boolean.parseBoolean(value));
return Pair.of(DataType.BOOLEAN, Boolean.parseBoolean(value));
}
return Pair.of(DataType.STRING, value);
}
public static Pair<DataType, Number> castToNumber(String value) {
if (isNumber(value)) {
String formattedValue = value.replace(',', '.');
BigDecimal bd = new BigDecimal(formattedValue);
if (bd.stripTrailingZeros().scale() > 0 || isSimpleDouble(formattedValue)) {
if (bd.scale() <= 16) {
return Pair.of(DataType.DOUBLE, bd.doubleValue());
} else {
return Pair.of(DataType.DOUBLE, bd);
}
} else {
return Pair.of(DataType.LONG, bd.longValueExact());
}
} else {
throw new IllegalArgumentException("'" + value + "' can't be parsed as number");
}
return Map.entry(DataType.STRING, value);
}
private static boolean isNumber(String value) {

74
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfigu
import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.transport.DeviceUpdatedEvent;
@ -113,19 +114,25 @@ public class SnmpTransportContext extends TransportContext {
SnmpDeviceProfileTransportConfiguration profileTransportConfiguration = (SnmpDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
SnmpDeviceTransportConfiguration deviceTransportConfiguration = (SnmpDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration();
DeviceSessionContext deviceSessionContext;
DeviceSessionContext sessionContext;
try {
deviceSessionContext = new DeviceSessionContext(
device, deviceProfile, credentials.getCredentialsId(),
profileTransportConfiguration, deviceTransportConfiguration, this
);
registerSessionMsgListener(deviceSessionContext);
sessionContext = DeviceSessionContext.builder()
.tenantId(deviceProfile.getTenantId())
.device(device)
.deviceProfile(deviceProfile)
.token(credentials.getCredentialsId())
.profileTransportConfiguration(profileTransportConfiguration)
.deviceTransportConfiguration(deviceTransportConfiguration)
.snmpTransportContext(this)
.build();
registerSessionMsgListener(sessionContext);
} catch (Exception e) {
log.error("Failed to establish session for SNMP device {}: {}", device.getId(), e.toString());
transportService.errorEvent(device.getTenantId(), device.getId(), "sessionEstablishing", e);
return;
}
sessions.put(device.getId(), deviceSessionContext);
snmpTransportService.createQueryingTasks(deviceSessionContext);
sessions.put(device.getId(), sessionContext);
snmpTransportService.createQueryingTasks(sessionContext);
log.info("Established SNMP device session for device {}", device.getId());
}
@ -148,14 +155,17 @@ public class SnmpTransportContext extends TransportContext {
sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration);
snmpTransportService.cancelQueryingTasks(sessionContext);
snmpTransportService.createQueryingTasks(sessionContext);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null);
} else if (!newDeviceTransportConfiguration.equals(sessionContext.getDeviceTransportConfiguration())) {
sessionContext.setDeviceTransportConfiguration(newDeviceTransportConfiguration);
sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null);
} else {
log.trace("Configuration of the device {} was not updated", device);
}
} catch (Exception e) {
log.error("Failed to update session for SNMP device {}: {}", sessionContext.getDeviceId(), e.getMessage());
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, false, e);
destroyDeviceSession(sessionContext);
}
}
@ -168,43 +178,58 @@ public class SnmpTransportContext extends TransportContext {
transportService.deregisterSession(sessionContext.getSessionInfo());
snmpTransportService.cancelQueryingTasks(sessionContext);
sessions.remove(sessionContext.getDeviceId());
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.STOPPED, true, null);
log.trace("Unregistered and removed session");
}
private void registerSessionMsgListener(DeviceSessionContext deviceSessionContext) {
private void registerSessionMsgListener(DeviceSessionContext sessionContext) {
transportService.process(DeviceTransportType.SNMP,
TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceSessionContext.getToken()).build(),
TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(sessionContext.getToken()).build(),
new TransportServiceCallback<>() {
@Override
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
if (msg.hasDeviceInfo()) {
SessionInfoProto sessionInfo = SessionInfoCreator.create(
msg, SnmpTransportContext.this, UUID.randomUUID()
);
transportService.registerAsyncSession(sessionInfo, deviceSessionContext);
transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), TransportServiceCallback.EMPTY);
transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder().build(), TransportServiceCallback.EMPTY);
deviceSessionContext.setSessionInfo(sessionInfo);
deviceSessionContext.setDeviceInfo(msg.getDeviceInfo());
deviceSessionContext.setConnected(true);
registerTransportSession(sessionContext, msg);
sessionContext.setSessionTimeoutHandler(() -> {
registerTransportSession(sessionContext, msg);
});
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.STARTED, true, null);
} else {
log.warn("[{}] Failed to process device auth", deviceSessionContext.getDeviceId());
log.warn("[{}] Failed to process device auth", sessionContext.getDeviceId());
}
}
@Override
public void onError(Throwable e) {
log.warn("[{}] Failed to process device auth: {}", deviceSessionContext.getDeviceId(), e);
log.warn("[{}] Failed to process device auth: {}", sessionContext.getDeviceId(), e);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.STARTED, false, e);
}
});
}
private void registerTransportSession(DeviceSessionContext deviceSessionContext, ValidateDeviceCredentialsResponse msg) {
SessionInfoProto sessionInfo = SessionInfoCreator.create(
msg, SnmpTransportContext.this, UUID.randomUUID()
);
log.debug("Registering transport session: {}", sessionInfo);
transportService.registerAsyncSession(sessionInfo, deviceSessionContext);
transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder()
.setSessionType(TransportProtos.SessionType.ASYNC)
.build(), TransportServiceCallback.EMPTY);
transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder()
.setSessionType(TransportProtos.SessionType.ASYNC)
.build(), TransportServiceCallback.EMPTY);
deviceSessionContext.setSessionInfo(sessionInfo);
deviceSessionContext.setDeviceInfo(msg.getDeviceInfo());
deviceSessionContext.setConnected(true);
}
@EventListener(DeviceUpdatedEvent.class)
public void onDeviceUpdatedOrCreated(DeviceUpdatedEvent deviceUpdatedEvent) {
Device device = deviceUpdatedEvent.getDevice();
log.trace("Got creating or updating device event for device {}", device);
log.debug("Got creating or updating device event for device {}", device);
DeviceTransportType transportType = Optional.ofNullable(device.getDeviceData().getTransportConfiguration())
.map(DeviceTransportConfiguration::getType)
.orElse(null);
@ -238,6 +263,7 @@ public class SnmpTransportContext extends TransportContext {
}
public void onDeviceProfileUpdated(DeviceProfile deviceProfile, DeviceSessionContext sessionContext) {
log.debug("Handling device profile {} update event for device {}", deviceProfile.getId(), sessionContext.getDeviceId());
updateDeviceSession(sessionContext, sessionContext.getDevice(), deviceProfile);
}

97
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/PduService.java

@ -15,7 +15,9 @@
*/
package org.thingsboard.server.transport.snmp.service;
import com.google.common.collect.Lists;
import com.google.gson.JsonObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.snmp4j.PDU;
import org.snmp4j.ScopedPDU;
@ -25,43 +27,61 @@ import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.Variable;
import org.snmp4j.smi.VariableBinding;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.transport.snmp.SnmpMapping;
import org.thingsboard.server.common.data.transport.snmp.SnmpMethod;
import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion;
import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig;
import org.thingsboard.server.common.data.util.TypeCastUtil;
import org.thingsboard.server.queue.util.TbSnmpTransportComponent;
import org.thingsboard.server.transport.snmp.session.DeviceSessionContext;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@TbSnmpTransportComponent
@Service
@Slf4j
@RequiredArgsConstructor
public class PduService {
public PDU createPdu(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map<String, String> values) {
PDU pdu = setUpPdu(sessionContext);
pdu.setType(communicationConfig.getMethod().getCode());
pdu.addAll(communicationConfig.getAllMappings().stream()
.filter(mapping -> values.isEmpty() || values.containsKey(mapping.getKey()))
.map(mapping -> Optional.ofNullable(values.get(mapping.getKey()))
.map(value -> {
Variable variable = toSnmpVariable(value, mapping.getDataType());
return new VariableBinding(new OID(mapping.getOid()), variable);
})
.orElseGet(() -> new VariableBinding(new OID(mapping.getOid()))))
.collect(Collectors.toList()));
@Value("${transport.snmp.max_request_oids:100}")
private int maxRequestOids;
@Value("${transport.snmp.response.ignore_type_cast_errors:false}")
private boolean ignoreTypeCastErrors;
public List<PDU> createPdus(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map<String, String> values) {
List<PDU> pdus = new ArrayList<>();
List<SnmpMapping> allMappings = communicationConfig.getAllMappings();
for (List<SnmpMapping> mappings : Lists.partition(allMappings, maxRequestOids)) {
PDU pdu = setUpPdu(sessionContext);
pdu.setType(communicationConfig.getMethod().getCode());
pdu.addAll(mappings.stream()
.filter(mapping -> values.isEmpty() || values.containsKey(mapping.getKey()))
.map(mapping -> Optional.ofNullable(values.get(mapping.getKey()))
.map(value -> {
Variable variable = toSnmpVariable(value, mapping.getDataType());
return new VariableBinding(new OID(mapping.getOid()), variable);
})
.orElseGet(() -> new VariableBinding(new OID(mapping.getOid()))))
.collect(Collectors.toList()));
if (pdu.size() > 0) {
pdus.add(pdu);
}
}
return pdu;
return pdus;
}
public PDU createSingleVariablePdu(DeviceSessionContext sessionContext, SnmpMethod snmpMethod, String oid, String value, DataType dataType) {
@ -116,8 +136,8 @@ public class PduService {
}
public JsonObject processPdu(PDU pdu, List<SnmpMapping> responseMappings) {
Map<OID, String> values = processPdu(pdu);
public JsonObject processPdus(List<PDU> pdus, List<SnmpMapping> responseMappings) {
Map<OID, String> values = processPdus(pdus);
Map<OID, SnmpMapping> mappings = new HashMap<>();
if (responseMappings != null) {
@ -143,29 +163,40 @@ public class PduService {
return data;
}
public Map<OID, String> processPdu(PDU pdu) {
return IntStream.range(0, pdu.size())
.mapToObj(pdu::get)
public Map<OID, String> processPdus(List<PDU> pdus) {
return pdus.stream()
.flatMap(pdu -> pdu.getVariableBindings().stream())
.filter(Objects::nonNull)
.filter(variableBinding -> !(variableBinding.getVariable() instanceof Null))
.collect(Collectors.toMap(VariableBinding::getOid, VariableBinding::toValueString));
}
public void processValue(String key, DataType dataType, String value, JsonObject result) {
switch (dataType) {
case LONG:
result.addProperty(key, Long.parseLong(value));
break;
case BOOLEAN:
result.addProperty(key, Boolean.parseBoolean(value));
break;
case DOUBLE:
result.addProperty(key, Double.parseDouble(value));
break;
case STRING:
case JSON:
default:
result.addProperty(key, value);
try {
switch (dataType) {
case STRING:
case JSON:
result.addProperty(key, value);
break;
case LONG:
case DOUBLE:
result.addProperty(key, TypeCastUtil.castToNumber(value).getValue());
break;
case BOOLEAN:
if (StringUtils.equalsAnyIgnoreCase(value, "true", "false")) {
result.addProperty(key, Boolean.parseBoolean(value));
} else {
throw new IllegalArgumentException("Can't parse '" + value + "' as boolean");
}
break;
}
} catch (IllegalArgumentException e) {
if (ignoreTypeCastErrors) {
log.debug("Ignoring value '{}' for key '{}' because of data type mismatch ({} required)", value, key, dataType);
} else {
throw e;
}
}
}
}

221
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java

@ -17,10 +17,13 @@ package org.thingsboard.server.transport.snmp.service;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.snmp4j.CommandResponder;
import org.snmp4j.CommandResponderEvent;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
import org.snmp4j.TransportMapping;
@ -29,10 +32,15 @@ import org.snmp4j.mp.MPv3;
import org.snmp4j.security.SecurityModels;
import org.snmp4j.security.SecurityProtocols;
import org.snmp4j.security.USM;
import org.snmp4j.smi.Address;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.TcpAddress;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.transport.DefaultTcpTransportMapping;
import org.snmp4j.transport.DefaultUdpTransportMapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
@ -45,15 +53,16 @@ import org.thingsboard.server.common.data.transport.snmp.SnmpMethod;
import org.thingsboard.server.common.data.transport.snmp.config.RepeatingQueryingSnmpCommunicationConfig;
import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbSnmpTransportComponent;
import org.thingsboard.server.transport.snmp.SnmpTransportContext;
import org.thingsboard.server.transport.snmp.session.DeviceSessionContext;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
@ -71,9 +80,11 @@ import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class SnmpTransportService implements TbTransportService {
public class SnmpTransportService implements TbTransportService, CommandResponder {
private final TransportService transportService;
private final PduService pduService;
@Autowired @Lazy
private SnmpTransportContext transportContext;
@Getter
private Snmp snmp;
@ -83,6 +94,8 @@ public class SnmpTransportService implements TbTransportService {
private final Map<SnmpCommunicationSpec, ResponseDataMapper> responseDataMappers = new EnumMap<>(SnmpCommunicationSpec.class);
private final Map<SnmpCommunicationSpec, ResponseProcessor> responseProcessors = new EnumMap<>(SnmpCommunicationSpec.class);
@Value("${transport.snmp.bind_port:1620}")
private Integer snmpBindPort;
@Value("${transport.snmp.response_processing.parallelism_level}")
private Integer responseProcessingParallelismLevel;
@Value("${transport.snmp.underlying_protocol}")
@ -114,15 +127,16 @@ public class SnmpTransportService implements TbTransportService {
TransportMapping<?> transportMapping;
switch (snmpUnderlyingProtocol) {
case "udp":
transportMapping = new DefaultUdpTransportMapping();
transportMapping = new DefaultUdpTransportMapping(new UdpAddress(snmpBindPort));
break;
case "tcp":
transportMapping = new DefaultTcpTransportMapping();
transportMapping = new DefaultTcpTransportMapping(new TcpAddress(snmpBindPort));
break;
default:
throw new IllegalArgumentException("Underlying protocol " + snmpUnderlyingProtocol + " for SNMP is not supported");
}
snmp = new Snmp(transportMapping);
snmp.addNotificationListener(transportMapping, transportMapping.getListenAddress(), this);
snmp.listen();
USM usm = new USM(SecurityProtocols.getInstance(), new OctetString(MPv3.createLocalEngineID()), 0);
@ -143,6 +157,7 @@ public class SnmpTransportService implements TbTransportService {
}
} catch (Exception e) {
log.error("Failed to send SNMP request for device {}: {}", sessionContext.getDeviceId(), e.toString());
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), config.getSpec().getLabel(), e);
}
}, queryingFrequency, queryingFrequency, TimeUnit.MILLISECONDS);
})
@ -161,18 +176,24 @@ public class SnmpTransportService implements TbTransportService {
}
private void sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map<String, String> values) {
PDU request = pduService.createPdu(sessionContext, communicationConfig, values);
RequestInfo requestInfo = new RequestInfo(communicationConfig.getSpec(), communicationConfig.getAllMappings());
sendRequest(sessionContext, request, requestInfo);
List<PDU> request = pduService.createPdus(sessionContext, communicationConfig, values);
RequestContext requestContext = RequestContext.builder()
.communicationSpec(communicationConfig.getSpec())
.method(communicationConfig.getMethod())
.responseMappings(communicationConfig.getAllMappings())
.requestSize(request.size())
.build();
sendRequest(sessionContext, request, requestContext);
}
private void sendRequest(DeviceSessionContext sessionContext, PDU request, RequestInfo requestInfo) {
if (request.size() > 0) {
log.trace("Executing SNMP request for device {}. Variables bindings: {}", sessionContext.getDeviceId(), request.getVariableBindings());
private void sendRequest(DeviceSessionContext sessionContext, List<PDU> request, RequestContext requestContext) {
for (PDU pdu : request) {
log.debug("Executing SNMP request for device {} with {} variable bindings", sessionContext.getDeviceId(), pdu.size());
try {
snmp.send(request, sessionContext.getTarget(), requestInfo, sessionContext);
snmp.send(pdu, sessionContext.getTarget(), requestContext, sessionContext);
} catch (IOException e) {
log.error("Failed to send SNMP request to device {}: {}", sessionContext.getDeviceId(), e.toString());
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), e);
}
}
}
@ -215,51 +236,135 @@ public class SnmpTransportService implements TbTransportService {
DataType dataType = snmpMapping.getDataType();
PDU request = pduService.createSingleVariablePdu(sessionContext, snmpMethod, oid, value, dataType);
RequestInfo requestInfo = new RequestInfo(toDeviceRpcRequestMsg.getRequestId(), communicationConfig.getSpec(), communicationConfig.getAllMappings());
sendRequest(sessionContext, request, requestInfo);
RequestContext requestContext = RequestContext.builder()
.requestId(toDeviceRpcRequestMsg.getRequestId())
.communicationSpec(communicationConfig.getSpec())
.method(snmpMethod)
.responseMappings(communicationConfig.getAllMappings())
.requestSize(1)
.build();
sendRequest(sessionContext, List.of(request), requestContext);
}
public void processResponseEvent(DeviceSessionContext sessionContext, ResponseEvent event) {
((Snmp) event.getSource()).cancel(event.getRequest(), sessionContext);
RequestContext requestContext = (RequestContext) event.getUserObject();
if (event.getError() != null) {
log.warn("SNMP response error: {}", event.getError().toString());
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException(event.getError()));
return;
}
PDU response = event.getResponse();
if (response == null) {
log.debug("No response from SNMP device {}, requestId: {}", sessionContext.getDeviceId(), event.getRequest().getRequestID());
PDU responsePdu = event.getResponse();
if (log.isTraceEnabled()) {
log.trace("Received PDU for device {}: {}", sessionContext.getDeviceId(), responsePdu);
}
List<PDU> response;
if (requestContext.getRequestSize() == 1) {
if (responsePdu == null) {
log.debug("No response from SNMP device {}, requestId: {}", sessionContext.getDeviceId(), event.getRequest().getRequestID());
if (requestContext.getMethod() == SnmpMethod.GET) {
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException("No response from device"));
}
return;
}
response = List.of(responsePdu);
} else {
List<PDU> responseParts = requestContext.getResponseParts();
responseParts.add(responsePdu);
if (responseParts.size() == requestContext.getRequestSize()) {
response = new ArrayList<>();
for (PDU responsePart : responseParts) {
if (responsePart != null) {
response.add(responsePart);
}
}
log.debug("All response parts are collected for request to device {}", sessionContext.getDeviceId());
} else {
log.trace("Awaiting other response parts for request to device {}", sessionContext.getDeviceId());
return;
}
}
responseProcessingExecutor.execute(() -> {
try {
processResponse(sessionContext, response, requestContext);
} catch (Exception e) {
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), e);
}
});
}
/*
* SNMP notifications handler
*
* TODO: add check for host uniqueness when saving device (for backward compatibility - only for the ones using from-device RPC requests)
*
* NOTE: SNMP TRAPs support won't work properly when there is more than one SNMP transport,
* due to load-balancing of requests from devices: session might not be on this instance
* */
@Override
public void processPdu(CommandResponderEvent event) {
Address sourceAddress = event.getPeerAddress();
DeviceSessionContext sessionContext = transportContext.getSessions().stream()
.filter(session -> session.getTarget().getAddress().equals(sourceAddress))
.findFirst().orElse(null);
if (sessionContext == null) {
log.warn("SNMP TRAP processing failed: couldn't find device session for address {}", sourceAddress);
return;
}
RequestInfo requestInfo = (RequestInfo) event.getUserObject();
try {
processIncomingTrap(sessionContext, event);
} catch (Throwable e) {
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(),
SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST.getLabel(), e);
}
}
private void processIncomingTrap(DeviceSessionContext sessionContext, CommandResponderEvent event) {
PDU pdu = event.getPDU();
if (pdu == null) {
log.warn("Got empty trap from device {}", sessionContext.getDeviceId());
throw new IllegalArgumentException("Received TRAP with no data");
}
log.debug("Processing SNMP trap from device {} (PDU: {}}", sessionContext.getDeviceId(), pdu);
SnmpCommunicationConfig communicationConfig = sessionContext.getProfileTransportConfiguration().getCommunicationConfigs().stream()
.filter(config -> config.getSpec() == SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST).findFirst()
.orElseThrow(() -> new IllegalArgumentException("No config found for to-server RPC requests"));
RequestContext requestContext = RequestContext.builder()
.communicationSpec(communicationConfig.getSpec())
.responseMappings(communicationConfig.getAllMappings())
.method(SnmpMethod.TRAP)
.build();
responseProcessingExecutor.execute(() -> {
processResponse(sessionContext, response, requestInfo);
processResponse(sessionContext, List.of(pdu), requestContext);
});
}
private void processResponse(DeviceSessionContext sessionContext, PDU response, RequestInfo requestInfo) {
ResponseProcessor responseProcessor = responseProcessors.get(requestInfo.getCommunicationSpec());
private void processResponse(DeviceSessionContext sessionContext, List<PDU> response, RequestContext requestContext) {
ResponseProcessor responseProcessor = responseProcessors.get(requestContext.getCommunicationSpec());
if (responseProcessor == null) return;
JsonObject responseData = responseDataMappers.get(requestInfo.getCommunicationSpec()).map(response, requestInfo);
if (responseData.entrySet().isEmpty()) {
log.debug("No values is the SNMP response for device {}. Request id: {}", sessionContext.getDeviceId(), response.getRequestID());
return;
JsonObject responseData = responseDataMappers.get(requestContext.getCommunicationSpec()).map(response, requestContext);
if (responseData.size() == 0) {
log.warn("No values in the SNMP response for device {}", sessionContext.getDeviceId());
throw new IllegalArgumentException("No values in the response");
}
responseProcessor.process(responseData, requestInfo, sessionContext);
responseProcessor.process(responseData, requestContext, sessionContext);
reportActivity(sessionContext.getSessionInfo());
}
private void configureResponseDataMappers() {
responseDataMappers.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (pdu, requestInfo) -> {
responseDataMappers.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (pdus, requestContext) -> {
JsonObject responseData = new JsonObject();
pduService.processPdu(pdu).forEach((oid, value) -> {
requestInfo.getResponseMappings().stream()
pduService.processPdus(pdus).forEach((oid, value) -> {
requestContext.getResponseMappings().stream()
.filter(snmpMapping -> snmpMapping.getOid().equals(oid.toDottedString()))
.findFirst()
.ifPresent(snmpMapping -> {
@ -269,8 +374,8 @@ public class SnmpTransportService implements TbTransportService {
return responseData;
});
ResponseDataMapper defaultResponseDataMapper = (pdu, requestInfo) -> {
return pduService.processPdu(pdu, requestInfo.getResponseMappings());
ResponseDataMapper defaultResponseDataMapper = (pdus, requestContext) -> {
return pduService.processPdus(pdus, requestContext.getResponseMappings());
};
Arrays.stream(SnmpCommunicationSpec.values())
.forEach(communicationSpec -> {
@ -279,34 +384,39 @@ public class SnmpTransportService implements TbTransportService {
}
private void configureResponseProcessors() {
responseProcessors.put(SnmpCommunicationSpec.TELEMETRY_QUERYING, (responseData, requestInfo, sessionContext) -> {
responseProcessors.put(SnmpCommunicationSpec.TELEMETRY_QUERYING, (responseData, requestContext, sessionContext) -> {
TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(responseData);
transportService.process(sessionContext.getSessionInfo(), postTelemetryMsg, null);
log.debug("Posted telemetry for SNMP device {}: {}", sessionContext.getDeviceId(), responseData);
});
responseProcessors.put(SnmpCommunicationSpec.CLIENT_ATTRIBUTES_QUERYING, (responseData, requestInfo, sessionContext) -> {
responseProcessors.put(SnmpCommunicationSpec.CLIENT_ATTRIBUTES_QUERYING, (responseData, requestContext, sessionContext) -> {
TransportProtos.PostAttributeMsg postAttributesMsg = JsonConverter.convertToAttributesProto(responseData);
transportService.process(sessionContext.getSessionInfo(), postAttributesMsg, null);
log.debug("Posted attributes for SNMP device {}: {}", sessionContext.getDeviceId(), responseData);
});
responseProcessors.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (responseData, requestInfo, sessionContext) -> {
responseProcessors.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (responseData, requestContext, sessionContext) -> {
TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
.setRequestId(requestInfo.getRequestId())
.setRequestId(requestContext.getRequestId())
.setPayload(JsonConverter.toJson(responseData))
.build();
transportService.process(sessionContext.getSessionInfo(), rpcResponseMsg, null);
log.debug("Posted RPC response {} for device {}", responseData, sessionContext.getDeviceId());
});
responseProcessors.put(SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST, (responseData, requestContext, sessionContext) -> {
TransportProtos.ToServerRpcRequestMsg toServerRpcRequestMsg = TransportProtos.ToServerRpcRequestMsg.newBuilder()
.setRequestId(0)
.setMethodName(requestContext.getMethod().name())
.setParams(JsonConverter.toJson(responseData))
.build();
transportService.process(sessionContext.getSessionInfo(), toServerRpcRequestMsg, null);
});
}
private void reportActivity(TransportProtos.SessionInfoProto sessionInfo) {
transportService.process(sessionInfo, TransportProtos.SubscriptionInfoProto.newBuilder()
.setAttributeSubscription(true)
.setRpcSubscription(true)
.setLastActivityTime(System.currentTimeMillis())
.build(), TransportServiceCallback.EMPTY);
transportService.reportActivity(sessionInfo);
}
@ -335,29 +445,34 @@ public class SnmpTransportService implements TbTransportService {
}
@Data
private static class RequestInfo {
private Integer requestId;
private SnmpCommunicationSpec communicationSpec;
private List<SnmpMapping> responseMappings;
private static class RequestContext {
private final Integer requestId;
private final SnmpCommunicationSpec communicationSpec;
private final SnmpMethod method;
private final List<SnmpMapping> responseMappings;
public RequestInfo(Integer requestId, SnmpCommunicationSpec communicationSpec, List<SnmpMapping> responseMappings) {
this.requestId = requestId;
this.communicationSpec = communicationSpec;
this.responseMappings = responseMappings;
}
private final int requestSize;
private List<PDU> responseParts;
public RequestInfo(SnmpCommunicationSpec communicationSpec, List<SnmpMapping> responseMappings) {
@Builder
public RequestContext(Integer requestId, SnmpCommunicationSpec communicationSpec, SnmpMethod method, List<SnmpMapping> responseMappings, int requestSize) {
this.requestId = requestId;
this.communicationSpec = communicationSpec;
this.method = method;
this.responseMappings = responseMappings;
this.requestSize = requestSize;
if (requestSize > 1) {
this.responseParts = Collections.synchronizedList(new ArrayList<>());
}
}
}
private interface ResponseDataMapper {
JsonObject map(PDU pdu, RequestInfo requestInfo);
JsonObject map(List<PDU> pdus, RequestContext requestContext);
}
private interface ResponseProcessor {
void process(JsonObject responseData, RequestInfo requestInfo, DeviceSessionContext sessionContext);
void process(JsonObject responseData, RequestContext requestContext, DeviceSessionContext sessionContext);
}
}

31
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.transport.snmp.session;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -26,9 +27,12 @@ import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.data.transport.snmp.SnmpCommunicationSpec;
import org.thingsboard.server.common.transport.SessionMsgListener;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.service.DefaultTransportService;
import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg;
@ -57,17 +61,22 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
private SnmpDeviceTransportConfiguration deviceTransportConfiguration;
@Getter
private final Device device;
@Getter
private final TenantId tenantId;
private final SnmpTransportContext snmpTransportContext;
private final AtomicInteger msgIdSeq = new AtomicInteger(0);
@Getter
private boolean isActive = true;
@Setter
private Runnable sessionTimeoutHandler;
@Getter
private final List<ScheduledFuture<?>> queryingTasks = new LinkedList<>();
public DeviceSessionContext(Device device, DeviceProfile deviceProfile, String token,
@Builder
public DeviceSessionContext(TenantId tenantId, Device device, DeviceProfile deviceProfile, String token,
SnmpDeviceProfileTransportConfiguration profileTransportConfiguration,
SnmpDeviceTransportConfiguration deviceTransportConfiguration,
SnmpTransportContext snmpTransportContext) throws Exception {
@ -75,6 +84,7 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
super.setDeviceId(device.getId());
super.setDeviceProfile(deviceProfile);
this.device = device;
this.tenantId = tenantId;
this.token = token;
this.snmpTransportContext = snmpTransportContext;
@ -131,19 +141,32 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
@Override
public void onAttributeUpdate(UUID sessionId, AttributeUpdateNotificationMsg attributeUpdateNotification) {
log.trace("[{}] Received attributes update notification to device", sessionId);
snmpTransportContext.getSnmpTransportService().onAttributeUpdate(this, attributeUpdateNotification);
try {
snmpTransportContext.getSnmpTransportService().onAttributeUpdate(this, attributeUpdateNotification);
} catch (Exception e) {
snmpTransportContext.getTransportService().errorEvent(getTenantId(), getDeviceId(), SnmpCommunicationSpec.SHARED_ATTRIBUTES_SETTING.getLabel(), e);
}
}
@Override
public void onRemoteSessionCloseCommand(UUID sessionId, SessionCloseNotificationProto sessionCloseNotification) {
log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage());
if (sessionCloseNotification.getMessage().equals(DefaultTransportService.SESSION_EXPIRED_MESSAGE)) {
if (sessionTimeoutHandler != null) {
sessionTimeoutHandler.run();
}
}
}
@Override
public void onToDeviceRpcRequest(UUID sessionId, ToDeviceRpcRequestMsg toDeviceRequest) {
log.trace("[{}] Received RPC command to device", sessionId);
snmpTransportContext.getSnmpTransportService().onToDeviceRpcRequest(this, toDeviceRequest);
snmpTransportContext.getTransportService().process(getSessionInfo(), toDeviceRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY);
try {
snmpTransportContext.getSnmpTransportService().onToDeviceRpcRequest(this, toDeviceRequest);
snmpTransportContext.getTransportService().process(getSessionInfo(), toDeviceRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY);
} catch (Exception e) {
snmpTransportContext.getTransportService().errorEvent(getTenantId(), getDeviceId(), SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST.getLabel(), e);
}
}
@Override

23
common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.transport.snmp;
import org.snmp4j.CommandResponderEvent;
import org.snmp4j.CommunityTarget;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
@ -35,7 +34,6 @@ import org.snmp4j.agent.mo.snmp.SnmpTargetMIB;
import org.snmp4j.agent.mo.snmp.StorageType;
import org.snmp4j.agent.mo.snmp.VacmMIB;
import org.snmp4j.agent.security.MutableVACM;
import org.snmp4j.mp.MPv3;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.security.SecurityLevel;
import org.snmp4j.security.SecurityModel;
@ -53,27 +51,11 @@ import org.snmp4j.transport.TransportMappings;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@SuppressWarnings("deprecation")
public class SnmpDeviceSimulatorV2 extends BaseAgent {
public static class RequestProcessor extends CommandProcessor {
private final Consumer<CommandResponderEvent> processor;
public RequestProcessor(Consumer<CommandResponderEvent> processor) {
super(new OctetString(MPv3.createLocalEngineID()));
this.processor = processor;
}
@Override
public void processPdu(CommandResponderEvent event) {
processor.accept(event);
}
}
private final Target target;
private final Address address;
private Snmp snmp;
@ -81,10 +63,7 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent {
private final String password;
public SnmpDeviceSimulatorV2(int port, String password) throws IOException {
super(new File("conf.agent"), new File("bootCounter.agent"), new RequestProcessor(event -> {
System.out.println("aboba");
((Snmp) event.getSource()).cancel(event.getPDU(), event1 -> System.out.println("canceled"));
}));
super(new File("conf.agent"), new File("bootCounter.agent"), new CommandProcessor(new OctetString("12312")));
CommunityTarget target = new CommunityTarget();
target.setCommunity(new OctetString(password));
this.address = GenericAddress.parse("udp:0.0.0.0/" + port);

43
common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java

@ -16,34 +16,41 @@
package org.thingsboard.server.transport.snmp;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class SnmpTestV2 {
public static void main(String[] args) throws IOException {
SnmpDeviceSimulatorV2 device = new SnmpDeviceSimulatorV2(1610, "public");
device.start();
device.setUpMappings(Map.of(
".1.3.6.1.2.1.1.1.50", "12",
".1.3.6.1.2.1.2.1.52", "56",
".1.3.6.1.2.1.3.1.54", "yes",
".1.3.6.1.2.1.7.1.58", ""
));
private static final Scanner scanner = new Scanner(System.in);
public static void main(String[] args) throws IOException {
SnmpDeviceSimulatorV2 client = new SnmpDeviceSimulatorV2(1610, "public");
// while (true) {
// new Scanner(System.in).nextLine();
// device.sendTrap("127.0.0.1", 1062, Map.of(".1.3.6.1.2.87.1.56", "12"));
// System.out.println("sent");
client.start();
Map<String, String> mappings = new HashMap<>();
// for (int i = 1; i <= 500; i++) {
// String oid = String.format(".1.3.6.1.2.1.%s.1.52", i);
// mappings.put(oid, "value_" + i);
// }
mappings.put("1.3.6.1.2.1.266.1.52", "****");
// Snmp snmp = new Snmp(device.transportMappings[0]);
// device.snmp.addCommandResponder(event -> {
// System.out.println(event);
// });
client.setUpMappings(mappings);
inputTraps(client);
scanner.nextLine();
}
new Scanner(System.in).nextLine();
private static void inputTraps(SnmpDeviceSimulatorV2 client) throws IOException {
while (true) {
String data = scanner.nextLine();
if (!data.isEmpty()) {
client.sendTrap("127.0.0.1", 1620, Map.of(
"1.3.6.1.2.1.266.1.52", data + " (266)",
"1.3.6.1.2.1.267.1.52", data + " (267)"
));
}
}
}
}

7
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java

@ -17,6 +17,9 @@ package org.thingsboard.server.common.transport;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse;
@ -144,6 +147,10 @@ public interface TransportService {
void reportActivity(SessionInfoProto sessionInfo);
void lifecycleEvent(TenantId tenantId, DeviceId deviceId, ComponentLifecycleEvent eventType, boolean success, Throwable error);
void errorEvent(TenantId tenantId, DeviceId deviceId, String method, Throwable error);
void deregisterSession(SessionInfoProto sessionInfo);
void log(SessionInfoProto sessionInfo, String msg);

49
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -22,6 +22,7 @@ import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
@ -49,6 +50,8 @@ import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.data.rpc.RpcStatus;
@ -827,6 +830,37 @@ public class DefaultTransportService implements TransportService {
sessionsToRemove.forEach(sessionsActivity::remove);
}
@Override
public void lifecycleEvent(TenantId tenantId, DeviceId deviceId, ComponentLifecycleEvent eventType, boolean success, Throwable error) {
ToCoreMsg msg = ToCoreMsg.newBuilder()
.setLifecycleEventMsg(TransportProtos.LifecycleEventProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setEntityIdMSB(deviceId.getId().getMostSignificantBits())
.setEntityIdLSB(deviceId.getId().getLeastSignificantBits())
.setServiceId(serviceInfoProvider.getServiceId())
.setLcEventType(eventType.name())
.setSuccess(success)
.setError(error != null ? ExceptionUtils.getStackTrace(error) : ""))
.build();
sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY);
}
@Override
public void errorEvent(TenantId tenantId, DeviceId deviceId, String method, Throwable error) {
ToCoreMsg msg = ToCoreMsg.newBuilder()
.setErrorEventMsg(TransportProtos.ErrorEventProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setEntityIdMSB(deviceId.getId().getMostSignificantBits())
.setEntityIdLSB(deviceId.getId().getLeastSignificantBits())
.setServiceId(serviceInfoProvider.getServiceId())
.setMethod(method)
.setError(ExceptionUtils.getStackTrace(error)))
.build();
sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY);
}
@Override
public SessionMetaData registerSyncSession(TransportProtos.SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout) {
SessionMetaData currentSession = new SessionMetaData(sessionInfo, TransportProtos.SessionType.SYNC, listener);
@ -1118,18 +1152,21 @@ public class DefaultTransportService implements TransportService {
}
protected void sendToDeviceActor(TransportProtos.SessionInfoProto sessionInfo, TransportToDeviceActorMsg toDeviceActorMsg, TransportServiceCallback<Void> callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, getTenantId(sessionInfo), getDeviceId(sessionInfo));
ToCoreMsg toCoreMsg = ToCoreMsg.newBuilder().setToDeviceActorMsg(toDeviceActorMsg).build();
sendToCore(getTenantId(sessionInfo), getDeviceId(sessionInfo), toCoreMsg, getRoutingKey(sessionInfo), callback);
}
private void sendToCore(TenantId tenantId, EntityId entityId, ToCoreMsg msg, UUID routingKey, TransportServiceCallback<Void> callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Pushing to topic {} message {}", getTenantId(sessionInfo), getDeviceId(sessionInfo), tpi.getFullTopicName(), toDeviceActorMsg);
log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, entityId, tpi.getFullTopicName(), msg);
}
TransportTbQueueCallback transportTbQueueCallback = callback != null ?
new TransportTbQueueCallback(callback) : null;
tbCoreProducerStats.incrementTotal();
StatsCallback wrappedCallback = new StatsCallback(transportTbQueueCallback, tbCoreProducerStats);
tbCoreMsgProducer.send(tpi,
new TbProtoQueueMsg<>(getRoutingKey(sessionInfo),
ToCoreMsg.newBuilder().setToDeviceActorMsg(toDeviceActorMsg).build()),
wrappedCallback);
tbCoreMsgProducer.send(tpi, new TbProtoQueueMsg<>(routingKey, msg), wrappedCallback);
}
private void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) {

2
docker/docker-compose.yml

@ -235,6 +235,8 @@ services:
tb-snmp-transport:
restart: always
image: "${DOCKER_REPO}/${SNMP_TRANSPORT_DOCKER_NAME}:${TB_VERSION}"
ports:
- "1620:1620/udp"
environment:
TB_SERVICE_ID: tb-snmp-transport
JAVA_OPTS: "${JAVA_OPTS}"

1
docker/tb-snmp-transport.env

@ -1,6 +1,7 @@
ZOOKEEPER_ENABLED=true
ZOOKEEPER_URL=zookeeper:2181
SNMP_BIND_PORT=1620
METRICS_ENABLED=true
METRICS_ENDPOINTS_EXPOSE=prometheus
WEB_APPLICATION_ENABLE=true

4
transport/snmp/src/main/resources/tb-snmp-transport.yml

@ -99,11 +99,15 @@ redis:
transport:
snmp:
enabled: "${SNMP_ENABLED:true}"
bind_port: "${SNMP_BIND_PORT:1620}"
response_processing:
# parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices
parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}"
# to configure SNMP to work over UDP or TCP
underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}"
max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}"
response:
ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}"
sessions:
inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:3000}"

8
ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts

@ -65,6 +65,10 @@ export enum ImportEntityColumnType {
lwm2mServerSecurityMode = 'LWM2M_SERVER_SECURITY_MODE',
lwm2mServerClientPublicKeyOrId = 'LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID',
lwm2mServerClientSecretKey = 'LWM2M_SERVER_CLIENT_SECRET_KEY',
snmpHost = 'SNMP_HOST',
snmpPort = 'SNMP_PORT',
snmpVersion = 'SNMP_VERSION',
snmpCommunityString = 'SNMP_COMMUNITY_STRING',
isGateway = 'IS_GATEWAY',
description = 'DESCRIPTION',
routingKey = 'ROUTING_KEY',
@ -96,6 +100,10 @@ export const importEntityColumnTypeTranslations = new Map<ImportEntityColumnType
[ImportEntityColumnType.lwm2mServerSecurityMode, 'import.column-type.lwm2m.lwm2m-server-security-mode'],
[ImportEntityColumnType.lwm2mServerClientPublicKeyOrId, 'import.column-type.lwm2m.lwm2m-server-public-key-id'],
[ImportEntityColumnType.lwm2mServerClientSecretKey, 'import.column-type.lwm2m.lwm2m-server-secret-key'],
[ImportEntityColumnType.snmpHost, 'import.column-type.snmp.host'],
[ImportEntityColumnType.snmpPort, 'import.column-type.snmp.port'],
[ImportEntityColumnType.snmpVersion, 'import.column-type.snmp.version'],
[ImportEntityColumnType.snmpCommunityString, 'import.column-type.snmp.community-string'],
[ImportEntityColumnType.isGateway, 'import.column-type.isgateway'],
[ImportEntityColumnType.description, 'import.column-type.description'],
[ImportEntityColumnType.routingKey, 'import.column-type.routing-key'],

4
ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts

@ -105,6 +105,10 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
{ value: ImportEntityColumnType.lwm2mServerSecurityMode },
{ value: ImportEntityColumnType.lwm2mServerClientPublicKeyOrId },
{ value: ImportEntityColumnType.lwm2mServerClientSecretKey },
{ value: ImportEntityColumnType.snmpHost },
{ value: ImportEntityColumnType.snmpPort },
{ value: ImportEntityColumnType.snmpVersion },
{ value: ImportEntityColumnType.snmpCommunityString },
);
break;
case EntityType.ASSET:

12
ui-ngx/src/app/shared/models/device.models.ts

@ -298,14 +298,16 @@ export enum SnmpSpecType {
TELEMETRY_QUERYING = 'TELEMETRY_QUERYING',
CLIENT_ATTRIBUTES_QUERYING = 'CLIENT_ATTRIBUTES_QUERYING',
SHARED_ATTRIBUTES_SETTING = 'SHARED_ATTRIBUTES_SETTING',
TO_DEVICE_RPC_REQUEST = 'TO_DEVICE_RPC_REQUEST'
TO_DEVICE_RPC_REQUEST = 'TO_DEVICE_RPC_REQUEST',
TO_SERVER_RPC_REQUEST = 'TO_SERVER_RPC_REQUEST'
}
export const SnmpSpecTypeTranslationMap = new Map<SnmpSpecType, string>([
[SnmpSpecType.TELEMETRY_QUERYING, ' Telemetry'],
[SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING, 'Client attributes'],
[SnmpSpecType.SHARED_ATTRIBUTES_SETTING, 'Shared attributes'],
[SnmpSpecType.TO_DEVICE_RPC_REQUEST, 'RPC request']
[SnmpSpecType.TELEMETRY_QUERYING, ' Telemetry (SNMP GET)'],
[SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING, 'Client attributes (SNMP GET)'],
[SnmpSpecType.SHARED_ATTRIBUTES_SETTING, 'Shared attributes (SNMP SET)'],
[SnmpSpecType.TO_DEVICE_RPC_REQUEST, 'To-device RPC request (SNMP GET/SET)'],
[SnmpSpecType.TO_SERVER_RPC_REQUEST, 'From-device RPC request (SNMP TRAP)']
]);
export interface SnmpCommunicationConfig {

6
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2784,6 +2784,12 @@
"lwm2m-server-secret-key": "LwM2M server secret key",
"lwm2m-server-public-key-id": "LwM2M server public key or id"
},
"snmp": {
"host": "SNMP host",
"port": "SNMP port",
"version": "SNMP version (v1, v2c or v3)",
"community-string": "SNMP community string"
},
"isgateway": "Is Gateway",
"activity-time-from-gateway-device": "Activity time from gateway device",
"description": "Description",

Loading…
Cancel
Save