diff --git a/opennms-config/src/main/java/org/opennms/netmgt/config/SnmpConfigUtils.java b/opennms-config/src/main/java/org/opennms/netmgt/config/SnmpConfigUtils.java new file mode 100644 index 000000000000..1b691c6dcdc0 --- /dev/null +++ b/opennms-config/src/main/java/org/opennms/netmgt/config/SnmpConfigUtils.java @@ -0,0 +1,284 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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.opennms.netmgt.config; + +import com.google.common.base.Strings; +import org.opennms.core.utils.InetAddressUtils; +import org.opennms.netmgt.config.snmp.Definition; +import org.opennms.netmgt.config.snmp.Range; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class SnmpConfigUtils { + private static final String COMMA_DELIMITER = ","; + private static final String RANGE_DELIMITER = "-"; + private static final String IPLIKE_IPV4_VALIDATION_REGEX = "(([0-9]{1,3}(([,-])[0-9]{1,3})*|\\*)\\.){3}([0-9]{1,3}(([,-])[0-9]{1,3})*|\\*)"; + private static final String IPLIKE_IPV6_VALIDATION_REGEX = "(([0-9a-fA-F]{1,4}(([,-])[0-9a-fA-F]{1,4})*|\\*):){7}([0-9a-fA-F]{1,4}(([,-])[0-9a-fA-F]{1,4})*|\\*)"; + + public enum DefinitionStatsValidationStatus { + VALID, + MISSING_CONTENTS, + CANNOT_MIX_RANGE_AND_IPMATCH + } + + public enum DefinitionContentsValidationStatus { + VALID, + INVALID_SPECIFIC_ADDRESS, + INVALID_RANGE, + INVALID_RANGE_BEGIN, + INVALID_RANGE_END, + INVALID_EMPTY + } + + private record DefinitionStats(boolean hasRange, boolean hasSpecific, boolean hasIpMatch) {} + + public record ValidatedDefinitionContents( + DefinitionContentsValidationStatus status, + String invalidItem, + List definitionIpMatches, + List definitionSpecifics, + List definitionRanges + ) {} + + public static DefinitionStatsValidationStatus validateDefinitionContents(Definition definition) { + DefinitionStats stats = getDefinitionStats(definition); + + if (!stats.hasRange && !stats.hasSpecific && !stats.hasIpMatch) { + return DefinitionStatsValidationStatus.MISSING_CONTENTS; + } + + if (stats.hasIpMatch && (stats.hasRange || stats.hasSpecific)) { + return DefinitionStatsValidationStatus.CANNOT_MIX_RANGE_AND_IPMATCH; + } + + return DefinitionStatsValidationStatus.VALID; + } + + /** + * Validates IP addresses in the Definition's specifics and ranges. + * @return error message if validation fails, null if valid + */ + public static String validateDefinitionIpAddresses(final Definition definition) { + for (final String specific : definition.getSpecifics()) { + if (Strings.isNullOrEmpty(specific)) { + return "Invalid specific IP address: empty value."; + } + InetAddress addr = null; + + try { + addr = InetAddressUtils.addr(specific); + } catch (IllegalArgumentException ignored) { + } + + if (addr == null) { + return String.format("Invalid specific IP address: %s", specific); + } + } + + for (final Range range : definition.getRanges()) { + if (Strings.isNullOrEmpty(range.getBegin()) || Strings.isNullOrEmpty(range.getEnd())) { + return String.format("Invalid range: begin and end must be specified. begin=%s, end=%s", + range.getBegin(), range.getEnd()); + } + + InetAddress beginAddr = null; + InetAddress endAddr = null; + + try { + beginAddr = InetAddressUtils.addr(range.getBegin()); + endAddr = InetAddressUtils.addr(range.getEnd()); + } catch (IllegalArgumentException ignored) { + } + + if (beginAddr == null) { + return String.format("Invalid range begin IP address: %s", range.getBegin()); + } + if (endAddr == null) { + return String.format("Invalid range end IP address: %s", range.getEnd()); + } + + // Ensure both addresses are the same IP version + // They should be either both Inet4Address or both Inet6Address, but not one of each + boolean beginIsV4 = beginAddr instanceof java.net.Inet4Address; + boolean endIsV4 = endAddr instanceof java.net.Inet4Address; + + if (beginIsV4 != endIsV4) { + return String.format("Invalid range: begin and end must be same IP version. begin=%s, end=%s", + range.getBegin(), range.getEnd()); + } + } + + return null; + } + + /** + * Validates IP match expressions in the Definition's ipMatch list. + * @return error message if validation fails, null if valid + */ + public static String validateDefinitionIpMatches(final Definition definition) { + for (final String ipMatch : definition.getIpMatches()) { + if (Strings.isNullOrEmpty(ipMatch)) { + return "Invalid IP match expression: empty value."; + } + + final String normalizedIpMatch = expandIPv6Compressed(ipMatch); + if (normalizedIpMatch == null || (!normalizedIpMatch.matches(IPLIKE_IPV4_VALIDATION_REGEX) && !normalizedIpMatch.matches(IPLIKE_IPV6_VALIDATION_REGEX))) { + return String.format("Invalid IP match expression: '%s'.", ipMatch); + } + + // TODO: Consider more robust validation, for example checking octets are in the 0-255 range + } + + return null; + } + + /** + * Given comma-separated strings of IP ranges, specifics and ipMatches (IPLIKE expressions), + * parse them into arrays and validate their contents. + * The returned `status` and `invalidItem` can be used to create an error message. + */ + public static ValidatedDefinitionContents sanitizeAndValidateDefinitionItems( + final String specifics, final String ranges, final String ipMatches) { + List rangeItems = splitAndTrim(ranges, COMMA_DELIMITER); + List definitionIpMatches = splitAndTrim(ipMatches, COMMA_DELIMITER); + List definitionSpecifics = splitAndTrim(specifics, COMMA_DELIMITER); + List definitionRanges = new ArrayList<>(); + + for (String specific : definitionSpecifics) { + if (safeGetInetAddress(specific) == null) { + return new ValidatedDefinitionContents(DefinitionContentsValidationStatus.INVALID_SPECIFIC_ADDRESS, + specific, null, null, null); + } + } + + for (String range : rangeItems) { + List rangeSplit = splitAndTrim(range, RANGE_DELIMITER); + + if (rangeSplit.size() != 2) { + return new ValidatedDefinitionContents(DefinitionContentsValidationStatus.INVALID_RANGE, + range, null, null, null); + } + + Range r = new Range(rangeSplit.get(0), rangeSplit.get(1)); + + if (safeGetInetAddress(r.getBegin()) == null) { + return new ValidatedDefinitionContents(DefinitionContentsValidationStatus.INVALID_RANGE_BEGIN, + r.getBegin(), null, null, null); + } + + if (safeGetInetAddress(r.getEnd()) == null) { + return new ValidatedDefinitionContents(DefinitionContentsValidationStatus.INVALID_RANGE_END, + r.getEnd(), null, null, null); + } + + definitionRanges.add(r); + } + + if (definitionSpecifics.isEmpty() && definitionRanges.isEmpty() && definitionIpMatches.isEmpty()) { + return new ValidatedDefinitionContents(DefinitionContentsValidationStatus.INVALID_EMPTY, + null, null, null, null); + } + + return new ValidatedDefinitionContents(DefinitionContentsValidationStatus.VALID, + null, definitionIpMatches, definitionSpecifics, definitionRanges); + } + + /** + * Gather statistics about a Definition. For now, just whether each part is empty or not. + */ + private static DefinitionStats getDefinitionStats(Definition definition) { + boolean hasRange = definition.getRanges().stream() + .anyMatch(range -> !Strings.isNullOrEmpty(range.getBegin()) && !Strings.isNullOrEmpty(range.getEnd())); + + boolean hasSpecific = definition.getSpecifics().stream().anyMatch(spec -> !Strings.isNullOrEmpty(spec)); + boolean hasIpMatch = definition.getIpMatches().stream().anyMatch(match -> !Strings.isNullOrEmpty(match)); + + return new DefinitionStats(hasRange, hasSpecific, hasIpMatch); + } + + /** Return a valid InetAddress, or null if it could not be parsed. */ + public static InetAddress safeGetInetAddress(String ipAddress) { + InetAddress addr = null; + + try { + if (!Strings.isNullOrEmpty(ipAddress)) { + addr = InetAddressUtils.addr(ipAddress); + } + } catch (Exception e) { + return null; + } + + return addr; + } + + /** + * Expands compressed IPv6 notation (e.g. {@code 2001:db8::1}) into its full 8-hextet form + * (e.g. {@code 2001:db8:0:0:0:0:0:1}) so it can be validated against the IPv6 IPLIKE regex. + * Returns the input unchanged if it contains no {@code ::}. + * Returns {@code null} if the input is malformed (e.g. contains multiple {@code ::}). + */ + private static String expandIPv6Compressed(final String ipMatch) { + if (!ipMatch.contains("::")) { + return ipMatch; + } + + final String[] sides = ipMatch.split("::", -1); + + if (sides.length > 2) { + return null; + } + final String left = sides[0]; + final String right = sides.length > 1 ? sides[1] : ""; + + final int leftGroupCount = left.isEmpty() ? 0 : left.split(":").length; + final int rightGroupCount = right.isEmpty() ? 0 : right.split(":").length; + final int zeroGroupCount = 8 - leftGroupCount - rightGroupCount; + + final StringBuilder expanded = new StringBuilder(); + + if (!left.isEmpty()) { + expanded.append(left).append(":"); + } + + expanded.append("0:".repeat(Math.max(0, zeroGroupCount))); + + if (!right.isEmpty()) { + expanded.append(right); + } else { + expanded.deleteCharAt(expanded.length() - 1); + } + + return expanded.toString(); + } + + private static List splitAndTrim(final String source, final String delimiter) { + String trimmed = !Strings.isNullOrEmpty(source) ? source.trim() : ""; + + return Arrays.stream(trimmed.split(delimiter)) + .map(String::trim) + .filter(s -> !Strings.isNullOrEmpty(s)) + .toList(); + } +} diff --git a/opennms-config/src/main/java/org/opennms/netmgt/config/SnmpPeerFactory.java b/opennms-config/src/main/java/org/opennms/netmgt/config/SnmpPeerFactory.java index d2044205f598..0a91ac9640c5 100644 --- a/opennms-config/src/main/java/org/opennms/netmgt/config/SnmpPeerFactory.java +++ b/opennms-config/src/main/java/org/opennms/netmgt/config/SnmpPeerFactory.java @@ -725,72 +725,6 @@ private Definition findMatchingDefinition(SnmpConfig config, InetAddress inetAdd return definitions.stream().filter(definition -> matchDefinition(definition, inetAddress, location)).findFirst().orElse(null); } - private static Definition createDefinition(Definition matchingDefinition) { - Definition definition = new Definition(); - - definition.setProfileLabel(matchingDefinition.getProfileLabel()); - definition.setLocation(matchingDefinition.getLocation()); - // Fill configuration - definition.setProxyHost(matchingDefinition.getProxyHost()); - definition.setMaxVarsPerPdu(matchingDefinition.getMaxVarsPerPdu()); - definition.setMaxRepetitions(matchingDefinition.getMaxRepetitions()); - definition.setMaxRequestSize(matchingDefinition.getMaxRequestSize()); - - definition.setSecurityName(matchingDefinition.getSecurityName()); - definition.setSecurityLevel(matchingDefinition.getSecurityLevel()); - definition.setAuthPassphrase(matchingDefinition.getAuthPassphrase()); - definition.setAuthProtocol(matchingDefinition.getAuthProtocol()); - definition.setEngineId(matchingDefinition.getEngineId()); - definition.setContextEngineId(matchingDefinition.getContextEngineId()); - definition.setContextName(matchingDefinition.getContextName()); - definition.setEnterpriseId(matchingDefinition.getEnterpriseId()); - definition.setPrivacyPassphrase(matchingDefinition.getPrivacyPassphrase()); - definition.setPrivacyProtocol(matchingDefinition.getPrivacyProtocol()); - definition.setVersion(matchingDefinition.getVersion()); - definition.setReadCommunity(matchingDefinition.getReadCommunity()); - definition.setWriteCommunity(matchingDefinition.getWriteCommunity()); - definition.setPort(matchingDefinition.getPort()); - definition.setTimeout(matchingDefinition.getTimeout()); - definition.setTTL(matchingDefinition.getTTL()); - definition.setRetry(matchingDefinition.getRetry()); - - return definition; - } - - private boolean matchDefinition(Definition definition, InetAddress inetAddress, String location) { - boolean locationMatched = LocationUtils.doesLocationsMatch(location, definition.getLocation()); - - return locationMatched && matchingIpAddress(inetAddress, definition); - } - - private static boolean matchingIpAddress(InetAddress inetAddress, Definition definition) { - boolean matchingIpAddress = definition.getSpecifics().stream() - .anyMatch(saddr -> saddr.equals(inetAddress.getHostAddress())); - - if (!matchingIpAddress) { - return definition.getRanges().stream().anyMatch(range -> matchingRanges(inetAddress, range)); - } - - return true; - } - - private static boolean matchingRanges(InetAddress inetAddress, Range range) { - final byte[] addr = inetAddress.getAddress(); - final byte[] begin = InetAddressUtils.toIpAddrBytes(range.getBegin()); - final byte[] end = InetAddressUtils.toIpAddrBytes(range.getEnd()); - - final boolean inRange; - final ByteArrayComparator BYTE_ARRAY_COMPARATOR = new ByteArrayComparator(); - - if (BYTE_ARRAY_COMPARATOR.compare(begin, end) <= 0) { - inRange = InetAddressUtils.isInetAddressInRange(addr, begin, end); - } else { - inRange = InetAddressUtils.isInetAddressInRange(addr, end, begin); - } - - return inRange; - } - @Override public void saveAgentConfigAsDefinition(SnmpAgentConfig snmpAgentConfig, String location, String module) { Definition definition = new Definition(); @@ -1017,4 +951,94 @@ Boolean getEncryptionEnabled() { void setTextEncryptor(TextEncryptor textEncryptor) { this.textEncryptor = textEncryptor; } + + private static Definition createDefinition(Definition matchingDefinition) { + Definition definition = new Definition(); + + definition.setProfileLabel(matchingDefinition.getProfileLabel()); + definition.setLocation(matchingDefinition.getLocation()); + // Fill configuration + definition.setProxyHost(matchingDefinition.getProxyHost()); + definition.setMaxVarsPerPdu(matchingDefinition.getMaxVarsPerPdu()); + definition.setMaxRepetitions(matchingDefinition.getMaxRepetitions()); + definition.setMaxRequestSize(matchingDefinition.getMaxRequestSize()); + + definition.setSecurityName(matchingDefinition.getSecurityName()); + definition.setSecurityLevel(matchingDefinition.getSecurityLevel()); + definition.setAuthPassphrase(matchingDefinition.getAuthPassphrase()); + definition.setAuthProtocol(matchingDefinition.getAuthProtocol()); + definition.setEngineId(matchingDefinition.getEngineId()); + definition.setContextEngineId(matchingDefinition.getContextEngineId()); + definition.setContextName(matchingDefinition.getContextName()); + definition.setEnterpriseId(matchingDefinition.getEnterpriseId()); + definition.setPrivacyPassphrase(matchingDefinition.getPrivacyPassphrase()); + definition.setPrivacyProtocol(matchingDefinition.getPrivacyProtocol()); + definition.setVersion(matchingDefinition.getVersion()); + definition.setReadCommunity(matchingDefinition.getReadCommunity()); + definition.setWriteCommunity(matchingDefinition.getWriteCommunity()); + definition.setPort(matchingDefinition.getPort()); + definition.setTimeout(matchingDefinition.getTimeout()); + definition.setTTL(matchingDefinition.getTTL()); + definition.setRetry(matchingDefinition.getRetry()); + + return definition; + } + + private boolean matchDefinition(Definition definition, InetAddress inetAddress, String location) { + boolean locationMatched = LocationUtils.doesLocationsMatch(location, definition.getLocation()); + + return locationMatched && matchingIpAddress(inetAddress, definition); + } + + private static boolean matchingIpAddress(InetAddress inetAddress, Definition definition) { + boolean matchingIpAddress = definition.getSpecifics().stream() + .anyMatch(saddr -> matchingSpecific(inetAddress, saddr)); + + if (!matchingIpAddress) { + return definition.getRanges().stream().anyMatch(range -> matchingRanges(inetAddress, range)); + } + + return true; + } + + private static boolean matchingSpecific(InetAddress inetAddress, String specific) { + final byte[] addr = inetAddress.getAddress(); + final byte[] specificBytes; + + try { + specificBytes = InetAddressUtils.toIpAddrBytes(specific); + } catch (IllegalArgumentException ignored) { + return false; + } + + return InetAddressUtils.areSameInetAddress(addr, specificBytes); + } + + private static boolean matchingRanges(InetAddress inetAddress, Range range) { + if (range == null || range.getBegin() == null || range.getEnd() == null) { + return false; + } + + final byte[] addr = inetAddress.getAddress(); + final byte[] begin; + final byte[] end; + + try { + begin = InetAddressUtils.toIpAddrBytes(range.getBegin()); + end = InetAddressUtils.toIpAddrBytes(range.getEnd()); + } catch (IllegalArgumentException ignored) { + return false; + } + + final boolean inRange; + final ByteArrayComparator BYTE_ARRAY_COMPARATOR = new ByteArrayComparator(); + + if (BYTE_ARRAY_COMPARATOR.compare(begin, end) <= 0) { + inRange = InetAddressUtils.isInetAddressInRange(addr, begin, end); + } else { + inRange = InetAddressUtils.isInetAddressInRange(addr, end, begin); + } + + return inRange; + } } diff --git a/opennms-config/src/test/java/org/opennms/netmgt/config/SnmpConfigManagerTest.java b/opennms-config/src/test/java/org/opennms/netmgt/config/SnmpConfigManagerTest.java new file mode 100644 index 000000000000..9aa73a0a4e51 --- /dev/null +++ b/opennms-config/src/test/java/org/opennms/netmgt/config/SnmpConfigManagerTest.java @@ -0,0 +1,795 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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.opennms.netmgt.config; + +import junit.framework.TestCase; +import org.apache.commons.io.IOUtils; +import org.codehaus.jackson.map.ObjectMapper; +import org.opennms.netmgt.config.snmp.Definition; +import org.opennms.netmgt.config.snmp.Range; +import org.opennms.netmgt.config.snmp.SnmpConfig; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.List; + +public class SnmpConfigManagerTest extends TestCase { + private static final ObjectMapper mapper = new ObjectMapper(); + + private static final String BASIC_CONFIG_PATH = "snmp-config-manager-test-basic.json"; + private static final String BASIC_IPV6_CONFIG_PATH = "snmp-config-manager-test-ipv6.json"; + private static final String BASIC_IPV6_LARGE_CONFIG_PATH = "snmp-config-manager-test-ipv6-large.json"; + + private SnmpConfig getSnmpConfig(final String path) { + SnmpConfig config = null; + + try (final InputStream inputStream = this.getClass().getResourceAsStream(path)) { + assertNotNull("Could not read resource file from path: " + path, inputStream); + + config = mapper.readValue(new ByteArrayInputStream(IOUtils.toByteArray(inputStream)), SnmpConfig.class); + } catch (Exception e) { + fail("Could not read resource file from path: " + path); + } + + return config; + } + + // IPv4 tests + + public void testReadBasicConfig() { + SnmpConfig config = getSnmpConfig(BASIC_CONFIG_PATH); + assertNotNull(config); + + assertEquals("public", config.getReadCommunity()); + + List definitions = config.getDefinitions(); + + assertEquals(1, definitions.size()); + + Definition definition = definitions.get(0); + assertEquals(0, definition.getSpecifics().size()); + assertEquals(0, definition.getIpMatches().size()); + + assertEquals("public-1", definition.getReadCommunity()); + assertEquals(1, definition.getRanges().size()); + assertEquals("10.0.0.1", definition.getRanges().get(0).getBegin()); + assertEquals("10.0.0.9", definition.getRanges().get(0).getEnd()); + } + + public void testAddDefinitionSimpleIPv4() { + SnmpConfig config = getSnmpConfig(BASIC_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-2"); + definition.setRanges(List.of(new Range("11.0.0.1", "11.0.0.9"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + manager.mergeIntoConfig(definition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(2, updatedConfig.getDefinitions().size()); + + List updatedDefinitions = updatedConfig.getDefinitions(); + Definition definition0 = updatedDefinitions.get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(1, definition0.getRanges().size()); + assertEquals("10.0.0.1", definition0.getRanges().get(0).getBegin()); + assertEquals("10.0.0.9", definition0.getRanges().get(0).getEnd()); + + Definition definition1 = updatedDefinitions.get(1); + assertEquals(0, definition1.getSpecifics().size()); + assertEquals(0, definition1.getIpMatches().size()); + + assertEquals("public-2", definition1.getReadCommunity()); + assertEquals(1, definition1.getRanges().size()); + assertEquals("11.0.0.1", definition1.getRanges().get(0).getBegin()); + assertEquals("11.0.0.9", definition1.getRanges().get(0).getEnd()); + } + + // Merge multiple non-contiguous ranges into a config that has no overlap with them. + public void testMergeMultipleNonContiguousRangesIPv4() { + SnmpConfig config = getSnmpConfig(BASIC_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-2"); + definition.setRanges(List.of(new Range("12.0.0.1", "12.0.0.9"), new Range("192.168.0.1", "192.168.10.1"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + manager.mergeIntoConfig(definition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(2, updatedConfig.getDefinitions().size()); + + List updatedDefinitions = updatedConfig.getDefinitions(); + + // Original definition is unchanged + Definition definition0 = updatedDefinitions.get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(1, definition0.getRanges().size()); + assertEquals("10.0.0.1", definition0.getRanges().get(0).getBegin()); + assertEquals("10.0.0.9", definition0.getRanges().get(0).getEnd()); + + // New definition with both non-contiguous ranges + Definition definition1 = updatedDefinitions.get(1); + assertEquals(0, definition1.getSpecifics().size()); + assertEquals(0, definition1.getIpMatches().size()); + assertEquals("public-2", definition1.getReadCommunity()); + assertEquals(2, definition1.getRanges().size()); + assertEquals("12.0.0.1", definition1.getRanges().get(0).getBegin()); + assertEquals("12.0.0.9", definition1.getRanges().get(0).getEnd()); + assertEquals("192.168.0.1", definition1.getRanges().get(1).getBegin()); + assertEquals("192.168.10.1", definition1.getRanges().get(1).getEnd()); + } + + // Merge an overlapping range with the same readCommunity. + // The overlapping portion is removed from the existing definition, then the new range is merged back in. + // Adjacent ranges are coalesced, so the result is a single extended range. + public void testMergeOverlappingRangeSameCommunityIPv4() { + SnmpConfig config = getSnmpConfig(BASIC_CONFIG_PATH); + + // Overlaps with 10.0.0.1-10.0.0.9 and extends to 10.0.0.12; same community + Definition definition = new Definition(); + definition.setReadCommunity("public-1"); + definition.setRanges(List.of(new Range("10.0.0.8", "10.0.0.12"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + manager.mergeIntoConfig(definition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(1, updatedConfig.getDefinitions().size()); + + // 10.0.0.1-10.0.0.7 (surviving) adjoins 10.0.0.8-10.0.0.12 (new), so they coalesce + Definition definition0 = updatedConfig.getDefinitions().get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(1, definition0.getRanges().size()); + assertEquals("10.0.0.1", definition0.getRanges().get(0).getBegin()); + assertEquals("10.0.0.12", definition0.getRanges().get(0).getEnd()); + } + + // Merge an overlapping range with a different readCommunity. + // The overlap is stripped from the existing definition; a new definition is added. + public void testMergeOverlappingRangeDifferentCommunityIPv4() { + SnmpConfig config = getSnmpConfig(BASIC_CONFIG_PATH); + + // Overlaps with 10.0.0.1-10.0.0.9 at 10.0.0.8-10.0.0.9; different community + Definition definition = new Definition(); + definition.setReadCommunity("public-2"); + definition.setRanges(List.of(new Range("10.0.0.8", "10.0.0.12"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + manager.mergeIntoConfig(definition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(2, updatedConfig.getDefinitions().size()); + + List updatedDefinitions = updatedConfig.getDefinitions(); + + // Existing definition is trimmed to 10.0.0.1-10.0.0.7 + Definition definition0 = updatedDefinitions.get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(1, definition0.getRanges().size()); + assertEquals("10.0.0.1", definition0.getRanges().get(0).getBegin()); + assertEquals("10.0.0.7", definition0.getRanges().get(0).getEnd()); + + // New definition owns the overlapping and extending portion + Definition definition1 = updatedDefinitions.get(1); + assertEquals(0, definition1.getSpecifics().size()); + assertEquals(0, definition1.getIpMatches().size()); + assertEquals("public-2", definition1.getReadCommunity()); + assertEquals(1, definition1.getRanges().size()); + assertEquals("10.0.0.8", definition1.getRanges().get(0).getBegin()); + assertEquals("10.0.0.12", definition1.getRanges().get(0).getEnd()); + } + + // Two sequential merges with multiple overlapping ranges. + // First merge: ranges 12.0.0.1-12.0.0.9 and 12.0.0.3-12.0.0.17 (same community public-1 as existing) coalesce to 12.0.0.1-12.0.0.17. + // Second merge: range 10.0.0.3-10.0.0.11 with community public-3 strips the overlap from public-1. + public void testMergeMultipleOverlappingRangesIPv4() { + SnmpConfig config = getSnmpConfig(BASIC_CONFIG_PATH); + SnmpConfigManager manager = new SnmpConfigManager(config); + + // First merge: overlapping ranges with the same community as the existing definition; + // they coalesce to 12.0.0.1-12.0.0.17 and are merged into the public-1 definition + Definition firstDefinition = new Definition(); + firstDefinition.setReadCommunity("public-1"); + firstDefinition.setRanges(List.of(new Range("12.0.0.1", "12.0.0.9"), new Range("12.0.0.3", "12.0.0.17"))); + manager.mergeIntoConfig(firstDefinition); + + // Second merge: public-3 range overlaps with 10.0.0.3-10.0.0.9 (part of public-1's 10.0.0.1-10.0.0.9) + Definition secondDefinition = new Definition(); + secondDefinition.setReadCommunity("public-3"); + secondDefinition.setRanges(List.of(new Range("10.0.0.3", "10.0.0.11"))); + manager.mergeIntoConfig(secondDefinition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(2, updatedConfig.getDefinitions().size()); + + List updatedDefinitions = updatedConfig.getDefinitions(); + + // public-1 retains 10.0.0.1-10.0.0.2 (trimmed by public-3) and 12.0.0.1-12.0.0.17 + Definition definition0 = updatedDefinitions.get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(2, definition0.getRanges().size()); + assertEquals("10.0.0.1", definition0.getRanges().get(0).getBegin()); + assertEquals("10.0.0.2", definition0.getRanges().get(0).getEnd()); + assertEquals("12.0.0.1", definition0.getRanges().get(1).getBegin()); + assertEquals("12.0.0.17", definition0.getRanges().get(1).getEnd()); + + // public-3 owns 10.0.0.3-10.0.0.11 + Definition definition1 = updatedDefinitions.get(1); + assertEquals(0, definition1.getSpecifics().size()); + assertEquals(0, definition1.getIpMatches().size()); + assertEquals("public-3", definition1.getReadCommunity()); + assertEquals(1, definition1.getRanges().size()); + assertEquals("10.0.0.3", definition1.getRanges().get(0).getBegin()); + assertEquals("10.0.0.11", definition1.getRanges().get(0).getEnd()); + } + + // Remove the entire definition from the config. + public void testRemoveEntireDefinitionIPv4() { + SnmpConfig config = getSnmpConfig(BASIC_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-1"); + definition.setRanges(List.of(new Range("10.0.0.1", "10.0.0.9"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + boolean removed = manager.removeDefinition(definition); + + assertTrue(removed); + assertEquals(0, manager.getConfig().getDefinitions().size()); + } + + // Remove part of the definition from the config. + // Removing 10.0.0.3-10.0.0.6 from 10.0.0.1-10.0.0.9 splits the range into two. + public void testRemovePartialDefinitionIPv4() { + SnmpConfig config = getSnmpConfig(BASIC_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-1"); + definition.setRanges(List.of(new Range("10.0.0.3", "10.0.0.6"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + boolean removed = manager.removeDefinition(definition); + + assertTrue(removed); + + SnmpConfig updatedConfig = manager.getConfig(); + assertEquals(1, updatedConfig.getDefinitions().size()); + + Definition definition0 = updatedConfig.getDefinitions().get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(2, definition0.getRanges().size()); + assertEquals("10.0.0.1", definition0.getRanges().get(0).getBegin()); + assertEquals("10.0.0.2", definition0.getRanges().get(0).getEnd()); + assertEquals("10.0.0.7", definition0.getRanges().get(1).getBegin()); + assertEquals("10.0.0.9", definition0.getRanges().get(1).getEnd()); + } + + // IPv6 tests — mirror all of the above using the IPv6 config (fd00::1-fd00::9 / public-1) + + public void testReadBasicConfigIPv6() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_CONFIG_PATH); + assertNotNull(config); + + assertEquals("public", config.getReadCommunity()); + + List definitions = config.getDefinitions(); + assertEquals(1, definitions.size()); + + Definition definition = definitions.get(0); + assertEquals(0, definition.getSpecifics().size()); + assertEquals(0, definition.getIpMatches().size()); + + assertEquals("public-1", definition.getReadCommunity()); + assertEquals(1, definition.getRanges().size()); + assertEquals("fd00::1", definition.getRanges().get(0).getBegin()); + assertEquals("fd00::9", definition.getRanges().get(0).getEnd()); + } + + public void testAddDefinitionSimpleIPv6() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-2"); + definition.setRanges(List.of(new Range("fd00:1::1", "fd00:1::9"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + manager.mergeIntoConfig(definition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(2, updatedConfig.getDefinitions().size()); + + List updatedDefinitions = updatedConfig.getDefinitions(); + Definition definition0 = updatedDefinitions.get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(1, definition0.getRanges().size()); + assertEquals("fd00::1", definition0.getRanges().get(0).getBegin()); + assertEquals("fd00::9", definition0.getRanges().get(0).getEnd()); + + Definition definition1 = updatedDefinitions.get(1); + assertEquals(0, definition1.getSpecifics().size()); + assertEquals(0, definition1.getIpMatches().size()); + + assertEquals("public-2", definition1.getReadCommunity()); + assertEquals(1, definition1.getRanges().size()); + assertEquals("fd00:1::1", definition1.getRanges().get(0).getBegin()); + assertEquals("fd00:1::9", definition1.getRanges().get(0).getEnd()); + } + + public void testMergeMultipleNonContiguousRangesIPv6() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-2"); + definition.setRanges(List.of(new Range("fd00:2::1", "fd00:2::9"), new Range("fd00:3::1", "fd00:3::ff"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + manager.mergeIntoConfig(definition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(2, updatedConfig.getDefinitions().size()); + + List updatedDefinitions = updatedConfig.getDefinitions(); + + // Original definition is unchanged + Definition definition0 = updatedDefinitions.get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(1, definition0.getRanges().size()); + assertEquals("fd00::1", definition0.getRanges().get(0).getBegin()); + assertEquals("fd00::9", definition0.getRanges().get(0).getEnd()); + + // New definition with both non-contiguous ranges + Definition definition1 = updatedDefinitions.get(1); + assertEquals(0, definition1.getSpecifics().size()); + assertEquals(0, definition1.getIpMatches().size()); + assertEquals("public-2", definition1.getReadCommunity()); + assertEquals(2, definition1.getRanges().size()); + assertEquals("fd00:2::1", definition1.getRanges().get(0).getBegin()); + assertEquals("fd00:2::9", definition1.getRanges().get(0).getEnd()); + assertEquals("fd00:3::1", definition1.getRanges().get(1).getBegin()); + assertEquals("fd00:3::ff", definition1.getRanges().get(1).getEnd()); + } + + // Merge an overlapping range with the same readCommunity. + // fd00::8-fd00::c overlaps fd00::1-fd00::9; after purge the surviving fd00::1-fd00::7 + // adjoins the incoming fd00::8-fd00::c and they coalesce to fd00::1-fd00::c. + public void testMergeOverlappingRangeSameCommunityIPv6() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-1"); + definition.setRanges(List.of(new Range("fd00::8", "fd00::c"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + manager.mergeIntoConfig(definition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(1, updatedConfig.getDefinitions().size()); + + Definition definition0 = updatedConfig.getDefinitions().get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(1, definition0.getRanges().size()); + assertEquals("fd00::1", definition0.getRanges().get(0).getBegin()); + assertEquals("fd00::c", definition0.getRanges().get(0).getEnd()); + } + + // Merge an overlapping range with a different readCommunity. + // The overlap is stripped from the existing definition; a new definition is added. + public void testMergeOverlappingRangeDifferentCommunityIPv6() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-2"); + definition.setRanges(List.of(new Range("fd00::8", "fd00::c"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + manager.mergeIntoConfig(definition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(2, updatedConfig.getDefinitions().size()); + + List updatedDefinitions = updatedConfig.getDefinitions(); + + // Existing definition is trimmed to fd00::1-fd00::7 + Definition definition0 = updatedDefinitions.get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(1, definition0.getRanges().size()); + assertEquals("fd00::1", definition0.getRanges().get(0).getBegin()); + assertEquals("fd00::7", definition0.getRanges().get(0).getEnd()); + + // New definition owns the overlapping and extending portion + Definition definition1 = updatedDefinitions.get(1); + assertEquals(0, definition1.getSpecifics().size()); + assertEquals(0, definition1.getIpMatches().size()); + assertEquals("public-2", definition1.getReadCommunity()); + assertEquals(1, definition1.getRanges().size()); + assertEquals("fd00::8", definition1.getRanges().get(0).getBegin()); + assertEquals("fd00::c", definition1.getRanges().get(0).getEnd()); + } + + // Two sequential merges with multiple overlapping ranges. + // First merge: fd00:2::1-fd00:2::9 and fd00:2::3-fd00:2::11 (public-1) coalesce to fd00:2::1-fd00:2::11 + // and are merged into the existing public-1 definition. + // Second merge: fd00::3-fd00::b (public-3) strips the overlap from public-1's fd00::1-fd00::9. + public void testMergeMultipleOverlappingRangesIPv6() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_CONFIG_PATH); + SnmpConfigManager manager = new SnmpConfigManager(config); + + Definition firstDefinition = new Definition(); + firstDefinition.setReadCommunity("public-1"); + firstDefinition.setRanges(List.of(new Range("fd00:2::1", "fd00:2::9"), new Range("fd00:2::3", "fd00:2::11"))); + manager.mergeIntoConfig(firstDefinition); + + Definition secondDefinition = new Definition(); + secondDefinition.setReadCommunity("public-3"); + secondDefinition.setRanges(List.of(new Range("fd00::3", "fd00::b"))); + manager.mergeIntoConfig(secondDefinition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(2, updatedConfig.getDefinitions().size()); + + List updatedDefinitions = updatedConfig.getDefinitions(); + + // public-1 retains fd00::1-fd00::2 (trimmed by public-3) and fd00:2::1-fd00:2::11 + Definition definition0 = updatedDefinitions.get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(2, definition0.getRanges().size()); + assertEquals("fd00::1", definition0.getRanges().get(0).getBegin()); + assertEquals("fd00::2", definition0.getRanges().get(0).getEnd()); + assertEquals("fd00:2::1", definition0.getRanges().get(1).getBegin()); + assertEquals("fd00:2::11", definition0.getRanges().get(1).getEnd()); + + // public-3 owns fd00::3-fd00::b + Definition definition1 = updatedDefinitions.get(1); + assertEquals(0, definition1.getSpecifics().size()); + assertEquals(0, definition1.getIpMatches().size()); + assertEquals("public-3", definition1.getReadCommunity()); + assertEquals(1, definition1.getRanges().size()); + assertEquals("fd00::3", definition1.getRanges().get(0).getBegin()); + assertEquals("fd00::b", definition1.getRanges().get(0).getEnd()); + } + + public void testRemoveEntireDefinitionIPv6() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-1"); + definition.setRanges(List.of(new Range("fd00::1", "fd00::9"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + boolean removed = manager.removeDefinition(definition); + + assertTrue(removed); + assertEquals(0, manager.getConfig().getDefinitions().size()); + } + + // Removing fd00::3-fd00::6 from fd00::1-fd00::9 splits the range into two. + public void testRemovePartialDefinitionIPv6() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-1"); + definition.setRanges(List.of(new Range("fd00::3", "fd00::6"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + boolean removed = manager.removeDefinition(definition); + + assertTrue(removed); + + SnmpConfig updatedConfig = manager.getConfig(); + assertEquals(1, updatedConfig.getDefinitions().size()); + + Definition definition0 = updatedConfig.getDefinitions().get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(2, definition0.getRanges().size()); + assertEquals("fd00::1", definition0.getRanges().get(0).getBegin()); + assertEquals("fd00::2", definition0.getRanges().get(0).getEnd()); + assertEquals("fd00::7", definition0.getRanges().get(1).getBegin()); + assertEquals("fd00::9", definition0.getRanges().get(1).getEnd()); + } + + // Large 8-group IPv6 tests — all 8 hextets are non-zero so no :: compression occurs. + // Basic config range: 2001:db8:1:2:3:4:5:1-2001:db8:1:2:3:4:5:9 / public-1 + + public void testReadBasicConfigIPv6Large() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_LARGE_CONFIG_PATH); + assertNotNull(config); + + assertEquals("public", config.getReadCommunity()); + + List definitions = config.getDefinitions(); + assertEquals(1, definitions.size()); + + Definition definition = definitions.get(0); + assertEquals(0, definition.getSpecifics().size()); + assertEquals(0, definition.getIpMatches().size()); + + assertEquals("public-1", definition.getReadCommunity()); + assertEquals(1, definition.getRanges().size()); + assertEquals("2001:db8:1:2:3:4:5:1", definition.getRanges().get(0).getBegin()); + assertEquals("2001:db8:1:2:3:4:5:9", definition.getRanges().get(0).getEnd()); + } + + public void testAddDefinitionSimpleIPv6Large() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_LARGE_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-2"); + definition.setRanges(List.of(new Range("2001:db8:1:2:3:4:6:1", "2001:db8:1:2:3:4:6:9"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + manager.mergeIntoConfig(definition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(2, updatedConfig.getDefinitions().size()); + + List updatedDefinitions = updatedConfig.getDefinitions(); + Definition definition0 = updatedDefinitions.get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(1, definition0.getRanges().size()); + assertEquals("2001:db8:1:2:3:4:5:1", definition0.getRanges().get(0).getBegin()); + assertEquals("2001:db8:1:2:3:4:5:9", definition0.getRanges().get(0).getEnd()); + + Definition definition1 = updatedDefinitions.get(1); + assertEquals(0, definition1.getSpecifics().size()); + assertEquals(0, definition1.getIpMatches().size()); + + assertEquals("public-2", definition1.getReadCommunity()); + assertEquals(1, definition1.getRanges().size()); + assertEquals("2001:db8:1:2:3:4:6:1", definition1.getRanges().get(0).getBegin()); + assertEquals("2001:db8:1:2:3:4:6:9", definition1.getRanges().get(0).getEnd()); + } + + public void testMergeMultipleNonContiguousRangesIPv6Large() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_LARGE_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-2"); + definition.setRanges(List.of( + new Range("2001:db8:1:2:3:4:7:1", "2001:db8:1:2:3:4:7:9"), + new Range("2001:db8:1:2:3:4:8:1", "2001:db8:1:2:3:4:8:ff"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + manager.mergeIntoConfig(definition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(2, updatedConfig.getDefinitions().size()); + + List updatedDefinitions = updatedConfig.getDefinitions(); + + // Original definition is unchanged + Definition definition0 = updatedDefinitions.get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(1, definition0.getRanges().size()); + assertEquals("2001:db8:1:2:3:4:5:1", definition0.getRanges().get(0).getBegin()); + assertEquals("2001:db8:1:2:3:4:5:9", definition0.getRanges().get(0).getEnd()); + + // New definition with both non-contiguous ranges + Definition definition1 = updatedDefinitions.get(1); + assertEquals(0, definition1.getSpecifics().size()); + assertEquals(0, definition1.getIpMatches().size()); + assertEquals("public-2", definition1.getReadCommunity()); + assertEquals(2, definition1.getRanges().size()); + assertEquals("2001:db8:1:2:3:4:7:1", definition1.getRanges().get(0).getBegin()); + assertEquals("2001:db8:1:2:3:4:7:9", definition1.getRanges().get(0).getEnd()); + assertEquals("2001:db8:1:2:3:4:8:1", definition1.getRanges().get(1).getBegin()); + assertEquals("2001:db8:1:2:3:4:8:ff", definition1.getRanges().get(1).getEnd()); + } + + // Merge an overlapping range with the same readCommunity. + // 2001:db8:1:2:3:4:5:8-2001:db8:1:2:3:4:5:c overlaps 2001:db8:1:2:3:4:5:1-2001:db8:1:2:3:4:5:9; + // after purge the surviving 2001:db8:1:2:3:4:5:1-2001:db8:1:2:3:4:5:7 adjoins the incoming range + // and they coalesce to 2001:db8:1:2:3:4:5:1-2001:db8:1:2:3:4:5:c. + public void testMergeOverlappingRangeSameCommunityIPv6Large() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_LARGE_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-1"); + definition.setRanges(List.of(new Range("2001:db8:1:2:3:4:5:8", "2001:db8:1:2:3:4:5:c"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + manager.mergeIntoConfig(definition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(1, updatedConfig.getDefinitions().size()); + + Definition definition0 = updatedConfig.getDefinitions().get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(1, definition0.getRanges().size()); + assertEquals("2001:db8:1:2:3:4:5:1", definition0.getRanges().get(0).getBegin()); + assertEquals("2001:db8:1:2:3:4:5:c", definition0.getRanges().get(0).getEnd()); + } + + // Merge an overlapping range with a different readCommunity. + // The overlap is stripped from the existing definition; a new definition is added. + public void testMergeOverlappingRangeDifferentCommunityIPv6Large() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_LARGE_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-2"); + definition.setRanges(List.of(new Range("2001:db8:1:2:3:4:5:8", "2001:db8:1:2:3:4:5:c"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + manager.mergeIntoConfig(definition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(2, updatedConfig.getDefinitions().size()); + + List updatedDefinitions = updatedConfig.getDefinitions(); + + // Existing definition is trimmed to 2001:db8:1:2:3:4:5:1-2001:db8:1:2:3:4:5:7 + Definition definition0 = updatedDefinitions.get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(1, definition0.getRanges().size()); + assertEquals("2001:db8:1:2:3:4:5:1", definition0.getRanges().get(0).getBegin()); + assertEquals("2001:db8:1:2:3:4:5:7", definition0.getRanges().get(0).getEnd()); + + // New definition owns the overlapping and extending portion + Definition definition1 = updatedDefinitions.get(1); + assertEquals(0, definition1.getSpecifics().size()); + assertEquals(0, definition1.getIpMatches().size()); + assertEquals("public-2", definition1.getReadCommunity()); + assertEquals(1, definition1.getRanges().size()); + assertEquals("2001:db8:1:2:3:4:5:8", definition1.getRanges().get(0).getBegin()); + assertEquals("2001:db8:1:2:3:4:5:c", definition1.getRanges().get(0).getEnd()); + } + + // Two sequential merges with multiple overlapping ranges. + // First merge: 2001:db8:1:2:3:4:7:1-2001:db8:1:2:3:4:7:9 and 2001:db8:1:2:3:4:7:3-2001:db8:1:2:3:4:7:11 + // (public-1) coalesce to 2001:db8:1:2:3:4:7:1-2001:db8:1:2:3:4:7:11 and merge into existing public-1. + // Second merge: 2001:db8:1:2:3:4:5:3-2001:db8:1:2:3:4:5:b (public-3) strips the overlap from public-1. + public void testMergeMultipleOverlappingRangesIPv6Large() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_LARGE_CONFIG_PATH); + SnmpConfigManager manager = new SnmpConfigManager(config); + + Definition firstDefinition = new Definition(); + firstDefinition.setReadCommunity("public-1"); + firstDefinition.setRanges(List.of( + new Range("2001:db8:1:2:3:4:7:1", "2001:db8:1:2:3:4:7:9"), + new Range("2001:db8:1:2:3:4:7:3", "2001:db8:1:2:3:4:7:11"))); + manager.mergeIntoConfig(firstDefinition); + + Definition secondDefinition = new Definition(); + secondDefinition.setReadCommunity("public-3"); + secondDefinition.setRanges(List.of(new Range("2001:db8:1:2:3:4:5:3", "2001:db8:1:2:3:4:5:b"))); + manager.mergeIntoConfig(secondDefinition); + + SnmpConfig updatedConfig = manager.getConfig(); + assertNotNull(updatedConfig); + assertEquals(2, updatedConfig.getDefinitions().size()); + + List updatedDefinitions = updatedConfig.getDefinitions(); + + // public-1 retains 2001:db8:1:2:3:4:5:1-2001:db8:1:2:3:4:5:2 (trimmed by public-3) + // and 2001:db8:1:2:3:4:7:1-2001:db8:1:2:3:4:7:11 + Definition definition0 = updatedDefinitions.get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(2, definition0.getRanges().size()); + assertEquals("2001:db8:1:2:3:4:5:1", definition0.getRanges().get(0).getBegin()); + assertEquals("2001:db8:1:2:3:4:5:2", definition0.getRanges().get(0).getEnd()); + assertEquals("2001:db8:1:2:3:4:7:1", definition0.getRanges().get(1).getBegin()); + assertEquals("2001:db8:1:2:3:4:7:11", definition0.getRanges().get(1).getEnd()); + + // public-3 owns 2001:db8:1:2:3:4:5:3-2001:db8:1:2:3:4:5:b + Definition definition1 = updatedDefinitions.get(1); + assertEquals(0, definition1.getSpecifics().size()); + assertEquals(0, definition1.getIpMatches().size()); + assertEquals("public-3", definition1.getReadCommunity()); + assertEquals(1, definition1.getRanges().size()); + assertEquals("2001:db8:1:2:3:4:5:3", definition1.getRanges().get(0).getBegin()); + assertEquals("2001:db8:1:2:3:4:5:b", definition1.getRanges().get(0).getEnd()); + } + + public void testRemoveEntireDefinitionIPv6Large() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_LARGE_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-1"); + definition.setRanges(List.of(new Range("2001:db8:1:2:3:4:5:1", "2001:db8:1:2:3:4:5:9"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + boolean removed = manager.removeDefinition(definition); + + assertTrue(removed); + assertEquals(0, manager.getConfig().getDefinitions().size()); + } + + // Removing 2001:db8:1:2:3:4:5:3-2001:db8:1:2:3:4:5:6 from 2001:db8:1:2:3:4:5:1-2001:db8:1:2:3:4:5:9 + // splits the range into two. + public void testRemovePartialDefinitionIPv6Large() { + SnmpConfig config = getSnmpConfig(BASIC_IPV6_LARGE_CONFIG_PATH); + + Definition definition = new Definition(); + definition.setReadCommunity("public-1"); + definition.setRanges(List.of(new Range("2001:db8:1:2:3:4:5:3", "2001:db8:1:2:3:4:5:6"))); + + SnmpConfigManager manager = new SnmpConfigManager(config); + boolean removed = manager.removeDefinition(definition); + + assertTrue(removed); + + SnmpConfig updatedConfig = manager.getConfig(); + assertEquals(1, updatedConfig.getDefinitions().size()); + + Definition definition0 = updatedConfig.getDefinitions().get(0); + assertEquals(0, definition0.getSpecifics().size()); + assertEquals(0, definition0.getIpMatches().size()); + assertEquals("public-1", definition0.getReadCommunity()); + assertEquals(2, definition0.getRanges().size()); + assertEquals("2001:db8:1:2:3:4:5:1", definition0.getRanges().get(0).getBegin()); + assertEquals("2001:db8:1:2:3:4:5:2", definition0.getRanges().get(0).getEnd()); + assertEquals("2001:db8:1:2:3:4:5:7", definition0.getRanges().get(1).getBegin()); + assertEquals("2001:db8:1:2:3:4:5:9", definition0.getRanges().get(1).getEnd()); + } +} diff --git a/opennms-config/src/test/java/org/opennms/netmgt/config/SnmpConfigUtilsTest.java b/opennms-config/src/test/java/org/opennms/netmgt/config/SnmpConfigUtilsTest.java new file mode 100644 index 000000000000..27603a715197 --- /dev/null +++ b/opennms-config/src/test/java/org/opennms/netmgt/config/SnmpConfigUtilsTest.java @@ -0,0 +1,383 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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.opennms.netmgt.config; + +import org.junit.Test; +import org.opennms.netmgt.config.SnmpConfigUtils.DefinitionContentsValidationStatus; +import org.opennms.netmgt.config.SnmpConfigUtils.DefinitionStatsValidationStatus; +import org.opennms.netmgt.config.SnmpConfigUtils.ValidatedDefinitionContents; +import org.opennms.netmgt.config.snmp.Definition; +import org.opennms.netmgt.config.snmp.Range; + +import java.net.InetAddress; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class SnmpConfigUtilsTest { + + // ------------------------------------------------------------------------- + // validateDefinitionContents + // ------------------------------------------------------------------------- + + @Test + public void testValidateDefinitionContentsWithRange() { + Definition definition = new Definition(); + definition.addRange(new Range("10.0.0.1", "10.0.0.10")); + assertEquals(DefinitionStatsValidationStatus.VALID, SnmpConfigUtils.validateDefinitionContents(definition)); + } + + @Test + public void testValidateDefinitionContentsWithSpecific() { + Definition definition = new Definition(); + definition.addSpecific("10.0.0.1"); + assertEquals(DefinitionStatsValidationStatus.VALID, SnmpConfigUtils.validateDefinitionContents(definition)); + } + + @Test + public void testValidateDefinitionContentsWithIpMatch() { + Definition definition = new Definition(); + definition.addIpMatch("10.0.0.*"); + assertEquals(DefinitionStatsValidationStatus.VALID, SnmpConfigUtils.validateDefinitionContents(definition)); + } + + @Test + public void testValidateDefinitionContentsEmpty() { + Definition definition = new Definition(); + assertEquals(DefinitionStatsValidationStatus.MISSING_CONTENTS, SnmpConfigUtils.validateDefinitionContents(definition)); + } + + @Test + public void testValidateDefinitionContentsIpMatchWithRange() { + Definition definition = new Definition(); + definition.addIpMatch("10.0.0.*"); + definition.addRange(new Range("10.0.0.1", "10.0.0.10")); + assertEquals(DefinitionStatsValidationStatus.CANNOT_MIX_RANGE_AND_IPMATCH, SnmpConfigUtils.validateDefinitionContents(definition)); + } + + @Test + public void testValidateDefinitionContentsIpMatchWithSpecific() { + Definition definition = new Definition(); + definition.addIpMatch("10.0.0.*"); + definition.addSpecific("10.0.0.5"); + assertEquals(DefinitionStatsValidationStatus.CANNOT_MIX_RANGE_AND_IPMATCH, SnmpConfigUtils.validateDefinitionContents(definition)); + } + + // ------------------------------------------------------------------------- + // validateDefinitionIpAddresses + // ------------------------------------------------------------------------- + + @Test + public void testValidateDefinitionIpAddressesValidIPv4Specific() { + Definition definition = new Definition(); + definition.addSpecific("192.168.1.1"); + assertNull(SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + @Test + public void testValidateDefinitionIpAddressesValidIPv6Specific() { + Definition definition = new Definition(); + definition.addSpecific("2001:db8::1"); + assertNull(SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + @Test + public void testValidateDefinitionIpAddressesValidIPv6FullSpecific() { + Definition definition = new Definition(); + definition.addSpecific("2001:db8:1:2:3:4:5:1"); + assertNull(SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + @Test + public void testValidateDefinitionIpAddressesEmptySpecific() { + Definition definition = new Definition(); + definition.addSpecific(""); + assertEquals("Invalid specific IP address: empty value.", SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + @Test + public void testValidateDefinitionIpAddressesInvalidSpecific() { + Definition definition = new Definition(); + definition.addSpecific("10."); + assertEquals("Invalid specific IP address: 10.", SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + @Test + public void testValidateDefinitionIpAddressesValidIPv4Range() { + Definition definition = new Definition(); + definition.addRange(new Range("10.0.0.1", "10.0.0.100")); + assertNull(SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + @Test + public void testValidateDefinitionIpAddressesValidIPv6Range() { + Definition definition = new Definition(); + definition.addRange(new Range("fd00::1", "fd00::ff")); + assertNull(SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + @Test + public void testValidateDefinitionIpAddressesValidIPv6FullRange() { + Definition definition = new Definition(); + definition.addRange(new Range("fd00:0:0:0:0:0:0:1", "fd00:0:0:0:0:0:0:ff")); + assertNull(SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + @Test + public void testValidateDefinitionIpAddressesRangeEmptyBegin() { + Definition definition = new Definition(); + definition.addRange(new Range("", "10.0.0.100")); + assertNotNull(SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + @Test + public void testValidateDefinitionIpAddressesRangeInvalidBegin() { + Definition definition = new Definition(); + definition.addRange(new Range("10.", "10.0.0.100")); + assertEquals("Invalid range begin IP address: 10.", SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + @Test + public void testValidateDefinitionIpAddressesRangeInvalidEnd() { + Definition definition = new Definition(); + definition.addRange(new Range("10.0.0.1", "10.")); + assertEquals("Invalid range end IP address: 10.", SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + @Test + public void testValidateDefinitionIpAddressesMixedVersionRange() { + Definition definition = new Definition(); + definition.addRange(new Range("10.0.0.1", "fd00::ff")); + assertNotNull(SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + @Test + public void testValidateDefinitionIpAddressesMixedVersionRangeFullIPv6() { + Definition definition = new Definition(); + definition.addRange(new Range("10.0.0.1", "fd00:0:0:0:0:0:0:ff")); + assertNotNull(SnmpConfigUtils.validateDefinitionIpAddresses(definition)); + } + + // ------------------------------------------------------------------------- + // validateDefinitionIpMatches + // ------------------------------------------------------------------------- + + @Test + public void testValidateDefinitionIpMatchesValidIPv4Wildcard() { + Definition definition = new Definition(); + definition.addIpMatch("10.0.0.*"); + assertNull(SnmpConfigUtils.validateDefinitionIpMatches(definition)); + } + + @Test + public void testValidateDefinitionIpMatchesValidIPv4RangeAndComma() { + Definition definition = new Definition(); + definition.addIpMatch("192.168.1,2.1-10"); + assertNull(SnmpConfigUtils.validateDefinitionIpMatches(definition)); + } + + @Test + public void testValidateDefinitionIpMatchesValidIPv6FullyExpanded() { + Definition definition = new Definition(); + definition.addIpMatch("2001:db8:0:0:0:0:0:*"); + assertNull(SnmpConfigUtils.validateDefinitionIpMatches(definition)); + } + + @Test + public void testValidateDefinitionIpMatchesValidIPv6WithWildcardCompressed() { + Definition definition = new Definition(); + definition.addIpMatch("2001:db8::*"); + assertNull(SnmpConfigUtils.validateDefinitionIpMatches(definition)); + } + + @Test + public void testValidateDefinitionIpMatchesValidIPv6Compressed() { + Definition definition = new Definition(); + definition.addIpMatch("2001:db8::1"); + assertNull(SnmpConfigUtils.validateDefinitionIpMatches(definition)); + } + + @Test + public void testValidateDefinitionIpMatchesValidIPv6LeadingCompressed() { + Definition definition = new Definition(); + definition.addIpMatch("::1"); + assertNull(SnmpConfigUtils.validateDefinitionIpMatches(definition)); + } + + @Test + public void testValidateDefinitionIpMatchesValidIPv6Range() { + Definition definition = new Definition(); + definition.addIpMatch("2001:db8:0:0:0:0:0:1-ffff"); + assertNull(SnmpConfigUtils.validateDefinitionIpMatches(definition)); + } + + @Test + public void testValidateDefinitionIpMatchesEmptyExpression() { + Definition definition = new Definition(); + definition.addIpMatch(""); + assertEquals("Invalid IP match expression: empty value.", SnmpConfigUtils.validateDefinitionIpMatches(definition)); + } + + @Test + public void testValidateDefinitionIpMatchesInvalidIPv4() { + Definition definition = new Definition(); + definition.addIpMatch("10.0.0."); + assertEquals("Invalid IP match expression: '10.0.0.'.", SnmpConfigUtils.validateDefinitionIpMatches(definition)); + } + + @Test + public void testValidateDefinitionIpMatchesInvalidIPv6Chars() { + Definition definition = new Definition(); + definition.addIpMatch("2001:zzzz:0:0:0:0:0:1"); + assertEquals("Invalid IP match expression: '2001:zzzz:0:0:0:0:0:1'.", SnmpConfigUtils.validateDefinitionIpMatches(definition)); + } + + @Test + public void testValidateDefinitionIpMatchesInvalidIPv6CompressedWithBadChars() { + Definition definition = new Definition(); + definition.addIpMatch("2001:zzzz::1"); + assertEquals("Invalid IP match expression: '2001:zzzz::1'.", SnmpConfigUtils.validateDefinitionIpMatches(definition)); + } + + @Test + public void testValidateDefinitionIpMatchesInvalidIPv6MultipleDoubleColons() { + Definition definition = new Definition(); + definition.addIpMatch("2001::db8::1"); + assertEquals("Invalid IP match expression: '2001::db8::1'.", SnmpConfigUtils.validateDefinitionIpMatches(definition)); + } + + // ------------------------------------------------------------------------- + // sanitizeAndValidateDefinitionItems + // ------------------------------------------------------------------------- + + @Test + public void testSanitizeAndValidateDefinitionItemsValidSpecific() { + ValidatedDefinitionContents result = SnmpConfigUtils.sanitizeAndValidateDefinitionItems("10.0.0.1", null, null); + assertEquals(DefinitionContentsValidationStatus.VALID, result.status()); + assertEquals(List.of("10.0.0.1"), result.definitionSpecifics()); + assertEquals(List.of(), result.definitionRanges()); + assertEquals(List.of(), result.definitionIpMatches()); + } + + @Test + public void testSanitizeAndValidateDefinitionItemsMultipleSpecifics() { + ValidatedDefinitionContents result = SnmpConfigUtils.sanitizeAndValidateDefinitionItems("10.0.0.1, 10.0.0.2", null, null); + assertEquals(DefinitionContentsValidationStatus.VALID, result.status()); + assertEquals(List.of("10.0.0.1", "10.0.0.2"), result.definitionSpecifics()); + } + + @Test + public void testSanitizeAndValidateDefinitionItemsValidRange() { + ValidatedDefinitionContents result = SnmpConfigUtils.sanitizeAndValidateDefinitionItems(null, "10.0.0.1-10.0.0.10", null); + assertEquals(DefinitionContentsValidationStatus.VALID, result.status()); + assertEquals(1, result.definitionRanges().size()); + assertEquals("10.0.0.1", result.definitionRanges().get(0).getBegin()); + assertEquals("10.0.0.10", result.definitionRanges().get(0).getEnd()); + } + + @Test + public void testSanitizeAndValidateDefinitionItemsMultipleRanges() { + ValidatedDefinitionContents result = SnmpConfigUtils.sanitizeAndValidateDefinitionItems(null, "10.0.0.1-10.0.0.10,10.0.1.1-10.0.1.20", null); + assertEquals(DefinitionContentsValidationStatus.VALID, result.status()); + assertEquals(2, result.definitionRanges().size()); + } + + @Test + public void testSanitizeAndValidateDefinitionItemsValidIpMatch() { + ValidatedDefinitionContents result = SnmpConfigUtils.sanitizeAndValidateDefinitionItems(null, null, "10.0.0.*"); + assertEquals(DefinitionContentsValidationStatus.VALID, result.status()); + assertEquals(List.of("10.0.0.*"), result.definitionIpMatches()); + } + + @Test + public void testSanitizeAndValidateDefinitionItemsInvalidSpecific() { + ValidatedDefinitionContents result = SnmpConfigUtils.sanitizeAndValidateDefinitionItems("10.", null, null); + assertEquals(DefinitionContentsValidationStatus.INVALID_SPECIFIC_ADDRESS, result.status()); + assertEquals("10.", result.invalidItem()); + } + + @Test + public void testSanitizeAndValidateDefinitionItemsInvalidRangeFormat() { + ValidatedDefinitionContents result = SnmpConfigUtils.sanitizeAndValidateDefinitionItems(null, "10.0.0.1", null); + assertEquals(DefinitionContentsValidationStatus.INVALID_RANGE, result.status()); + } + + @Test + public void testSanitizeAndValidateDefinitionItemsInvalidRangeBegin() { + ValidatedDefinitionContents result = SnmpConfigUtils.sanitizeAndValidateDefinitionItems(null, "10.-10.0.0.10", null); + assertEquals(DefinitionContentsValidationStatus.INVALID_RANGE_BEGIN, result.status()); + assertEquals("10.", result.invalidItem()); + } + + @Test + public void testSanitizeAndValidateDefinitionItemsInvalidRangeEnd() { + ValidatedDefinitionContents result = SnmpConfigUtils.sanitizeAndValidateDefinitionItems(null, "10.0.0.1-10.", null); + assertEquals(DefinitionContentsValidationStatus.INVALID_RANGE_END, result.status()); + assertEquals("10.", result.invalidItem()); + } + + @Test + public void testSanitizeAndValidateDefinitionItemsAllEmpty() { + ValidatedDefinitionContents result = SnmpConfigUtils.sanitizeAndValidateDefinitionItems(null, null, null); + assertEquals(DefinitionContentsValidationStatus.INVALID_EMPTY, result.status()); + } + + @Test + public void testSanitizeAndValidateDefinitionItemsAllBlank() { + ValidatedDefinitionContents result = SnmpConfigUtils.sanitizeAndValidateDefinitionItems("", " ", ""); + assertEquals(DefinitionContentsValidationStatus.INVALID_EMPTY, result.status()); + } + + // ------------------------------------------------------------------------- + // safeGetInetAddress + // ------------------------------------------------------------------------- + + @Test + public void testSafeGetInetAddressValidIPv4() { + InetAddress addr = SnmpConfigUtils.safeGetInetAddress("192.168.1.1"); + assertNotNull(addr); + assertEquals("192.168.1.1", addr.getHostAddress()); + } + + @Test + public void testSafeGetInetAddressValidIPv6() { + InetAddress addr = SnmpConfigUtils.safeGetInetAddress("2001:db8::1"); + assertNotNull(addr); + } + + @Test + public void testSafeGetInetAddressInvalid() { + assertNull(SnmpConfigUtils.safeGetInetAddress("10.")); + } + + @Test + public void testSafeGetInetAddressNull() { + assertNull(SnmpConfigUtils.safeGetInetAddress(null)); + } + + @Test + public void testSafeGetInetAddressEmpty() { + assertNull(SnmpConfigUtils.safeGetInetAddress("")); + } +} diff --git a/opennms-config/src/test/java/org/opennms/netmgt/config/SnmpPeerFactoryTest.java b/opennms-config/src/test/java/org/opennms/netmgt/config/SnmpPeerFactoryTest.java index eccf9d784b85..70a506a18cc1 100644 --- a/opennms-config/src/test/java/org/opennms/netmgt/config/SnmpPeerFactoryTest.java +++ b/opennms-config/src/test/java/org/opennms/netmgt/config/SnmpPeerFactoryTest.java @@ -486,7 +486,7 @@ public void testReversedRange() throws UnknownHostException { assertEquals("rangev2c", agentConfig.getReadCommunity()); } - public void testSnmpv3WithNoAuthNoPriv() throws Exception { + public void testSnmpV3WithNoAuthNoPriv() throws Exception { SnmpPeerFactory.setResource(new ByteArrayResource(getSnmpConfig().getBytes())); SnmpAgentConfig agentConfig = SnmpPeerFactory.getInstance().getAgentConfig(InetAddressUtils.addr("10.11.12.13")); assertEquals("opennmsuser1", agentConfig.getSecurityName()); @@ -610,6 +610,161 @@ public void testRemoveSpecificIpAddress() { assertEquals("private", config.getWriteCommunity()); } + public void testRemoveSpecificIPv6Address() { + // add an IPv6 specific entry + final String ipv6Address = "2266:25::12:0:ad12"; + InetAddress addr = InetAddressUtils.addr(ipv6Address); + + SnmpPeerFactory.getInstance().saveDefinition(new Definition() {{ + setSpecifics(List.of(ipv6Address)); + setReadCommunity("read-ipv6-21"); + setVersion("v2c"); + }}, true); + + SnmpConfig snmpConfig = SnmpPeerFactory.getInstance().getSnmpConfig(); + + assertTrue(snmpConfig.getDefinitions().stream().anyMatch(d -> d.getSpecifics().contains(ipv6Address))); + + SnmpAgentConfig config = SnmpPeerFactory.getInstance().getAgentConfig(addr, "Default"); + assertNotNull(config); + assertEquals(2, config.getVersion()); + assertEquals("read-ipv6-21", config.getReadCommunity()); + + // now delete it + assertTrue(SnmpPeerFactory.getInstance().removeRangesFromDefinition(null, List.of(ipv6Address), null, "Default", "unit test")); + + final String expectedLogMessage = + String.format("Removed %d ranges, %d specifics, %d ipMatches from definitions at location %s by module %s", + 0, 1, 0, "Default", "unit test"); + + MockLogAppender.assertLogMatched(Level.INFO, expectedLogMessage); + + snmpConfig = SnmpPeerFactory.getInstance().getSnmpConfig(); + assertTrue(snmpConfig.getDefinitions().stream().noneMatch(d -> d.getSpecifics().contains(ipv6Address))); + + // config should have reverted to defaults + config = SnmpPeerFactory.getInstance().getAgentConfig(addr, "Default"); + assertNotNull(config); + assertEquals(1, config.getVersion()); + assertEquals("public", config.getReadCommunity()); + assertEquals("private", config.getWriteCommunity()); + } + + /** Add compressed, remove using full/expanded notation. */ + public void testRemoveSpecificIPv6AddressAddCompressedRemoveFull() { + final String compressedAddress = "2266:25::12:0:ad12"; + final String fullAddress = "2266:25:0:0:0:12:0:ad12"; + InetAddress addr = InetAddressUtils.addr(compressedAddress); + + SnmpPeerFactory.getInstance().saveDefinition(new Definition() {{ + setSpecifics(List.of(compressedAddress)); + setReadCommunity("read-ipv6-compressed"); + setVersion("v2c"); + }}, true); + + SnmpConfig snmpConfig = SnmpPeerFactory.getInstance().getSnmpConfig(); + assertTrue(snmpConfig.getDefinitions().stream().anyMatch(d -> d.getSpecifics().contains(compressedAddress))); + + SnmpAgentConfig config = SnmpPeerFactory.getInstance().getAgentConfig(addr, "Default"); + assertNotNull(config); + assertEquals(2, config.getVersion()); + assertEquals("read-ipv6-compressed", config.getReadCommunity()); + + // remove using full notation + assertTrue(SnmpPeerFactory.getInstance().removeRangesFromDefinition(null, List.of(fullAddress), null, "Default", "unit test")); + + final String expectedLogMessage = + String.format("Removed %d ranges, %d specifics, %d ipMatches from definitions at location %s by module %s", + 0, 1, 0, "Default", "unit test"); + MockLogAppender.assertLogMatched(Level.INFO, expectedLogMessage); + + snmpConfig = SnmpPeerFactory.getInstance().getSnmpConfig(); + assertTrue(snmpConfig.getDefinitions().stream().noneMatch(d -> d.getSpecifics().contains(compressedAddress))); + + config = SnmpPeerFactory.getInstance().getAgentConfig(addr, "Default"); + assertNotNull(config); + assertEquals(1, config.getVersion()); + assertEquals("public", config.getReadCommunity()); + assertEquals("private", config.getWriteCommunity()); + } + + /** Add full/expanded notation, remove using compressed notation. */ + public void testRemoveSpecificIPv6AddressAddFullRemoveCompressed() { + final String fullAddress = "2266:25:0:0:0:12:0:ad12"; + final String compressedAddress = "2266:25::12:0:ad12"; + InetAddress addr = InetAddressUtils.addr(fullAddress); + + SnmpPeerFactory.getInstance().saveDefinition(new Definition() {{ + setSpecifics(List.of(fullAddress)); + setReadCommunity("read-ipv6-full"); + setVersion("v2c"); + }}, true); + + SnmpConfig snmpConfig = SnmpPeerFactory.getInstance().getSnmpConfig(); + assertTrue(snmpConfig.getDefinitions().stream().anyMatch(d -> d.getSpecifics().contains(fullAddress))); + + SnmpAgentConfig config = SnmpPeerFactory.getInstance().getAgentConfig(addr, "Default"); + assertNotNull(config); + assertEquals(2, config.getVersion()); + assertEquals("read-ipv6-full", config.getReadCommunity()); + + // remove using compressed notation + assertTrue(SnmpPeerFactory.getInstance().removeRangesFromDefinition(null, List.of(compressedAddress), null, "Default", "unit test")); + + final String expectedLogMessage = + String.format("Removed %d ranges, %d specifics, %d ipMatches from definitions at location %s by module %s", + 0, 1, 0, "Default", "unit test"); + MockLogAppender.assertLogMatched(Level.INFO, expectedLogMessage); + + snmpConfig = SnmpPeerFactory.getInstance().getSnmpConfig(); + assertTrue(snmpConfig.getDefinitions().stream().noneMatch(d -> d.getSpecifics().contains(fullAddress))); + + config = SnmpPeerFactory.getInstance().getAgentConfig(addr, "Default"); + assertNotNull(config); + assertEquals(1, config.getVersion()); + assertEquals("public", config.getReadCommunity()); + assertEquals("private", config.getWriteCommunity()); + } + + public void testRemoveSpecificIPv6LongAddress() { + // add an IPv6 specific entry + final String ipv6Address = "2001:db8:1:2:3:4:5:1"; + InetAddress addr = InetAddressUtils.addr(ipv6Address); + + SnmpPeerFactory.getInstance().saveDefinition(new Definition() {{ + setSpecifics(List.of(ipv6Address)); + setReadCommunity("read-ipv6-21"); + setVersion("v2c"); + }}, true); + + SnmpConfig snmpConfig = SnmpPeerFactory.getInstance().getSnmpConfig(); + assertTrue(snmpConfig.getDefinitions().stream().anyMatch(d -> d.getSpecifics().contains(ipv6Address))); + + SnmpAgentConfig config = SnmpPeerFactory.getInstance().getAgentConfig(addr, "Default"); + assertNotNull(config); + assertEquals(2, config.getVersion()); + assertEquals("read-ipv6-21", config.getReadCommunity()); + + // now delete it + assertTrue(SnmpPeerFactory.getInstance().removeRangesFromDefinition(null, List.of(ipv6Address), null, "Default", "unit test")); + + final String expectedLogMessage = + String.format("Removed %d ranges, %d specifics, %d ipMatches from definitions at location %s by module %s", + 0, 1, 0, "Default", "unit test"); + + MockLogAppender.assertLogMatched(Level.INFO, expectedLogMessage); + + snmpConfig = SnmpPeerFactory.getInstance().getSnmpConfig(); + assertTrue(snmpConfig.getDefinitions().stream().noneMatch(d -> d.getSpecifics().contains(ipv6Address))); + + // config should have reverted to defaults + config = SnmpPeerFactory.getInstance().getAgentConfig(addr, "Default"); + assertNotNull(config); + assertEquals(1, config.getVersion()); + assertEquals("public", config.getReadCommunity()); + assertEquals("private", config.getWriteCommunity()); + } + public void testRemoveIpAddressRange() { // confirm definition with range exists SnmpConfig snmpConfig = SnmpPeerFactory.getInstance().getSnmpConfig(); diff --git a/opennms-config/src/test/resources/org/opennms/netmgt/config/snmp-config-manager-test-basic.json b/opennms-config/src/test/resources/org/opennms/netmgt/config/snmp-config-manager-test-basic.json new file mode 100644 index 000000000000..a6f65308e821 --- /dev/null +++ b/opennms-config/src/test/resources/org/opennms/netmgt/config/snmp-config-manager-test-basic.json @@ -0,0 +1,18 @@ +{ + "readCommunity": "public", + "definition": [ + { + "readCommunity": "public-1", + "range": [ + { + "begin": "10.0.0.1", + "end": "10.0.0.9" + } + ], + "specific": [], + "ipMatch": [], + "location": null, + "profileLabel": null + } + ] +} diff --git a/opennms-config/src/test/resources/org/opennms/netmgt/config/snmp-config-manager-test-ipv6-large.json b/opennms-config/src/test/resources/org/opennms/netmgt/config/snmp-config-manager-test-ipv6-large.json new file mode 100644 index 000000000000..b597dc545525 --- /dev/null +++ b/opennms-config/src/test/resources/org/opennms/netmgt/config/snmp-config-manager-test-ipv6-large.json @@ -0,0 +1,18 @@ +{ + "readCommunity": "public", + "definition": [ + { + "readCommunity": "public-1", + "range": [ + { + "begin": "2001:db8:1:2:3:4:5:1", + "end": "2001:db8:1:2:3:4:5:9" + } + ], + "specific": [], + "ipMatch": [], + "location": null, + "profileLabel": null + } + ] +} diff --git a/opennms-config/src/test/resources/org/opennms/netmgt/config/snmp-config-manager-test-ipv6.json b/opennms-config/src/test/resources/org/opennms/netmgt/config/snmp-config-manager-test-ipv6.json new file mode 100644 index 000000000000..387a80ab4d08 --- /dev/null +++ b/opennms-config/src/test/resources/org/opennms/netmgt/config/snmp-config-manager-test-ipv6.json @@ -0,0 +1,18 @@ +{ + "readCommunity": "public", + "definition": [ + { + "readCommunity": "public-1", + "range": [ + { + "begin": "fd00::1", + "end": "fd00::9" + } + ], + "specific": [], + "ipMatch": [], + "location": null, + "profileLabel": null + } + ] +} diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/SnmpConfigRestService.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/SnmpConfigRestService.java index bafe96e2fe2c..0d33f49560f3 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/SnmpConfigRestService.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/SnmpConfigRestService.java @@ -26,8 +26,6 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -36,12 +34,13 @@ import org.apache.cxf.jaxrs.ext.multipart.Attachment; import org.codehaus.jackson.JsonProcessingException; import org.codehaus.jackson.map.ObjectMapper; -import org.opennms.core.utils.InetAddressUtils; import org.opennms.core.xml.JaxbUtils; +import org.opennms.netmgt.config.SnmpConfigUtils; +import org.opennms.netmgt.config.SnmpConfigUtils.DefinitionContentsValidationStatus; +import org.opennms.netmgt.config.SnmpConfigUtils.ValidatedDefinitionContents; import org.opennms.netmgt.config.SnmpPeerFactory; import org.opennms.netmgt.config.snmp.Configuration; import org.opennms.netmgt.config.snmp.Definition; -import org.opennms.netmgt.config.snmp.Range; import org.opennms.netmgt.config.snmp.SnmpConfig; import org.opennms.netmgt.config.snmp.SnmpProfile; import org.opennms.netmgt.dao.api.MonitoringLocationDao; @@ -63,10 +62,6 @@ public class SnmpConfigRestService implements SnmpConfigRestApi { private static final Logger LOG = LoggerFactory.getLogger(SnmpConfigRestService.class); private static final String MODULE_NAME = "web rest api"; - private static final String COMMA_DELIMITER = ","; - private static final String RANGE_DELIMITER = "-"; - private static final String IPLIKE_VALIDATION_REGEX = "(([0-9]{1,3}(([,-])[0-9]{1,3})*|\\*)\\.){3}([0-9]{1,3}(([,-])[0-9]{1,3})*|\\*)"; - private static final ObjectMapper objectMapper = new ObjectMapper(); public static final String DEFINITION_MISSING_CONTENTS_MESSAGE = "Definition must have at least one specific IP, IP range or IP match specified."; @@ -74,8 +69,6 @@ public class SnmpConfigRestService implements SnmpConfigRestApi { "Cannot have an IP match expression along with IP ranges or specific IP addresses."; public static final String DEFINITION_NO_ITEMS_REMOVED_MESSAGE = "No configuration items removed, mostly likely no matching definitions found."; - record DefinitionContents(boolean hasRange, boolean hasSpecific, boolean hasIpMatch) {} - @Autowired private MonitoringLocationDao monitoringLocationDao; @@ -97,7 +90,7 @@ public Response getSnmpConfig() { @Override public Response getConfigForIp(final String ipAddress, final String location) { try { - InetAddress addr = safeGetInetAddress(ipAddress); + InetAddress addr = SnmpConfigUtils.safeGetInetAddress(ipAddress); if (addr == null) { return createBadRequestResponse("Missing or invalid 'ipAddress'."); @@ -151,13 +144,11 @@ public Response addDefinition(Definition definition) { return createBadRequestResponse("Missing or invalid request parameters."); } - DefinitionContents contents = getDefinitionContents(definition); + SnmpConfigUtils.DefinitionStatsValidationStatus status = SnmpConfigUtils.validateDefinitionContents(definition); - if (!contents.hasRange && !contents.hasSpecific && !contents.hasIpMatch) { + if (status == SnmpConfigUtils.DefinitionStatsValidationStatus.MISSING_CONTENTS) { return createBadRequestResponse(DEFINITION_MISSING_CONTENTS_MESSAGE); - } - - if (contents.hasIpMatch && (contents.hasRange || contents.hasSpecific)) { + } else if (status == SnmpConfigUtils.DefinitionStatsValidationStatus.CANNOT_MIX_RANGE_AND_IPMATCH) { return createBadRequestResponse(DEFINITION_CANNOT_MIX_RANGE_AND_IPMATCH_MESSAGE); } @@ -170,13 +161,14 @@ public Response addDefinition(Definition definition) { } // Validate IP addresses in the definition - String ipValidationError = validateDefinitionIpAddresses(definition); + String ipValidationError = SnmpConfigUtils.validateDefinitionIpAddresses(definition); + if (ipValidationError != null) { return createBadRequestResponse(ipValidationError); } // Validate IP match expressions in the definition - String ipMatchValidationError = validateDefinitionIpMatches(definition); + String ipMatchValidationError = SnmpConfigUtils.validateDefinitionIpMatches(definition); if (ipMatchValidationError != null) { return createBadRequestResponse(ipMatchValidationError); @@ -207,44 +199,31 @@ public Response removeDefinition(final String specifics, final String ranges, return createBadRequestResponse("Missing or invalid 'location'."); } - List rangeItems = splitAndTrim(ranges, COMMA_DELIMITER); - List definitionIpMatches = splitAndTrim(ipMatches, COMMA_DELIMITER); - List definitionSpecifics = splitAndTrim(specifics, COMMA_DELIMITER); - List definitionRanges = new ArrayList<>(); - - for (String specific : definitionSpecifics) { - if (safeGetInetAddress(specific) == null) { - return createBadRequestResponse("The specific IP address '" + specific + "' was invalid."); + ValidatedDefinitionContents validatedContents = SnmpConfigUtils.sanitizeAndValidateDefinitionItems(specifics, ranges, ipMatches); + + if (validatedContents.status() != DefinitionContentsValidationStatus.VALID) { + switch (validatedContents.status()) { + case INVALID_SPECIFIC_ADDRESS -> { + return createBadRequestResponse("The specific IP address '" + validatedContents.invalidItem() + "' was invalid."); + } + case INVALID_RANGE -> { + return createBadRequestResponse("Invalid range '" + validatedContents.invalidItem() + "'."); + } + case INVALID_RANGE_BEGIN -> { + return createBadRequestResponse("The range begin IP address '" + validatedContents.invalidItem() + "' was invalid."); + } + case INVALID_RANGE_END -> { + return createBadRequestResponse("The range end IP address '" + validatedContents.invalidItem() + "' was invalid."); + } + case INVALID_EMPTY -> { + return createBadRequestResponse("Must supply at least one specific or range of IP addresses or IP match expression to remove."); + } } } - for (String range : rangeItems) { - List rangeSplit = splitAndTrim(range, RANGE_DELIMITER); - - if (rangeSplit.size() != 2) { - return createBadRequestResponse("Invalid range '" + range + "'."); - } - - Range r = new Range(rangeSplit.get(0), rangeSplit.get(1)); - - if (safeGetInetAddress(r.getBegin()) == null) { - return createBadRequestResponse("The range begin IP address '" + r.getBegin() + "' was invalid."); - } - - if (safeGetInetAddress(r.getEnd()) == null) { - return createBadRequestResponse("The range end IP address '" + r.getEnd() + "' was invalid."); - } - - definitionRanges.add(r); - } - - if (definitionSpecifics.isEmpty() && definitionRanges.isEmpty() && definitionIpMatches.isEmpty()) { - return createBadRequestResponse("Must supply at least one specific or range of IP addresses or IP match expression to remove."); - } - // removes and also saves - boolean result = SnmpPeerFactory.getInstance() - .removeRangesFromDefinition(definitionRanges, definitionSpecifics, definitionIpMatches, validLocation, MODULE_NAME); + boolean result = SnmpPeerFactory.getInstance().removeRangesFromDefinition(validatedContents.definitionRanges(), + validatedContents.definitionSpecifics(), validatedContents.definitionIpMatches(), validLocation, MODULE_NAME); if (!result) { LOG.info(DEFINITION_NO_ITEMS_REMOVED_MESSAGE); @@ -398,16 +377,6 @@ private String convertToValidLocation(String location) { return location; } - private DefinitionContents getDefinitionContents(Definition definition) { - boolean hasRange = definition.getRanges().stream() - .anyMatch(range -> !Strings.isNullOrEmpty(range.getBegin()) && !Strings.isNullOrEmpty(range.getEnd())); - - boolean hasSpecific = definition.getSpecifics().stream().anyMatch(spec -> !Strings.isNullOrEmpty(spec)); - boolean hasIpMatch = definition.getIpMatches().stream().anyMatch(match -> !Strings.isNullOrEmpty(match)); - - return new DefinitionContents(hasRange, hasSpecific, hasIpMatch); - } - private static Response createBadRequestResponse(String message) { return Response.status(Response.Status.BAD_REQUEST) @@ -421,108 +390,4 @@ private static WebApplicationException createServerException(String message) { .type(MediaType.TEXT_PLAIN) .entity(message).build()); } - - /** Return a valid InetAddress, or null if it could not be parsed. */ - private static InetAddress safeGetInetAddress(String ipAddress) { - InetAddress addr = null; - - try { - if (!Strings.isNullOrEmpty(ipAddress)) { - addr = InetAddressUtils.addr(ipAddress); - } - } catch (Exception e) { - LOG.debug("Invalid IP address: {}", ipAddress, e); - } - - return addr; - } - - private static List splitAndTrim(final String source, final String delimiter) { - String trimmed = !Strings.isNullOrEmpty(source) ? source.trim() : ""; - - List items = - Arrays.stream(trimmed.split(delimiter)) - .map(String::trim) - .filter(s -> !Strings.isNullOrEmpty(s)) - .toList(); - - return items; - } - - /** - * Validates IP addresses in the Definition's specifics and ranges. - * @return error message if validation fails, null if valid - */ - private static String validateDefinitionIpAddresses(final Definition definition) { - for (final String specific : definition.getSpecifics()) { - if (Strings.isNullOrEmpty(specific)) { - return "Invalid specific IP address: empty value."; - } - InetAddress addr = null; - - try { - addr = InetAddressUtils.addr(specific); - } catch (IllegalArgumentException ignored) { - } - - if (addr == null) { - return String.format("Invalid specific IP address: %s", specific); - } - } - - for (final Range range : definition.getRanges()) { - if (Strings.isNullOrEmpty(range.getBegin()) || Strings.isNullOrEmpty(range.getEnd())) { - return String.format("Invalid range: begin and end must be specified. begin=%s, end=%s", - range.getBegin(), range.getEnd()); - } - - InetAddress beginAddr = null; - InetAddress endAddr = null; - - try { - beginAddr = InetAddressUtils.addr(range.getBegin()); - endAddr = InetAddressUtils.addr(range.getEnd()); - } catch (IllegalArgumentException ignored) { - } - - if (beginAddr == null) { - return String.format("Invalid range begin IP address: %s", range.getBegin()); - } - if (endAddr == null) { - return String.format("Invalid range end IP address: %s", range.getEnd()); - } - - // Ensure both addresses are the same IP version - // They should be either both Inet4Address or both Inet6Address, but not one of each - boolean beginIsV4 = beginAddr instanceof java.net.Inet4Address; - boolean endIsV4 = endAddr instanceof java.net.Inet4Address; - - if (beginIsV4 != endIsV4) { - return String.format("Invalid range: begin and end must be same IP version. begin=%s, end=%s", - range.getBegin(), range.getEnd()); - } - } - - return null; - } - - /** - * Validates IP match expressions in the Definition's ipMatch list. - * @return error message if validation fails, null if valid - */ - private static String validateDefinitionIpMatches(final Definition definition) { - for (final String ipMatch : definition.getIpMatches()) { - if (Strings.isNullOrEmpty(ipMatch)) { - return "Invalid IP match expression: empty value."; - } - - if (!ipMatch.matches(IPLIKE_VALIDATION_REGEX)) { - return String.format("Invalid IP match expression: '%s'.", ipMatch); - } - - // TODO: Consider more robust validation, for example checking octets are in the 0-255 range - } - - return null; - } } diff --git a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/SnmpConfigRestServiceIT.java b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/SnmpConfigRestServiceIT.java index 0e6317315043..d07addfda1b7 100644 --- a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/SnmpConfigRestServiceIT.java +++ b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/SnmpConfigRestServiceIT.java @@ -86,6 +86,8 @@ @JUnitTemporaryDatabase public class SnmpConfigRestServiceIT extends AbstractSpringJerseyRestTestCase { private static final org.codehaus.jackson.map.ObjectMapper mapper = new org.codehaus.jackson.map.ObjectMapper(); + private static final String SNMP_CONFIG_IPV4_RESOURCE_NAME = "snmp-config.xml"; + private static final String SNMP_CONFIG_IPV6_RESOURCE_NAME = "snmp-config-ipv6.xml"; public SnmpConfigRestServiceIT () { super(CXF_REST_V2_CONTEXT_PATH); @@ -97,14 +99,7 @@ public SnmpConfigRestServiceIT () { @Before public void setUp() { try { - // NOTE: Make sure 'snmpPeerFactory' setup in 'applicationContext-rest-test.xml' is - // set to 'MockSnmpPeerFactory' - // - URL xmlPath = Thread.currentThread().getContextClassLoader().getResource("snmp-config.xml"); - FileSystemResource resource = new FileSystemResource(xmlPath.getPath()); - - SnmpPeerFactory factory = new SnmpPeerFactory(resource); - SnmpPeerFactory.setInstance(factory); + populateSnmpPeerFactory(SNMP_CONFIG_IPV4_RESOURCE_NAME); } catch (Exception e) { Assert.fail("setUp failed"); } @@ -120,6 +115,17 @@ protected void afterServletStart() throws Exception { MockLogAppender.setupLogging(true, "DEBUG"); } + private void populateSnmpPeerFactory(final String resourceName) { + // NOTE: Make sure 'snmpPeerFactory' setup in 'applicationContext-rest-test.xml' is + // set to 'MockSnmpPeerFactory' + // + URL xmlPath = Thread.currentThread().getContextClassLoader().getResource(resourceName); + FileSystemResource resource = new FileSystemResource(xmlPath.getPath()); + + SnmpPeerFactory factory = new SnmpPeerFactory(resource); + SnmpPeerFactory.setInstance(factory); + } + @Test public void testGetSnmpConfig() throws Exception { Response response = snmpConfigRestApi.getSnmpConfig(); @@ -553,6 +559,131 @@ public void testAddDefinitionBadRequest() { assertEquals("Missing or invalid 'location'.", message); } + @Test + public void testAddDefinitionWithValidIpv4IpMatch() { + Definition definition = new Definition(); + definition.addIpMatch("10.0.0.*"); + definition.setLocation("Default"); + definition.setReadCommunity("testing-ipv4-ipmatch"); + + Response response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(201, response.getStatus()); + + definition = new Definition(); + definition.addIpMatch("192.168.1,2.1-10"); + definition.setLocation("Default"); + definition.setReadCommunity("testing-ipv4-ipmatch2"); + + response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(201, response.getStatus()); + } + + @Test + public void testAddDefinitionWithInvalidIpv4IpMatch() { + Definition definition = new Definition(); + definition.addIpMatch("10.0.0."); + definition.setLocation("Default"); + definition.setReadCommunity("testing-bad-ipmatch"); + + Response response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(400, response.getStatus()); + String message = (String) response.getEntity(); + assertEquals("Invalid IP match expression: '10.0.0.'.", message); + } + + @Test + public void testAddDefinitionWithValidIpv6IpMatch() { + // wildcard in last hextet, fully expanded + Definition definition = new Definition(); + definition.addIpMatch("2001:db8:0:0:0:0:0:*"); + definition.setLocation("Default"); + definition.setReadCommunity("testing-ipv6-ipmatch"); + + Response response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(201, response.getStatus()); + + // range in one hextet, fully expanded + definition = new Definition(); + definition.addIpMatch("2001:db8:0:0:0:0:0:1-ffff"); + definition.setLocation("Default"); + definition.setReadCommunity("testing-ipv6-ipmatch2"); + + response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(201, response.getStatus()); + + // comma-separated values in one hextet, fully expanded + definition = new Definition(); + definition.addIpMatch("fd00:0:0:0:0:0:0:1,2"); + definition.setLocation("Default"); + definition.setReadCommunity("testing-ipv6-ipmatch3"); + + response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(201, response.getStatus()); + + // compressed :: notation — expands to 2001:db8:0:0:0:0:0:1 + definition = new Definition(); + definition.addIpMatch("2001:db8::1"); + definition.setLocation("Default"); + definition.setReadCommunity("testing-ipv6-ipmatch4"); + + response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(201, response.getStatus()); + + // compressed :: with wildcard — expands to 2001:db8:0:0:0:0:0:* + definition = new Definition(); + definition.addIpMatch("2001:db8::*"); + definition.setLocation("Default"); + definition.setReadCommunity("testing-ipv6-ipmatch5"); + + response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(201, response.getStatus()); + + // leading :: — expands to 0:0:0:0:0:0:0:1 + definition = new Definition(); + definition.addIpMatch("::1"); + definition.setLocation("Default"); + definition.setReadCommunity("testing-ipv6-ipmatch6"); + + response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(201, response.getStatus()); + } + + @Test + public void testAddDefinitionWithInvalidIpv6IpMatch() { + // invalid hex characters, fully expanded + Definition definition = new Definition(); + definition.addIpMatch("2001:zzzz:0:0:0:0:0:1"); + definition.setLocation("Default"); + definition.setReadCommunity("testing-bad-ipv6-ipmatch"); + + Response response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(400, response.getStatus()); + String message = (String) response.getEntity(); + assertEquals("Invalid IP match expression: '2001:zzzz:0:0:0:0:0:1'.", message); + + // invalid hex characters with :: compression + definition = new Definition(); + definition.addIpMatch("2001:zzzz::1"); + definition.setLocation("Default"); + definition.setReadCommunity("testing-bad-ipv6-ipmatch2"); + + response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(400, response.getStatus()); + message = (String) response.getEntity(); + assertEquals("Invalid IP match expression: '2001:zzzz::1'.", message); + } + @Test public void testSaveProfile() throws Exception { SnmpProfile profile = new SnmpProfile(); @@ -1196,6 +1327,169 @@ public void testSaveDefaultOverrides_NullConfig() { assertEquals("Missing or invalid request body.", message); } + @Test + public void testAddAndRemoveSnmpDefinitionsIPv6() { + populateSnmpPeerFactory(SNMP_CONFIG_IPV6_RESOURCE_NAME); + + // Add a new definition with an IPv6 range + Definition definition = new Definition(); + definition.addRange(new Range("fd00:99::1", "fd00:99::2")); + definition.setLocation("Default"); + definition.setReadCommunity("testing99"); + + Response response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(201, response.getStatus()); + + // Check if config was updated with new community string for both IPs in range + response = snmpConfigRestApi.getConfigForIp("fd00:99::1", "Default"); + assertNotNull(response); + assertEquals(200, response.getStatus()); + + SnmpAgentConfig config = (SnmpAgentConfig) response.getEntity(); + assertNotNull(config); + assertEquals("testing99", config.getReadCommunity()); + + response = snmpConfigRestApi.getConfigForIp("fd00:99::2", "Default"); + assertNotNull(response); + assertEquals(200, response.getStatus()); + + config = (SnmpAgentConfig) response.getEntity(); + assertNotNull(config); + assertEquals("testing99", config.getReadCommunity()); + + // make sure community string for a previously-existing item was not changed + response = snmpConfigRestApi.getConfigForIp("fd00::1", "Default"); + assertNotNull(response); + assertEquals(200, response.getStatus()); + + config = (SnmpAgentConfig) response.getEntity(); + assertNotNull(config); + assertEquals("public", config.getReadCommunity()); + assertEquals("profile2", config.getProfileLabel()); + + // Delete part of the definition (first IP only) + response = snmpConfigRestApi.removeDefinition("fd00:99::1", null, null, "Default"); + assertNotNull(response); + assertEquals(204, response.getStatus()); + + // Check if config reverted to the default for the deleted IP + response = snmpConfigRestApi.getConfigForIp("fd00:99::1", "Default"); + assertNotNull(response); + assertEquals(200, response.getStatus()); + config = (SnmpAgentConfig) response.getEntity(); + assertNotNull(config); + assertEquals("public", config.getReadCommunity()); + + // config for not-yet-deleted IP should still be there + response = snmpConfigRestApi.getConfigForIp("fd00:99::2", "Default"); + assertNotNull(response); + assertEquals(200, response.getStatus()); + + config = (SnmpAgentConfig) response.getEntity(); + assertNotNull(config); + assertEquals("testing99", config.getReadCommunity()); + + // Delete the remaining IP + response = snmpConfigRestApi.removeDefinition("fd00:99::2", null, null, "Default"); + assertNotNull(response); + assertEquals(204, response.getStatus()); + + // Check if config reverted to the default + response = snmpConfigRestApi.getConfigForIp("fd00:99::2", "Default"); + assertNotNull(response); + assertEquals(200, response.getStatus()); + + config = (SnmpAgentConfig) response.getEntity(); + assertNotNull(config); + assertEquals("public", config.getReadCommunity()); + } + + @Test + public void testAddAndRemoveDefinitionRangesIPv6() { + populateSnmpPeerFactory(SNMP_CONFIG_IPV6_RESOURCE_NAME); + + // get the original config + final SnmpConfig originalConfig = getCurrentConfig(); + + // Add a new definition with an IPv6 range and a specific + Definition definition = new Definition(); + definition.addRange(new Range("fd00:99::1", "fd00:99::63")); + definition.addSpecific("fd00::1"); + definition.setLocation("Default"); + definition.setReadCommunity("testing99"); + + Response response = snmpConfigRestApi.addDefinition(definition); + assertNotNull(response); + assertEquals(201, response.getStatus()); + + // config should have changed + final SnmpConfig updatedConfigAfterAdd = getCurrentConfig(); + assertNotEquals(originalConfig, updatedConfigAfterAdd); + + // Check if all IPs in the range and the specific have the new community string + List ipsToTest = + List.of("fd00::1", "fd00:99::1", "fd00:99::2", "fd00:99::62", "fd00:99::63"); + + ipsToTest.forEach(ip -> { + Response resp = snmpConfigRestApi.getConfigForIp(ip, "Default"); + assertNotNull(resp); + assertEquals(200, resp.getStatus()); + + SnmpAgentConfig config = (SnmpAgentConfig) resp.getEntity(); + assertNotNull(config); + assertEquals("testing99", config.getReadCommunity()); + }); + + // Remove the range only + response = snmpConfigRestApi.removeDefinition(null, "fd00:99::1-fd00:99::63", null, "Default"); + assertNotNull(response); + assertEquals(204, response.getStatus()); + + // config should have changed + final SnmpConfig updatedConfigAfterDelete = getCurrentConfig(); + assertNotEquals(updatedConfigAfterDelete, updatedConfigAfterAdd); + + // IPs in the deleted range should revert to the default community + List deletedRangeIps = + List.of("fd00:99::1", "fd00:99::2", "fd00:99::62", "fd00:99::63"); + + deletedRangeIps.forEach(ip -> { + Response resp = snmpConfigRestApi.getConfigForIp(ip, "Default"); + assertNotNull(resp); + assertEquals(200, resp.getStatus()); + + SnmpAgentConfig config = (SnmpAgentConfig) resp.getEntity(); + assertNotNull(config); + assertEquals("public", config.getReadCommunity()); + }); + + // the specific was not deleted, should still have updated config + response = snmpConfigRestApi.getConfigForIp("fd00::1", "Default"); + assertNotNull(response); + assertEquals(200, response.getStatus()); + SnmpAgentConfig config = (SnmpAgentConfig) response.getEntity(); + assertNotNull(config); + assertEquals("testing99", config.getReadCommunity()); + + // Now delete the specific + response = snmpConfigRestApi.removeDefinition("fd00::1", null, null, "Default"); + assertNotNull(response); + assertEquals(204, response.getStatus()); + + // config should have changed + final SnmpConfig updatedConfigAfterSecondDelete = getCurrentConfig(); + assertNotEquals(updatedConfigAfterSecondDelete, updatedConfigAfterAdd); + + // specific should now revert to the default community + response = snmpConfigRestApi.getConfigForIp("fd00::1", "Default"); + assertNotNull(response); + assertEquals(200, response.getStatus()); + config = (SnmpAgentConfig) response.getEntity(); + assertNotNull(config); + assertEquals("public", config.getReadCommunity()); + } + /** * Helper method to get the Rest service's current SNMP config. */ diff --git a/opennms-webapp-rest/src/test/resources/snmp-config-ipv6.xml b/opennms-webapp-rest/src/test/resources/snmp-config-ipv6.xml new file mode 100644 index 000000000000..21845d683b90 --- /dev/null +++ b/opennms-webapp-rest/src/test/resources/snmp-config-ipv6.xml @@ -0,0 +1,25 @@ + + + + + + + fd00:1::1 + + + fd00:2::1 + + + + + + + + iphostname LIKE '%opennms%' + + + + IPADDR IPLIKE 172.1.*.* + + +