From b512ce3251e9c709a19254c8890d124a491614a2 Mon Sep 17 00:00:00 2001 From: maalej Date: Wed, 27 Aug 2025 14:54:43 +0200 Subject: [PATCH 1/4] Issue #60 Add OfflineSnmpFileClient for emulation purpose * Develop a new client for testing in emulation mode --- .../metricshub/snmp/client/ISnmpClient.java | 74 ++++++++ .../snmp/client/OfflineSnmpFileClient.java | 170 ++++++++++++++++++ .../metricshub/snmp/client/SnmpClient.java | 2 +- 3 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/metricshub/snmp/client/ISnmpClient.java create mode 100644 src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java diff --git a/src/main/java/org/metricshub/snmp/client/ISnmpClient.java b/src/main/java/org/metricshub/snmp/client/ISnmpClient.java new file mode 100644 index 0000000..780bbc2 --- /dev/null +++ b/src/main/java/org/metricshub/snmp/client/ISnmpClient.java @@ -0,0 +1,74 @@ +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * SNMP Java Client + * ჻჻჻჻჻჻ + * Copyright 2023 MetricsHub + * ჻჻჻჻჻჻ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +package org.metricshub.snmp.client; + +import java.util.List; + +/** + * Unified abstraction that both {@link SnmpClient} (live network) and + * {@link OfflineSnmpFileClient} (file‑backed) can implement. + *

+ * All operations return plain {@link String}s – no {@code Optional}s – so that + * the API mirrors {@code SnmpClient}'s existing contract and remains + * straightforward for scripting / CLI use‑cases. + *

+ * Return formats + *

+ */ +public interface ISnmpClient { + + /** SNMP GET. */ + String get(String oid) throws Exception; + + /** SNMP GETNEXT. */ + String getNext(String oid) throws Exception; + + + /** + * Reads an SNMP table. + * + * @param rootOID root OID of the table (e.g. …7.1) + * @param selectColumns numeric column indexes or "ID" for the row index + * @return rows serialized with semicolons (one row per line) + */ + List> table(String rootOID, String[] selectColumns) throws Exception; + + /** + * Strips the leading dot from an OID string if it exists. + * + * @param s the OID string to process. + * @return the OID string without a leading dot. + */ + default String stripDot(String s) { + return s.startsWith(".") ? s.substring(1) : s; + } + + String walk(String oid) throws Exception; +} diff --git a/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java b/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java new file mode 100644 index 0000000..641994f --- /dev/null +++ b/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java @@ -0,0 +1,170 @@ +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * SNMP Java Client + * ჻჻჻჻჻჻ + * Copyright 2023 MetricsHub + * ჻჻჻჻჻჻ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +package org.metricshub.snmp.client; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; + +public final class OfflineSnmpFileClient implements ISnmpClient { + + /** OID → value, sorted for efficient GETNEXT. */ + private final NavigableMap oidValues = new TreeMap<>(); + + /** + * Constructs an SNMP client that reads OID values from a file. + * + * @param file the path to the file containing OID values in UTF-16 encoding. + * @throws IOException if an I/O error occurs while reading the file. + */ + public OfflineSnmpFileClient(Path file) throws IOException { + try (final BufferedReader br = + Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (!line.contains("\t")) continue; + final String[] parts = line.split("\\t", 3); + if (parts.length < 3) { + continue; + } + final String oid = stripDot(parts[0]); + final String value = parts[2]; + oidValues.put(oid, value); + } + } + } + + /** + * SNMP GET: returns the value or throws if the OID does not exist. + */ + public String get(String oid) throws Exception { + final String key = stripDot(oid); + final String val = oidValues.get(key); + if (val == null) { + throw new Exception("(no-such-oid)"); + } + return val; + } + + /** + * SNMP GETNEXT: returns a TAB-separated triple "OIDTYPEVALUE", + * or "(end-of-mib-view)" if there is no next entry. + *

+ * Note: the offline client currently doesn’t retain the original ASN.1 type, + * so this uses a placeholder ("string"). If you want to preserve the type you + * should adjust the constructor to keep parts[1] from the input file and store + * both type and value. + */ + public String getNext(String oid) { + Map.Entry next = oidValues.higherEntry(stripDot(oid)); + if (next == null) { + return "(end-of-mib-view)"; + } + String nextOid = next.getKey(); + String value = next.getValue(); + String type = "string"; // placeholder since original type isn’t stored + return nextOid + "\t" + type + "\t" + value; + } + + /** + * Reconstructs an SNMP table under rootOID. + * + * @param rootOID the entry OID prefix (e.g. …7.1) + * @param selectColumns array of column numbers as strings ("1","2",… or "ID") + * @return list of rows, each a List in selectColumns order + */ + public List> table(String rootOID, String[] selectColumns) { + if (rootOID == null || rootOID.length() < 3) { + throw new IllegalArgumentException("Invalid SNMP Table OID: " + rootOID); + } + if (selectColumns == null || selectColumns.length < 1) { + throw new IllegalArgumentException("Invalid SNMP Table columns"); + } + + final String base = stripDot(rootOID); + + // find first column under base + Map.Entry firstEntry = oidValues.higherEntry(base); + if (firstEntry == null || !firstEntry.getKey().startsWith(base + ".")) { + return new ArrayList<>(); // empty table + } + + final String firstKey = firstEntry.getKey(); // e.g. base + ".1." + final int dotAfter = firstKey.indexOf('.', base.length() + 1); + if (dotAfter < 0) return new ArrayList<>(); + + final String firstColOid = firstKey.substring(0, dotAfter); // e.g. "…7.1" + int colOidLen = firstColOid.length(); + + // collect row IDs + final List ids = new ArrayList<>(); + String cursor = firstColOid; + while (true) { + Map.Entry nxt = oidValues.higherEntry(cursor); + if (nxt == null) break; + final String key = nxt.getKey(); + if (!key.startsWith(firstColOid + ".")) { + break; + } + final String id = key.substring(colOidLen + 1); + ids.add(id); + cursor = key; + if (ids.size() > 10000) { + break; + } + } + + // build result rows + final List> rows = new ArrayList<>(ids.size()); + for (String id : ids) { + List row = new ArrayList<>(selectColumns.length); + for (String col : selectColumns) { + if ("ID".equals(col)) { + row.add(id); + } else { + String lookupOid = base + "." + col + "." + id; + try { + row.add(get(lookupOid)); + } catch (Exception e) { + row.add(""); + } + } + } + rows.add(row); + } + return rows; + } + + @Override + public String walk(String oid) { + return ""; + } +} \ No newline at end of file diff --git a/src/main/java/org/metricshub/snmp/client/SnmpClient.java b/src/main/java/org/metricshub/snmp/client/SnmpClient.java index 7bcfc34..a2f4c1e 100644 --- a/src/main/java/org/metricshub/snmp/client/SnmpClient.java +++ b/src/main/java/org/metricshub/snmp/client/SnmpClient.java @@ -41,7 +41,7 @@ import java.util.List; import java.util.Set; -public class SnmpClient { +public class SnmpClient implements ISnmpClient { // Default SNMP port number public static final int SNMP_PORT = 161; From 1df630a023101c28e3360a29ea107c564cd3bd86 Mon Sep 17 00:00:00 2001 From: maalej Date: Thu, 4 Sep 2025 10:25:13 +0200 Subject: [PATCH 2/4] Issue #60 Create offline SNMP client * Update code after review * Add unit test --- .../metricshub/snmp/client/ISnmpClient.java | 40 +++---- .../snmp/client/OfflineSnmpFileClient.java | 86 ++++++++------ .../client/OfflineSnmpFileClientTest.java | 105 ++++++++++++++++++ 3 files changed, 181 insertions(+), 50 deletions(-) create mode 100644 src/test/java/org/metricshub/snmp/client/OfflineSnmpFileClientTest.java diff --git a/src/main/java/org/metricshub/snmp/client/ISnmpClient.java b/src/main/java/org/metricshub/snmp/client/ISnmpClient.java index 780bbc2..21aa98c 100644 --- a/src/main/java/org/metricshub/snmp/client/ISnmpClient.java +++ b/src/main/java/org/metricshub/snmp/client/ISnmpClient.java @@ -27,36 +27,32 @@ /** * Unified abstraction that both {@link SnmpClient} (live network) and * {@link OfflineSnmpFileClient} (file‑backed) can implement. - *

- * All operations return plain {@link String}s – no {@code Optional}s – so that - * the API mirrors {@code SnmpClient}'s existing contract and remains - * straightforward for scripting / CLI use‑cases. - *

- * Return formats - *

    - *
  • get – the value of the OID, as produced by the underlying client.
  • - *
  • getNext – a TAB‑separated triple “OID TYPE VALUE” - * (same convention as {@code SnmpClient#getNext}).
  • - *
  • walk – a multi‑line string, one “OID TYPE VALUE” per line.
  • - *
  • table – a multi‑line string; each row is the selected columns - * joined by semicolons.
  • - *
*/ public interface ISnmpClient { - /** SNMP GET. */ + /** + * Performs SNMP get action for a single OID. + * @param oid A given OID. + * @return The corresponding object as a string. + * @throws Exception on error (e.g. no such OID). + */ String get(String oid) throws Exception; - /** SNMP GETNEXT. */ + /** + * Performs SNMP getNext action for a single OID. + * @param oid A given OID. + * @return The corresponding object as a string. + * @throws Exception on error (e.g. no such OID). + */ String getNext(String oid) throws Exception; /** * Reads an SNMP table. * - * @param rootOID root OID of the table (e.g. …7.1) - * @param selectColumns numeric column indexes or "ID" for the row index - * @return rows serialized with semicolons (one row per line) + * @param rootOID Root OID of the table (e.g. …7.1) + * @param selectColumns Numeric column indexes or "ID" for the row index + * @return Rows serialized with semicolons (one row per line) */ List> table(String rootOID, String[] selectColumns) throws Exception; @@ -70,5 +66,11 @@ default String stripDot(String s) { return s.startsWith(".") ? s.substring(1) : s; } + /** + * Performs SNMP walk action starting from a given OID. + * @param oid A given OID. + * @return The corresponding object as a string. + * @throws Exception on error (e.g. no such OID). + */ String walk(String oid) throws Exception; } diff --git a/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java b/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java index 641994f..6b80faf 100644 --- a/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java +++ b/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java @@ -33,10 +33,24 @@ import java.util.NavigableMap; import java.util.TreeMap; +/** + * An offline SNMP client that reads OID values from a snmp WALK dump file and executes + * SNMP operations (GET, GET_NEXT, TABLE, WALK) against those OIDs. + */ public final class OfflineSnmpFileClient implements ISnmpClient { - /** OID → value, sorted for efficient GETNEXT. */ - private final NavigableMap oidValues = new TreeMap<>(); + /** Container for Snmp object's type and value. */ + private static final class SnmpValue { + final String type; + final String value; + SnmpValue(String type, String value) { + this.type = type; + this.value = value; + } + } + + // Maps the OIDs to their corresponding SnmpValue (type + value). + private final NavigableMap oidValues = new TreeMap<>(); /** * Constructs an SNMP client that reads OID values from a file. @@ -56,51 +70,35 @@ public OfflineSnmpFileClient(Path file) throws IOException { continue; } final String oid = stripDot(parts[0]); + final String type = parts[1]; final String value = parts[2]; - oidValues.put(oid, value); + oidValues.put(oid, new SnmpValue(type, value)); } } } - /** - * SNMP GET: returns the value or throws if the OID does not exist. - */ + @Override public String get(String oid) throws Exception { final String key = stripDot(oid); - final String val = oidValues.get(key); + final SnmpValue val = oidValues.get(key); if (val == null) { throw new Exception("(no-such-oid)"); } - return val; + return val.value; } - /** - * SNMP GETNEXT: returns a TAB-separated triple "OIDTYPEVALUE", - * or "(end-of-mib-view)" if there is no next entry. - *

- * Note: the offline client currently doesn’t retain the original ASN.1 type, - * so this uses a placeholder ("string"). If you want to preserve the type you - * should adjust the constructor to keep parts[1] from the input file and store - * both type and value. - */ + @Override public String getNext(String oid) { - Map.Entry next = oidValues.higherEntry(stripDot(oid)); + Map.Entry next = oidValues.higherEntry(stripDot(oid)); if (next == null) { return "(end-of-mib-view)"; } String nextOid = next.getKey(); - String value = next.getValue(); - String type = "string"; // placeholder since original type isn’t stored - return nextOid + "\t" + type + "\t" + value; + SnmpValue snmpVal = next.getValue(); + return nextOid + "\t" + snmpVal.type + "\t" + snmpVal.value; } - /** - * Reconstructs an SNMP table under rootOID. - * - * @param rootOID the entry OID prefix (e.g. …7.1) - * @param selectColumns array of column numbers as strings ("1","2",… or "ID") - * @return list of rows, each a List in selectColumns order - */ + @Override public List> table(String rootOID, String[] selectColumns) { if (rootOID == null || rootOID.length() < 3) { throw new IllegalArgumentException("Invalid SNMP Table OID: " + rootOID); @@ -112,7 +110,7 @@ public List> table(String rootOID, String[] selectColumns) { final String base = stripDot(rootOID); // find first column under base - Map.Entry firstEntry = oidValues.higherEntry(base); + Map.Entry firstEntry = oidValues.higherEntry(base); if (firstEntry == null || !firstEntry.getKey().startsWith(base + ".")) { return new ArrayList<>(); // empty table } @@ -128,7 +126,7 @@ public List> table(String rootOID, String[] selectColumns) { final List ids = new ArrayList<>(); String cursor = firstColOid; while (true) { - Map.Entry nxt = oidValues.higherEntry(cursor); + Map.Entry nxt = oidValues.higherEntry(cursor); if (nxt == null) break; final String key = nxt.getKey(); if (!key.startsWith(firstColOid + ".")) { @@ -165,6 +163,32 @@ public List> table(String rootOID, String[] selectColumns) { @Override public String walk(String oid) { - return ""; + final String base = stripDot(oid); + final StringBuilder walkResult = new StringBuilder(); + + // Find the first OID that is >= base + Map.Entry entry = oidValues.ceilingEntry(base); + if (entry == null) { + return ""; // Nothing at or after base + } + + while (entry != null) { + final String nextOid = entry.getKey(); + if (!nextOid.startsWith(base + ".") && !nextOid.equals(base)) { + break; // left the subtree + } + + final SnmpValue snmpValue = entry.getValue(); + walkResult.append(nextOid) + .append("\t") + .append(snmpValue.type) + .append("\t") + .append(snmpValue.value) + .append("\n"); + + entry = oidValues.higherEntry(nextOid); + } + + return walkResult.toString(); } } \ No newline at end of file diff --git a/src/test/java/org/metricshub/snmp/client/OfflineSnmpFileClientTest.java b/src/test/java/org/metricshub/snmp/client/OfflineSnmpFileClientTest.java new file mode 100644 index 0000000..f8ddd7c --- /dev/null +++ b/src/test/java/org/metricshub/snmp/client/OfflineSnmpFileClientTest.java @@ -0,0 +1,105 @@ +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * SNMP Java Client + * ჻჻჻჻჻჻ + * Copyright 2023 MetricsHub + * ჻჻჻჻჻჻ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +package org.metricshub.snmp.client; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class OfflineSnmpFileClientTest { + + private OfflineSnmpFileClient client; + + @BeforeEach + void setUp() throws IOException { + // Create a temporary snmpWalk dump file + final Path tempFile = Files.createTempFile("snmpWalk", ".txt"); + final String dump = + "1.3.6.1.2.1.2.2.1.1.1\tASN_INTEGER\t1\n" + + "1.3.6.1.2.1.2.2.1.1.6\tASN_INTEGER\t6\n" + + "1.3.6.1.2.1.2.2.1.2.1\tASN_OCTET_STR\tlo\n" + + "1.3.6.1.2.1.2.2.1.2.6\tASN_OCTET_STR\tbond0\n"; + Files.write(tempFile, dump.getBytes(StandardCharsets.UTF_8)); + + + client = new OfflineSnmpFileClient(tempFile); + + // cleanup on exit + tempFile.toFile().deleteOnExit(); + } + + @Test + void testGetExistingOid() throws Exception { + final String value = client.get("1.3.6.1.2.1.2.2.1.2.1"); + assertEquals("lo", value); + } + + @Test + void testGetNonExistingOid() { + final Exception exception = assertThrows(Exception.class, + () -> client.get("1.3.6.1.999.1")); + assertEquals("(no-such-oid)", exception.getMessage()); + } + + @Test + void testGetNext() { + final String next = client.getNext("1.3.6.1.2.1.2.2.1.1.1"); + assertTrue(next.startsWith("1.3.6.1.2.1.2.2.1.1.6")); + assertTrue(next.contains("ASN_INTEGER")); + assertTrue(next.endsWith("6")); + } + + @Test + void testWalk() { + final String walk = client.walk("1.3.6.1.2.1.2.2.1.1"); + assertTrue(walk.contains("1.3.6.1.2.1.2.2.1.1.1\tASN_INTEGER\t1")); + assertTrue(walk.contains("1.3.6.1.2.1.2.2.1.1.6\tASN_INTEGER\t6")); + assertFalse(walk.contains("bond0")); // bond0 is under .2, not .1 + } + + @Test + void testTable() { + // The OIDs have structure like ...2.2.1.. + final List> rows = client.table("1.3.6.1.2.1.2.2.1", new String[]{"1", "2", "ID"}); + assertEquals(2, rows.size()); + + // First row: ID=1 + final List row1 = rows.get(0); + assertEquals("1", row1.get(2)); // ID + assertEquals("1", row1.get(0)); // column 1 value + assertEquals("lo", row1.get(1)); // column 2 value + + // Second row: ID=6 + final List row2 = rows.get(1); + assertEquals("6", row2.get(2)); // ID + assertEquals("6", row2.get(0)); + assertEquals("bond0", row2.get(1)); + } +} From e2c61778dc4a26261700158ae02e462c5214623e Mon Sep 17 00:00:00 2001 From: maalej Date: Thu, 4 Sep 2025 10:34:37 +0200 Subject: [PATCH 3/4] Issue #60 Create offline SNMP client * Refactor code and add missing final keyword --- .../snmp/client/OfflineSnmpFileClient.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java b/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java index 6b80faf..b3e7220 100644 --- a/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java +++ b/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java @@ -89,12 +89,12 @@ public String get(String oid) throws Exception { @Override public String getNext(String oid) { - Map.Entry next = oidValues.higherEntry(stripDot(oid)); + final Map.Entry next = oidValues.higherEntry(stripDot(oid)); if (next == null) { return "(end-of-mib-view)"; } - String nextOid = next.getKey(); - SnmpValue snmpVal = next.getValue(); + final String nextOid = next.getKey(); + final SnmpValue snmpVal = next.getValue(); return nextOid + "\t" + snmpVal.type + "\t" + snmpVal.value; } @@ -110,7 +110,7 @@ public List> table(String rootOID, String[] selectColumns) { final String base = stripDot(rootOID); // find first column under base - Map.Entry firstEntry = oidValues.higherEntry(base); + final Map.Entry firstEntry = oidValues.higherEntry(base); if (firstEntry == null || !firstEntry.getKey().startsWith(base + ".")) { return new ArrayList<>(); // empty table } @@ -120,15 +120,15 @@ public List> table(String rootOID, String[] selectColumns) { if (dotAfter < 0) return new ArrayList<>(); final String firstColOid = firstKey.substring(0, dotAfter); // e.g. "…7.1" - int colOidLen = firstColOid.length(); + final int colOidLen = firstColOid.length(); // collect row IDs final List ids = new ArrayList<>(); String cursor = firstColOid; while (true) { - Map.Entry nxt = oidValues.higherEntry(cursor); - if (nxt == null) break; - final String key = nxt.getKey(); + final Map.Entry next = oidValues.higherEntry(cursor); + if (next == null) break; + final String key = next.getKey(); if (!key.startsWith(firstColOid + ".")) { break; } @@ -143,7 +143,7 @@ public List> table(String rootOID, String[] selectColumns) { // build result rows final List> rows = new ArrayList<>(ids.size()); for (String id : ids) { - List row = new ArrayList<>(selectColumns.length); + final List row = new ArrayList<>(selectColumns.length); for (String col : selectColumns) { if ("ID".equals(col)) { row.add(id); From 0fb47162944b30c93419f26e997ddfdf6702c439 Mon Sep 17 00:00:00 2001 From: maalej Date: Fri, 5 Sep 2025 15:33:39 +0200 Subject: [PATCH 4/4] Issue #60 Create offline snmp client * Rename the client * Iterate on the walk file directory * Update the tests --- .../metricshub/snmp/client/ISnmpClient.java | 2 +- ...FileClient.java => OfflineSnmpClient.java} | 50 +++-- .../snmp/client/OfflineSnmpClientTest.java | 207 ++++++++++++++++++ .../client/OfflineSnmpFileClientTest.java | 105 --------- 4 files changed, 237 insertions(+), 127 deletions(-) rename src/main/java/org/metricshub/snmp/client/{OfflineSnmpFileClient.java => OfflineSnmpClient.java} (79%) create mode 100644 src/test/java/org/metricshub/snmp/client/OfflineSnmpClientTest.java delete mode 100644 src/test/java/org/metricshub/snmp/client/OfflineSnmpFileClientTest.java diff --git a/src/main/java/org/metricshub/snmp/client/ISnmpClient.java b/src/main/java/org/metricshub/snmp/client/ISnmpClient.java index 21aa98c..9ff1463 100644 --- a/src/main/java/org/metricshub/snmp/client/ISnmpClient.java +++ b/src/main/java/org/metricshub/snmp/client/ISnmpClient.java @@ -26,7 +26,7 @@ /** * Unified abstraction that both {@link SnmpClient} (live network) and - * {@link OfflineSnmpFileClient} (file‑backed) can implement. + * {@link OfflineSnmpClient} (file‑backed) can implement. */ public interface ISnmpClient { diff --git a/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java b/src/main/java/org/metricshub/snmp/client/OfflineSnmpClient.java similarity index 79% rename from src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java rename to src/main/java/org/metricshub/snmp/client/OfflineSnmpClient.java index b3e7220..24d124b 100644 --- a/src/main/java/org/metricshub/snmp/client/OfflineSnmpFileClient.java +++ b/src/main/java/org/metricshub/snmp/client/OfflineSnmpClient.java @@ -24,6 +24,7 @@ import java.io.BufferedReader; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -32,12 +33,13 @@ import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; +import java.util.stream.Stream; /** * An offline SNMP client that reads OID values from a snmp WALK dump file and executes * SNMP operations (GET, GET_NEXT, TABLE, WALK) against those OIDs. */ -public final class OfflineSnmpFileClient implements ISnmpClient { +public final class OfflineSnmpClient implements ISnmpClient { /** Container for Snmp object's type and value. */ private static final class SnmpValue { @@ -52,28 +54,34 @@ private static final class SnmpValue { // Maps the OIDs to their corresponding SnmpValue (type + value). private final NavigableMap oidValues = new TreeMap<>(); + /** - * Constructs an SNMP client that reads OID values from a file. - * - * @param file the path to the file containing OID values in UTF-16 encoding. - * @throws IOException if an I/O error occurs while reading the file. + * Reads oid values from all .walk files in the specified directory. + * @param directory A given directory containing .walk files + * @throws IOException If an I/O error occurs reading from the directory or a file */ - public OfflineSnmpFileClient(Path file) throws IOException { - try (final BufferedReader br = - Files.newBufferedReader(file, StandardCharsets.UTF_8)) { - String line; - while ((line = br.readLine()) != null) { - line = line.trim(); - if (!line.contains("\t")) continue; - final String[] parts = line.split("\\t", 3); - if (parts.length < 3) { - continue; - } - final String oid = stripDot(parts[0]); - final String type = parts[1]; - final String value = parts[2]; - oidValues.put(oid, new SnmpValue(type, value)); - } + public OfflineSnmpClient(Path directory) throws IOException { + try (Stream paths = Files.list(directory)) { + paths.filter(path -> path.toString().endsWith(".walk")) + .forEach(file -> { + try (BufferedReader br = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (!line.contains("\t")) continue; + + final String[] parts = line.split("\\t", 3); + if (parts.length < 3) continue; + + final String oid = stripDot(parts[0]); + final String type = parts[1]; + final String value = parts[2]; + oidValues.put(oid, new SnmpValue(type, value)); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } } diff --git a/src/test/java/org/metricshub/snmp/client/OfflineSnmpClientTest.java b/src/test/java/org/metricshub/snmp/client/OfflineSnmpClientTest.java new file mode 100644 index 0000000..ba3517a --- /dev/null +++ b/src/test/java/org/metricshub/snmp/client/OfflineSnmpClientTest.java @@ -0,0 +1,207 @@ +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * SNMP Java Client + * ჻჻჻჻჻჻ + * Copyright 2023 MetricsHub + * ჻჻჻჻჻჻ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +package org.metricshub.snmp.client; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class OfflineSnmpClientTest { + + private OfflineSnmpClient client; + + @BeforeEach + void setUp() throws IOException { + final Path tempDir = Files.createTempDirectory("snmpWalkDir"); + + // -------- File 1: out.walk (original structure) -------- + final Path file1 = tempDir.resolve("out.walk"); + final String dump1 = + "1.3.6.1.2.1.2.2.1.1.1\tASN_INTEGER\t1\n" + + "1.3.6.1.2.1.2.2.1.1.6\tASN_INTEGER\t6\n" + + "1.3.6.1.2.1.2.2.1.2.1\tASN_OCTET_STR\tlo\n" + + "1.3.6.1.2.1.2.2.1.2.6\tASN_OCTET_STR\tbond0\n"; + Files.write(file1, dump1.getBytes(StandardCharsets.UTF_8)); + + // -------- File 2: extra.walk (table layout) -------- + final Path file2 = tempDir.resolve("extra.walk"); + final String dump2 = + // row 1 + "1.3.6.1.4.1.1.1.1.1\tASN_OCTET_STR\tmyDevice\n" + + "1.3.6.1.4.1.1.1.2.1\tASN_INTEGER\t7\n" + + // row 2 + "1.3.6.1.4.1.1.1.1.2\tASN_OCTET_STR\teth0\n" + + "1.3.6.1.4.1.1.1.2.2\tASN_INTEGER\t99\n"; + Files.write(file2, dump2.getBytes(StandardCharsets.UTF_8)); + + // -------- File 3: another.walk (table layout) -------- + final Path file3 = tempDir.resolve("another.walk"); + final String dump3 = + // row 1 + "1.3.6.1.4.1.1.2.1.1\tASN_INTEGER\t42\n" + + "1.3.6.1.4.1.1.2.2.1\tASN_OCTET_STR\tanswer\n" + + // row 2 + "1.3.6.1.4.1.1.2.1.2\tASN_INTEGER\t123\n" + + "1.3.6.1.4.1.1.2.2.2\tASN_OCTET_STR\tsecond\n"; + Files.write(file3, dump3.getBytes(StandardCharsets.UTF_8)); + + client = new OfflineSnmpClient(tempDir); + + file1.toFile().deleteOnExit(); + file2.toFile().deleteOnExit(); + file3.toFile().deleteOnExit(); + tempDir.toFile().deleteOnExit(); + } + + // --- Tests for File 1 (out.walk) --- + @Test + void testGetExistingOid() throws Exception { + assertEquals("lo", client.get("1.3.6.1.2.1.2.2.1.2.1")); + } + + @Test + void testGetNonExistingOid() { + final Exception exception = assertThrows(Exception.class, + () -> client.get("1.3.6.1.999.1")); + assertEquals("(no-such-oid)", exception.getMessage()); + } + + @Test + void testGetNextFromFirstFile() { + final String next = client.getNext("1.3.6.1.2.1.2.2.1.1.1"); + assertTrue(next.startsWith("1.3.6.1.2.1.2.2.1.1.6")); + assertTrue(next.contains("ASN_INTEGER")); + assertTrue(next.endsWith("6")); + } + + @Test + void testWalkFromFirstFile() { + final String walk = client.walk("1.3.6.1.2.1.2.2.1.1"); + assertTrue(walk.contains("1.3.6.1.2.1.2.2.1.1.1\tASN_INTEGER\t1")); + assertTrue(walk.contains("1.3.6.1.2.1.2.2.1.1.6\tASN_INTEGER\t6")); + assertFalse(walk.contains("bond0")); // bond0 is under .2, not .1 + } + + @Test + void testTableFromFirstFile() { + final List> rows = client.table("1.3.6.1.2.1.2.2.1", new String[]{"1", "2", "ID"}); + assertEquals(2, rows.size()); + + final List row1 = rows.get(0); + assertEquals("1", row1.get(2)); + assertEquals("1", row1.get(0)); + assertEquals("lo", row1.get(1)); + + final List row2 = rows.get(1); + assertEquals("6", row2.get(2)); + assertEquals("6", row2.get(0)); + assertEquals("bond0", row2.get(1)); + } + + // --- Tests for File 2 (extra.walk) --- + @Test + void testGetFromSecondFile() throws Exception { + assertEquals("myDevice", client.get("1.3.6.1.4.1.1.1.1.1")); + assertEquals("7", client.get("1.3.6.1.4.1.1.1.2.1")); + assertEquals("eth0", client.get("1.3.6.1.4.1.1.1.1.2")); + assertEquals("99", client.get("1.3.6.1.4.1.1.1.2.2")); + } + + @Test + void testGetNextFromSecondFile() { + final String next = client.getNext("1.3.6.1.4.1.1.1.1.1"); // myDevice + assertTrue(next.endsWith("eth0")); // because lex order goes to 1.1.2 + } + + @Test + void testWalkFromSecondFile() { + final String walk = client.walk("1.3.6.1.4.1.1.1"); + assertTrue(walk.contains("1.3.6.1.4.1.1.1.1.1\tASN_OCTET_STR\tmyDevice")); + assertTrue(walk.contains("1.3.6.1.4.1.1.1.2.1\tASN_INTEGER\t7")); + assertTrue(walk.contains("1.3.6.1.4.1.1.1.1.2\tASN_OCTET_STR\teth0")); + assertTrue(walk.contains("1.3.6.1.4.1.1.1.2.2\tASN_INTEGER\t99")); + } + + @Test + void testTableFromSecondFile() { + final List> rows = client.table("1.3.6.1.4.1.1.1", new String[]{"1", "2", "ID"}); + assertEquals(2, rows.size()); + + final List row1 = rows.get(0); + assertEquals("myDevice", row1.get(0)); + assertEquals("7", row1.get(1)); + assertEquals("1", row1.get(2)); + + final List row2 = rows.get(1); + assertEquals("eth0", row2.get(0)); + assertEquals("99", row2.get(1)); + assertEquals("2", row2.get(2)); + } + + // --- Tests for File 3 (another.walk) --- + @Test + void testGetFromThirdFile() throws Exception { + assertEquals("42", client.get("1.3.6.1.4.1.1.2.1.1")); + assertEquals("answer", client.get("1.3.6.1.4.1.1.2.2.1")); + assertEquals("123", client.get("1.3.6.1.4.1.1.2.1.2")); + assertEquals("second", client.get("1.3.6.1.4.1.1.2.2.2")); + } + + @Test + void testWalkFromThirdFile() { + final String walk = client.walk("1.3.6.1.4.1.1.2"); + assertTrue(walk.contains("1.3.6.1.4.1.1.2.1.1\tASN_INTEGER\t42")); + assertTrue(walk.contains("1.3.6.1.4.1.1.2.2.1\tASN_OCTET_STR\tanswer")); + assertTrue(walk.contains("1.3.6.1.4.1.1.2.1.2\tASN_INTEGER\t123")); + assertTrue(walk.contains("1.3.6.1.4.1.1.2.2.2\tASN_OCTET_STR\tsecond")); + } + + @Test + void testGetNextFromThirdFile() { + final String next = client.getNext("1.3.6.1.4.1.1.2.1.1"); // 42 + assertTrue(next.endsWith("123")); // because lex order goes to 1.2 + } + + @Test + void testTableFromThirdFile() { + final List> rows = client.table("1.3.6.1.4.1.1.2", new String[]{"1", "2", "ID"}); + assertEquals(2, rows.size()); + + final List row1 = rows.get(0); + assertEquals("42", row1.get(0)); + assertEquals("answer", row1.get(1)); + assertEquals("1", row1.get(2)); + + final List row2 = rows.get(1); + assertEquals("123", row2.get(0)); + assertEquals("second", row2.get(1)); + assertEquals("2", row2.get(2)); + } +} diff --git a/src/test/java/org/metricshub/snmp/client/OfflineSnmpFileClientTest.java b/src/test/java/org/metricshub/snmp/client/OfflineSnmpFileClientTest.java deleted file mode 100644 index f8ddd7c..0000000 --- a/src/test/java/org/metricshub/snmp/client/OfflineSnmpFileClientTest.java +++ /dev/null @@ -1,105 +0,0 @@ -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * SNMP Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 MetricsHub - * ჻჻჻჻჻჻ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Lesser Public License for more details. - * - * You should have received a copy of the GNU General Lesser Public - * License along with this program. If not, see - * . - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -package org.metricshub.snmp.client; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -class OfflineSnmpFileClientTest { - - private OfflineSnmpFileClient client; - - @BeforeEach - void setUp() throws IOException { - // Create a temporary snmpWalk dump file - final Path tempFile = Files.createTempFile("snmpWalk", ".txt"); - final String dump = - "1.3.6.1.2.1.2.2.1.1.1\tASN_INTEGER\t1\n" + - "1.3.6.1.2.1.2.2.1.1.6\tASN_INTEGER\t6\n" + - "1.3.6.1.2.1.2.2.1.2.1\tASN_OCTET_STR\tlo\n" + - "1.3.6.1.2.1.2.2.1.2.6\tASN_OCTET_STR\tbond0\n"; - Files.write(tempFile, dump.getBytes(StandardCharsets.UTF_8)); - - - client = new OfflineSnmpFileClient(tempFile); - - // cleanup on exit - tempFile.toFile().deleteOnExit(); - } - - @Test - void testGetExistingOid() throws Exception { - final String value = client.get("1.3.6.1.2.1.2.2.1.2.1"); - assertEquals("lo", value); - } - - @Test - void testGetNonExistingOid() { - final Exception exception = assertThrows(Exception.class, - () -> client.get("1.3.6.1.999.1")); - assertEquals("(no-such-oid)", exception.getMessage()); - } - - @Test - void testGetNext() { - final String next = client.getNext("1.3.6.1.2.1.2.2.1.1.1"); - assertTrue(next.startsWith("1.3.6.1.2.1.2.2.1.1.6")); - assertTrue(next.contains("ASN_INTEGER")); - assertTrue(next.endsWith("6")); - } - - @Test - void testWalk() { - final String walk = client.walk("1.3.6.1.2.1.2.2.1.1"); - assertTrue(walk.contains("1.3.6.1.2.1.2.2.1.1.1\tASN_INTEGER\t1")); - assertTrue(walk.contains("1.3.6.1.2.1.2.2.1.1.6\tASN_INTEGER\t6")); - assertFalse(walk.contains("bond0")); // bond0 is under .2, not .1 - } - - @Test - void testTable() { - // The OIDs have structure like ...2.2.1.. - final List> rows = client.table("1.3.6.1.2.1.2.2.1", new String[]{"1", "2", "ID"}); - assertEquals(2, rows.size()); - - // First row: ID=1 - final List row1 = rows.get(0); - assertEquals("1", row1.get(2)); // ID - assertEquals("1", row1.get(0)); // column 1 value - assertEquals("lo", row1.get(1)); // column 2 value - - // Second row: ID=6 - final List row2 = rows.get(1); - assertEquals("6", row2.get(2)); // ID - assertEquals("6", row2.get(0)); - assertEquals("bond0", row2.get(1)); - } -}