Skip to content

Commit

Permalink
Issue #424: Add CLI options for specific query execution
Browse files Browse the repository at this point in the history
* Added IQuery interface to handle protocols queries.
* Developed SnmpCli executable which enables to execute SNMP get, getNext & walk commands through CLI.
* Tested the generated SNMP.exe with different commands.
  • Loading branch information
CherfaElyes committed Dec 2, 2024
1 parent 4a3c9aa commit c6440e5
Show file tree
Hide file tree
Showing 25 changed files with 635 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ public Integer call() throws Exception {
validate();

// Setup Log4j
setLogLevel();
setLogLevel(verbose);

final HostConfiguration hostConfiguration = HostConfiguration
.builder()
Expand Down Expand Up @@ -527,8 +527,10 @@ private void validate() {

/**
* Set Log4j logging level according to the verbose flags
*
* @param verbose array of boolean values specifying logger level.
*/
void setLogLevel() {
public static void setLogLevel(final boolean[] verbose) {
// Disable ANSI in the logging if we don't have a console
ThreadContext.put("disableAnsi", Boolean.toString(!ConsoleService.hasConsole()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
* ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
*/

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.util.Arrays;
import lombok.Data;
import org.sentrysoftware.metricshub.cli.service.CliExtensionManager;
import org.sentrysoftware.metricshub.engine.common.exception.InvalidConfigurationException;
Expand Down Expand Up @@ -79,6 +81,14 @@ public class SnmpConfigCli implements IProtocolConfigCli {
)
String timeout;

@Option(
names = { "--snmp-retry-intervals", "--retry" },
order = 2,
paramLabel = "RETRYINTERVALS",
description = "Timeout in milliseconds after which the elementary operations will be retried"
)
int[] retryIntervals;

/**
* This method creates an {@link IConfiguration} for a given username and a given password
*
Expand All @@ -97,6 +107,10 @@ public IConfiguration toProtocol(final String defaultUsername, final char[] defa
}
configuration.set("port", new IntNode(port));
configuration.set("timeout", new TextNode(timeout));
if (retryIntervals != null) {
final ArrayNode retryIntervalsArrayNode = configuration.putArray("retryIntervals");
Arrays.stream(retryIntervals).forEach(retryIntervalsArrayNode::add);
}

return CliExtensionManager
.getExtensionManagerSingleton()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package org.sentrysoftware.metricshub.cli.snmp;

/*-
* ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
* MetricsHub Agent
* ჻჻჻჻჻჻
* Copyright 2023 - 2024 Sentry Software
* ჻჻჻჻჻჻
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
*/

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;

import lombok.Data;

import java.io.PrintWriter;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.stream.Stream;
import org.fusesource.jansi.AnsiConsole;
import org.sentrysoftware.metricshub.cli.service.CliExtensionManager;
import org.sentrysoftware.metricshub.cli.service.PrintExceptionMessageHandlerService;
import org.sentrysoftware.metricshub.cli.service.protocol.SnmpConfigCli;
import org.sentrysoftware.metricshub.engine.common.IQuery;
import org.sentrysoftware.metricshub.engine.configuration.IConfiguration;
import picocli.CommandLine;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.Spec;

@Data
public class SnmpCli implements IQuery, Callable<Integer> {

@Parameters(index = "0", paramLabel = "HOSTNAME", description = "Hostname or IP address of the host to monitor")
String hostname;

@Spec
CommandSpec spec;

@Option(names = "--snmp-get", order = 1, paramLabel = "OID", description = "SNMP Get request")
String get;

@Option(names = "--snmp-getnext", order = 2, paramLabel = "OID", description = "SNMP Get Next request")
String getNext;

@Option(names = "--snmp-walk", order = 3, paramLabel = "OID", description = "SNMP Walk request")
String walk;

@ArgGroup(exclusive = false, heading = "%n@|bold,underline SNMP Options|@:%n")
SnmpConfigCli snmpConfigCli;

@Option(names = { "-h", "-?", "--help" }, usageHelp = true, description = "Shows this help message and exits")
boolean usageHelpRequested;

@Option(names = "-v", order = 7, description = "Verbose mode (repeat the option to increase verbosity)")
boolean[] verbose;

public JsonNode getQuery() {
final ObjectNode queryNode = JsonNodeFactory.instance.objectNode();
String action;
String oid;

if (get != null) {
action = "get";
oid = get;
} else if (getNext != null) {
action = "getNext";
oid = getNext;
} else {
action = "walk";
oid = walk;
}

queryNode.set("action", new TextNode(action));
queryNode.set("oid", new TextNode(oid));

return queryNode;
}

void validate() throws ParameterException {
if (snmpConfigCli == null) {
throw new ParameterException(spec.commandLine(), "SNMP protocol must be configured: --snmp.");
}

Stream.of(get, getNext, walk)
.filter(Objects::nonNull)
.reduce((a, b) -> {
throw new ParameterException(
spec.commandLine(),
"Only one SNMP query can be specified at a time: --snmp-get, --snmp-getnext, --snmp-walk."
);
})
.orElseThrow(() -> new ParameterException(
spec.commandLine(),
"At least one SNMP query must be specified: --snmp-get, --snmp-getnext, --snmp-walk."
));
}

public static void main(String[] args) {
System.setProperty("log4j2.configurationFile", "log4j2-cli.xml");

// Enable colors on Windows terminal
AnsiConsole.systemInstall();

final CommandLine cli = new CommandLine(new SnmpCli());

// Keep the below line commented for future reference
// Using JAnsi on Windows breaks the output of Unicode (UTF-8) chars
// It can be fixed using the below line... when running in Windows Terminal
// and not CMD.EXE.
// As this is poorly documented, we keep this for future improvement.
// cli.setOut(new PrintWriter(AnsiConsole.out(), true, StandardCharsets.UTF_8)); // NOSONAR on commented code

// Set the exception handler
cli.setExecutionExceptionHandler(new PrintExceptionMessageHandlerService());

// Allow case insensitive enum values
cli.setCaseInsensitiveEnumValuesAllowed(true);

// Execute the command
final int exitCode = cli.execute(args);

// Cleanup Windows terminal settings
AnsiConsole.systemUninstall();

System.exit(exitCode);
}

@Override
public Integer call() throws Exception {
final PrintWriter printWriter = spec.commandLine().getOut();
validate();
CliExtensionManager
.getExtensionManagerSingleton()
.findExtensionByType("snmp")
.ifPresent(extension -> {
try {
IConfiguration protocol = snmpConfigCli.toProtocol(null, null);
protocol.setHostname(hostname);
extension.executeQuery(protocol, getQuery(), printWriter);
} catch (Exception e) {
printWriter.print("Invalid configuration detected");
printWriter.flush();
throw new IllegalStateException("Invalid configuration detected.", e);
}
});
return CommandLine.ExitCode.OK;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,27 +59,20 @@ void testListAllConnectors() {

@Test
void testSetLogLevel() {
final MetricsHubCliService metricsHubCliService = new MetricsHubCliService();

assertDoesNotThrow(() -> {
metricsHubCliService.verbose = new boolean[] {};
metricsHubCliService.setLogLevel();
MetricsHubCliService.setLogLevel(new boolean[] {});
});
assertDoesNotThrow(() -> {
metricsHubCliService.verbose = new boolean[] { true };
metricsHubCliService.setLogLevel();
MetricsHubCliService.setLogLevel(new boolean[] { true });
});
assertDoesNotThrow(() -> {
metricsHubCliService.verbose = new boolean[] { true, true };
metricsHubCliService.setLogLevel();
MetricsHubCliService.setLogLevel(new boolean[] { true, true });
});
assertDoesNotThrow(() -> {
metricsHubCliService.verbose = new boolean[] { true, true, true };
metricsHubCliService.setLogLevel();
MetricsHubCliService.setLogLevel(new boolean[] { true, true, true });
});
assertDoesNotThrow(() -> {
metricsHubCliService.verbose = new boolean[] { true, true, true, true };
metricsHubCliService.setLogLevel();
MetricsHubCliService.setLogLevel(new boolean[] { true, true, true, true });
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package org.sentrysoftware.metricshub.cli.snmp;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;
import org.sentrysoftware.metricshub.cli.service.protocol.SnmpConfigCli;

import com.fasterxml.jackson.databind.JsonNode;
import picocli.CommandLine;
import picocli.CommandLine.ParameterException;

class SnmpCliTest {

SnmpCli snmpCli;
CommandLine commandLine;

static String SNMP_OID = "1.3.6.1.4.1.674.10892.5.5.1.20.130.4";
static String SNMP_VERSION = "v2c";
static String SNMP_COMMUNITY = "public";

void initCli() {
snmpCli = new SnmpCli();
commandLine = new CommandLine(snmpCli);
}

void initSnmpGet() {
initCli();

commandLine.execute(
"hostname",
"--snmp-get",
SNMP_OID,
"--snmp",
SNMP_VERSION,
"--community",
SNMP_COMMUNITY,
"--retry",
"1000",
"--retry",
"6000"
);
}

void initSnmpGetNext() {
initCli();

commandLine.execute(
"hostname",
"--snmp-getnext",
SNMP_OID,
"--snmp",
SNMP_VERSION,
"--community",
SNMP_COMMUNITY,
"--retry",
"1000",
"--retry",
"6000"
);
}

void initSnmpWalk() {
initCli();

commandLine.execute(
"hostname",
"--snmp-walk",
SNMP_OID,
"--snmp",
SNMP_VERSION,
"--community",
SNMP_COMMUNITY,
"--retry",
"1000",
"--retry",
"6000"
);
}
@Test
void testExecute() {
initSnmpGet();
assertEquals(SNMP_OID, snmpCli.get);
initSnmpGetNext();
assertEquals(SNMP_OID, snmpCli.getNext);
initSnmpWalk();
assertEquals(SNMP_OID, snmpCli.walk);
}

@Test
void testGetQuery() {
initSnmpGet();
JsonNode snmpQuery = snmpCli.getQuery();
assertEquals("get", snmpQuery.get("action").asText());
assertEquals(SNMP_OID, snmpQuery.get("oid").asText());

initSnmpGetNext();
snmpQuery = snmpCli.getQuery();
assertEquals("getNext", snmpQuery.get("action").asText());
assertEquals(SNMP_OID, snmpQuery.get("oid").asText());

initSnmpWalk();
snmpQuery = snmpCli.getQuery();
assertEquals("walk", snmpQuery.get("action").asText());
assertEquals(SNMP_OID, snmpQuery.get("oid").asText());
}

@Test
void testValidate() {
initCli();
ParameterException snmpConfigException = assertThrows(ParameterException.class, () -> snmpCli.validate());
assertEquals("SNMP protocol must be configured: --snmp.", snmpConfigException.getMessage());
snmpCli.setSnmpConfigCli(new SnmpConfigCli());
ParameterException noQueriesException = assertThrows(ParameterException.class, () -> snmpCli.validate());
assertEquals("At least one SNMP query must be specified: --snmp-get, --snmp-getnext, --snmp-walk.", noQueriesException.getMessage());
snmpCli.setGet(SNMP_OID);
assertDoesNotThrow(() -> snmpCli.validate());
snmpCli.setGetNext(SNMP_OID);
ParameterException manyQueriesException = assertThrows(ParameterException.class, () -> snmpCli.validate());
assertEquals("Only one SNMP query can be specified at a time: --snmp-get, --snmp-getnext, --snmp-walk.", manyQueriesException.getMessage());
}
}
2 changes: 1 addition & 1 deletion metricshub-doc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
<plugin>
<groupId>org.sentrysoftware.maven</groupId>
<artifactId>metricshub-connector-maven-plugin</artifactId>
<version>1.0.05</version>
<version>1.0.07-SNAPSHOT</version>
<configuration>
<sourceDirectory>${project.basedir}/../metricshub-agent/target/connectors</sourceDirectory>
</configuration>
Expand Down
Loading

0 comments on commit c6440e5

Please sign in to comment.