Provide SNMP transport configuration validation; refactor

This commit is contained in:
Viacheslav Klimov 2021-03-24 16:43:28 +02:00
parent 0532d22d9d
commit afab5150b8
12 changed files with 173 additions and 86 deletions

View File

@ -38,4 +38,7 @@ public interface DeviceTransportConfiguration extends Serializable {
@JsonIgnore
DeviceTransportType getType();
default void validate() {
}
}

View File

@ -19,25 +19,30 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion;
@Data
public class SnmpDeviceTransportConfiguration implements DeviceTransportConfiguration {
private String address;
private int port;
private String community;
private String protocolVersion;
private SnmpProtocolVersion protocolVersion;
@Override
public DeviceTransportType getType() {
return DeviceTransportType.SNMP;
}
@Override
public void validate() {
if (!isValid()) {
throw new IllegalArgumentException("Transport configuration is not valid");
}
}
@JsonIgnore
public boolean isValid() {
return StringUtils.isNotEmpty(this.address)
&& this.port > 0
&& StringUtils.isNotEmpty(this.community)
&& StringUtils.isNotEmpty(this.protocolVersion);
private boolean isValid() {
return StringUtils.isNotBlank(address) && port > 0 &&
StringUtils.isNotBlank(community) && protocolVersion != null;
}
}

View File

@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
@ -30,10 +31,13 @@ import org.thingsboard.server.common.data.DeviceTransportType;
@JsonSubTypes.Type(value = DefaultDeviceProfileTransportConfiguration.class, name = "DEFAULT"),
@JsonSubTypes.Type(value = MqttDeviceProfileTransportConfiguration.class, name = "MQTT"),
@JsonSubTypes.Type(value = Lwm2mDeviceProfileTransportConfiguration.class, name = "LWM2M"),
@JsonSubTypes.Type(value = SnmpProfileTransportConfiguration.class, name = "SNMP")})
@JsonSubTypes.Type(value = SnmpDeviceProfileTransportConfiguration.class, name = "SNMP")})
public interface DeviceProfileTransportConfiguration {
@JsonIgnore
DeviceTransportType getType();
default void validate() {
}
}

View File

@ -18,6 +18,7 @@ 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 java.util.Collections;
import java.util.List;
@ -25,12 +26,12 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
@Data
public class SnmpProfileTransportConfiguration implements DeviceProfileTransportConfiguration {
public class SnmpDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration {
private int pollPeriodMs;
private int timeoutMs;
private int retries;
private List<SnmpMapping> attributesMappings;
private List<SnmpMapping> telemetryMappings;
private List<SnmpMapping> attributesMappings;
@Override
public DeviceTransportType getType() {
@ -39,10 +40,25 @@ public class SnmpProfileTransportConfiguration implements DeviceProfileTransport
@JsonIgnore
public List<SnmpMapping> getAllMappings() {
if (attributesMappings != null && telemetryMappings != null) {
return Stream.concat(attributesMappings.stream(), telemetryMappings.stream()).collect(Collectors.toList());
if (telemetryMappings != null && attributesMappings != null) {
return Stream.concat(telemetryMappings.stream(), attributesMappings.stream()).collect(Collectors.toList());
} else {
return Collections.emptyList();
}
}
@Override
public void validate() {
if (!isValid()) {
throw new IllegalArgumentException("Transport configuration is not valid");
}
}
@JsonIgnore
private boolean isValid() {
List<SnmpMapping> mappings = getAllMappings();
return pollPeriodMs > 0 && timeoutMs > 0 && retries >= 0 &&
!mappings.isEmpty() && mappings.stream().allMatch(SnmpMapping::isValid) &&
mappings.stream().map(SnmpMapping::getOid).distinct().count() == mappings.size();
}
}

View File

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2021 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;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.kv.DataType;
import java.util.regex.Pattern;
@Data
public class SnmpMapping {
private String oid;
private SnmpMethod method;
private String key;
private DataType dataType;
private static final Pattern OID_PATTERN = Pattern.compile("^\\.?([0-2])((\\.0)|(\\.[1-9][0-9]*))*$");
@JsonIgnore
public boolean isValid() {
return StringUtils.isNotEmpty(oid) && OID_PATTERN.matcher(oid).matches() && method != null &&
StringUtils.isNotBlank(key) && dataType != null;
}
}

View File

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2021 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;
public enum SnmpMethod {
GET(-96),
SET(-93);
// codes taken from org.snmp4j.PDU class
private final int code;
SnmpMethod(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}

View File

@ -13,15 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.device.profile;
package org.thingsboard.server.common.data.transport.snmp;
import lombok.Data;
import org.thingsboard.server.common.data.kv.DataType;
public enum SnmpProtocolVersion {
V1(0),
V2C(1),
V3(3);
@Data
public class SnmpMapping {
private String oid;
private String method;
private String key;
private DataType dataType;
private final int code;
SnmpProtocolVersion(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}

View File

@ -27,12 +27,13 @@ import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.device.data.DeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.SnmpMapping;
import org.thingsboard.server.common.data.device.profile.SnmpProfileTransportConfiguration;
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.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.transport.snmp.SnmpMapping;
import org.thingsboard.server.common.data.transport.snmp.SnmpMethod;
import org.thingsboard.server.common.transport.DeviceUpdatedEvent;
import org.thingsboard.server.common.transport.TransportContext;
import org.thingsboard.server.common.transport.TransportDeviceProfileCache;
@ -74,7 +75,7 @@ public class SnmpTransportContext extends TransportContext {
private final SnmpTransportBalancingService balancingService;
private final Map<DeviceId, DeviceSessionContext> sessions = new ConcurrentHashMap<>();
private final Map<DeviceProfileId, SnmpProfileTransportConfiguration> profilesTransportConfigs = new ConcurrentHashMap<>();
private final Map<DeviceProfileId, SnmpDeviceProfileTransportConfiguration> profilesTransportConfigs = new ConcurrentHashMap<>();
private final Map<DeviceProfileId, List<PDU>> profilesPdus = new ConcurrentHashMap<>();
private Collection<DeviceId> allSnmpDevicesIds = new ConcurrentLinkedDeque<>();
@ -94,7 +95,13 @@ public class SnmpTransportContext extends TransportContext {
managedDevicesIds.stream()
.map(protoEntityService::getDeviceById)
.collect(Collectors.toList())
.forEach(this::establishDeviceSession);
.forEach(device -> {
try {
establishDeviceSession(device);
} catch (Exception e) {
log.error("Failed to establish session for SNMP device {}: {}", device.getId(), e.getMessage());
}
});
}
private void establishDeviceSession(Device device) {
@ -110,13 +117,10 @@ public class SnmpTransportContext extends TransportContext {
return;
}
SnmpDeviceProfileTransportConfiguration profileTransportConfiguration = (SnmpDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
SnmpDeviceTransportConfiguration deviceTransportConfiguration = (SnmpDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration();
if (!deviceTransportConfiguration.isValid()) {
log.warn("SNMP device transport configuration is not valid");
return;
}
SnmpProfileTransportConfiguration profileTransportConfiguration = (SnmpProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
profilesTransportConfigs.put(deviceProfileId, profileTransportConfiguration);
profilesPdus.computeIfAbsent(deviceProfileId, id -> createPdus(profileTransportConfiguration));
DeviceSessionContext deviceSessionContext = new DeviceSessionContext(
@ -140,22 +144,18 @@ public class SnmpTransportContext extends TransportContext {
return;
}
SnmpProfileTransportConfiguration profileTransportConfiguration = (SnmpProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
SnmpDeviceTransportConfiguration deviceTransportConfiguration = (SnmpDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration();
sessionContext.setProfileTransportConfiguration(profileTransportConfiguration);
sessionContext.setDeviceTransportConfiguration(deviceTransportConfiguration);
if (!deviceTransportConfiguration.isValid()) {
log.warn("SNMP device transport configuration is not valid");
destroyDeviceSession(sessionContext);
return;
}
SnmpDeviceProfileTransportConfiguration newProfileTransportConfiguration = (SnmpDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
SnmpDeviceTransportConfiguration newDeviceTransportConfiguration = (SnmpDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration();
if (!profileTransportConfiguration.equals(profilesTransportConfigs.get(deviceProfileId))) {
profilesPdus.put(deviceProfileId, createPdus(profileTransportConfiguration));
profilesTransportConfigs.put(deviceProfileId, profileTransportConfiguration);
sessionContext.initTarget(profileTransportConfiguration, deviceTransportConfiguration);
} else if (!deviceTransportConfiguration.equals(sessionContext.getDeviceTransportConfiguration())) {
sessionContext.initTarget(profileTransportConfiguration, deviceTransportConfiguration);
if (!newProfileTransportConfiguration.equals(sessionContext.getProfileTransportConfiguration())) {
profilesPdus.put(deviceProfileId, createPdus(newProfileTransportConfiguration));
profilesTransportConfigs.put(deviceProfileId, newProfileTransportConfiguration);
sessionContext.setProfileTransportConfiguration(newProfileTransportConfiguration);
sessionContext.initTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration);
} else if (!newDeviceTransportConfiguration.equals(sessionContext.getDeviceTransportConfiguration())) {
sessionContext.setDeviceTransportConfiguration(newDeviceTransportConfiguration);
sessionContext.initTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration);
} else {
log.trace("Configuration of the device {} was not updated", device);
}
@ -205,8 +205,8 @@ public class SnmpTransportContext extends TransportContext {
});
}
private List<PDU> createPdus(SnmpProfileTransportConfiguration deviceProfileConfig) {
Map<String, List<VariableBinding>> bindingsPerMethod = new HashMap<>();
private List<PDU> createPdus(SnmpDeviceProfileTransportConfiguration deviceProfileConfig) {
Map<SnmpMethod, List<VariableBinding>> bindingsPerMethod = new HashMap<>();
deviceProfileConfig.getAllMappings().forEach(mapping -> bindingsPerMethod
.computeIfAbsent(mapping.getMethod(), v -> new ArrayList<>())
@ -215,7 +215,7 @@ public class SnmpTransportContext extends TransportContext {
return bindingsPerMethod.keySet().stream()
.map(method -> {
PDU request = new PDU();
request.setType(getSnmpMethod(method));
request.setType(method.getCode());
request.addAll(bindingsPerMethod.get(method));
return request;
})
@ -311,22 +311,9 @@ public class SnmpTransportContext extends TransportContext {
return Optional.empty();
}
private Optional<SnmpMapping> getMapping(OID responseOid, List<SnmpMapping> mappings) {
private Optional<SnmpMapping> getMapping(OID oid, List<SnmpMapping> mappings) {
return mappings.stream()
.filter(kvMapping -> new OID(kvMapping.getOid()).equals(responseOid))
//TODO: OID shouldn't be duplicated in the config, add backend and UI verification
.filter(mapping -> new OID(mapping.getOid()).equals(oid))
.findFirst();
}
private int getSnmpMethod(String configMethod) {
switch (configMethod) {
case "get":
return PDU.GET;
case "getNext":
case "response":
case "set":
default:
return -1;
}
}
}

View File

@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
import org.snmp4j.event.ResponseEvent;
import org.snmp4j.smi.Null;
import org.snmp4j.smi.VariableBinding;
import org.snmp4j.transport.DefaultUdpTransportMapping;
import org.springframework.context.annotation.Lazy;
@ -103,8 +104,8 @@ public class SnmpTransportService implements TbTransportService {
try {
log.debug("[{}] Sending SNMP message for device {}", pdu.getRequestID(), sessionContext.getDeviceId());
snmp.send(pdu, sessionContext.getTarget(), deviceProfileId, sessionContext);
} catch (IOException e) {
log.error(e.getMessage(), e);
} catch (Exception e) {
log.error("Failed to send SNMP request: {}", e.getMessage());
}
});
}
@ -123,7 +124,7 @@ public class SnmpTransportService implements TbTransportService {
PDU response = event.getResponse();
if (response == null) {
log.warn("No SNMP response, requestId: {}", event.getRequest().getRequestID());
log.warn("No response from SNMP device {}, requestId: {}", sessionContext.getDeviceId(), event.getRequest().getRequestID());
return;
}
@ -138,6 +139,11 @@ public class SnmpTransportService implements TbTransportService {
VariableBinding variableBinding = response.get(i);
log.trace("Processing variable binding {}: {}", i, variableBinding);
if (variableBinding.getVariable() instanceof Null) {
log.debug("Response variable is empty");
continue;
}
snmpTransportContext.getTelemetryMapping(deviceProfileId, variableBinding.getOid()).ifPresent(mapping -> {
log.trace("Found telemetry mapping for oid {}: {}", variableBinding.getOid(), mapping);
processValue(mapping.getKey(), mapping.getDataType(), variableBinding.toValueString(), telemetry);

View File

@ -22,13 +22,12 @@ import org.snmp4j.CommunityTarget;
import org.snmp4j.Target;
import org.snmp4j.event.ResponseEvent;
import org.snmp4j.event.ResponseListener;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.smi.GenericAddress;
import org.snmp4j.smi.OctetString;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.SnmpProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.transport.SessionMsgListener;
import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
@ -51,7 +50,7 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
private final String token;
@Getter
@Setter
private SnmpProfileTransportConfiguration profileTransportConfiguration;
private SnmpDeviceProfileTransportConfiguration profileTransportConfiguration;
@Getter
@Setter
private SnmpDeviceTransportConfiguration deviceTransportConfiguration;
@ -79,7 +78,7 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
this.snmpTransportContext = snmpTransportContext;
this.snmpTransportService = snmpTransportService;
this.profileTransportConfiguration = (SnmpProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
this.profileTransportConfiguration = (SnmpDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
this.deviceTransportConfiguration = deviceTransportConfiguration;
initTarget(this.profileTransportConfiguration, this.deviceTransportConfiguration);
@ -105,11 +104,11 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
}
}
public void initTarget(SnmpProfileTransportConfiguration profileTransportConfig, SnmpDeviceTransportConfiguration deviceTransportConfig) {
public void initTarget(SnmpDeviceProfileTransportConfiguration profileTransportConfig, SnmpDeviceTransportConfiguration deviceTransportConfig) {
log.trace("Initializing target for SNMP session of device {}", device);
CommunityTarget communityTarget = new CommunityTarget();
communityTarget.setAddress(GenericAddress.parse(GenericAddress.TYPE_UDP + ":" + deviceTransportConfig.getAddress() + "/" + deviceTransportConfig.getPort()));
communityTarget.setVersion(getSnmpVersion(deviceTransportConfig.getProtocolVersion()));
communityTarget.setVersion(deviceTransportConfig.getProtocolVersion().getCode());
communityTarget.setCommunity(new OctetString(deviceTransportConfig.getCommunity()));
communityTarget.setTimeout(profileTransportConfig.getTimeoutMs());
communityTarget.setRetries(profileTransportConfig.getRetries());
@ -125,20 +124,6 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
return token;
}
//TODO: replace with enum, wtih preliminary discussion of type version in config (string or integer)
private int getSnmpVersion(String configSnmpVersion) {
switch (configSnmpVersion) {
case ("v1"):
return SnmpConstants.version1;
case ("v2c"):
return SnmpConstants.version2c;
case ("v3"):
return SnmpConstants.version3;
default:
return -1;
}
}
@Override
public int nextMsgId() {
return msgIdSeq.incrementAndGet();

View File

@ -350,6 +350,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
}
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
transportConfiguration.validate();
if (transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) {
MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration;
if (mqttTransportConfiguration.getTransportPayloadTypeConfiguration() instanceof ProtoTransportPayloadConfiguration) {

View File

@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.device.credentials.BasicMqttCredential
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.data.DeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.MqttDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
@ -604,6 +605,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
throw new DataValidationException("Can't assign device to customer from different tenant!");
}
}
Optional.ofNullable(device.getDeviceData())
.flatMap(deviceData -> Optional.ofNullable(deviceData.getTransportConfiguration()))
.ifPresent(DeviceTransportConfiguration::validate);
}
};