org.sentrysoftware.metricshub.extension.wbem.WbemCriterionProcessor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of metricshub-wbem-extension Show documentation
Show all versions of metricshub-wbem-extension Show documentation
MetricsHub WBEM Extension implementation providing a set of functionalities used to process WBEM criterion and source.
package org.sentrysoftware.metricshub.extension.wbem;
/*-
* ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
* MetricsHub Wbem Extension
* ჻჻჻჻჻჻
* 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 .
* ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
*/
import static org.sentrysoftware.metricshub.engine.common.helpers.MetricsHubConstants.AUTOMATIC_NAMESPACE;
import static org.sentrysoftware.metricshub.engine.common.helpers.MetricsHubConstants.TABLE_SEP;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.sentrysoftware.metricshub.engine.common.exception.ClientException;
import org.sentrysoftware.metricshub.engine.connector.model.identity.criterion.WbemCriterion;
import org.sentrysoftware.metricshub.engine.connector.model.identity.criterion.WqlCriterion;
import org.sentrysoftware.metricshub.engine.strategy.detection.CriterionTestResult;
import org.sentrysoftware.metricshub.engine.strategy.source.SourceTable;
import org.sentrysoftware.metricshub.engine.strategy.utils.PslUtils;
import org.sentrysoftware.metricshub.engine.telemetry.TelemetryManager;
@Slf4j
@RequiredArgsConstructor
public class WbemCriterionProcessor {
private static final String SELECT_NAME_FROM_CIM_NAMESPACE = "SELECT Name from CIM_Namespace";
private static final Set IGNORED_WBEM_NAMESPACES = Set.of("root", "/root");
private static final String INTEROP_LOWER_CASE = "interop";
private static final String ROOT_SLASH = "root/";
static final List WBEM_INTEROP_QUERIES = List.of(
new WqlQuery("SELECT Name FROM __NAMESPACE", "root"),
new WqlQuery(SELECT_NAME_FROM_CIM_NAMESPACE, "Interop"),
new WqlQuery(SELECT_NAME_FROM_CIM_NAMESPACE, "PG_Interop"),
new WqlQuery(SELECT_NAME_FROM_CIM_NAMESPACE, "root/Interop"),
new WqlQuery(SELECT_NAME_FROM_CIM_NAMESPACE, "root/PG_Interop"),
new WqlQuery(SELECT_NAME_FROM_CIM_NAMESPACE, INTEROP_LOWER_CASE)
);
@NonNull
private WbemRequestExecutor wbemRequestExecutor;
@NonNull
private String connectorId;
/**
* Find the namespace to use for the execution of the given {@link WbemCriterion}.
*
* @param hostname The hostname of the host device
* @param wbemConfiguration The WBEM protocol configuration (port, credentials, etc.)
* @param wbemCriterion The WQL criterion with an "Automatic" namespace
* @return A {@link CriterionTestResult} telling whether we found the proper namespace for the specified WQL
*/
private CriterionTestResult findNamespace(
final String hostname,
final WbemConfiguration wbemConfiguration,
final WbemCriterion wbemCriterion,
final TelemetryManager telemetryManager,
final String connectorId
) {
// Get the list of possible namespaces on this host
Set possibleWbemNamespaces = telemetryManager.getHostProperties().getPossibleWbemNamespaces();
// Only one thread at a time must be figuring out the possible namespaces on a given host
synchronized (possibleWbemNamespaces) {
if (possibleWbemNamespaces.isEmpty()) {
// If we don't have this list already, figure it out now
final PossibleNamespacesResult possibleWbemNamespacesResult = findPossibleNamespaces(
hostname,
wbemConfiguration,
telemetryManager
);
// If we can't detect the namespace then we must stop
if (!possibleWbemNamespacesResult.isSuccess()) {
return CriterionTestResult.error(wbemCriterion, possibleWbemNamespacesResult.getErrorMessage());
}
// Store the list of possible namespaces in HostMonitoring, for next time we need it
possibleWbemNamespaces.clear();
possibleWbemNamespaces.addAll(possibleWbemNamespacesResult.getPossibleNamespaces());
}
}
// Perform a namespace detection
NamespaceResult namespaceResult = detectNamespace(
hostname,
wbemConfiguration,
wbemCriterion,
Collections.unmodifiableSet(possibleWbemNamespaces),
telemetryManager
);
// If that was successful, remember it in HostMonitoring, so we don't perform this
// (costly) detection again
if (namespaceResult.getResult().isSuccess()) {
telemetryManager
.getHostProperties()
.getConnectorNamespace(connectorId)
.setAutomaticWbemNamespace(namespaceResult.getNamespace());
}
return namespaceResult.getResult();
}
/**
* Detect the WBEM namespace applicable to the specified WBEM criterion.
*
* The namespace in the criterion must be "Automatic".
*
*
* @param hostname The host name
* @param configuration WBEM configuration (credentials, timeout)
* @param criterion WQL detection properties (WQL, expected result, namespace must be "Automatic")
* @param possibleNamespaces The possible namespaces to execute the WQL on
* @return A {@link NamespaceResult} wrapping the detected namespace
* and the error message if the detection fails.
*/
private NamespaceResult detectNamespace(
final String hostname,
final WbemConfiguration configuration,
final WqlCriterion criterion,
final Set possibleNamespaces,
final TelemetryManager telemetryManager
) {
// Run the query on each namespace and check if the result match the criterion
final Map namespaces = new TreeMap<>();
final WqlCriterion tentativeCriterion = criterion.copy();
// Loop over each namespace and run the WBEM query and check if the result matches
for (final String namespace : possibleNamespaces) {
// Update the criterion with the current namespace that needs to be tested
tentativeCriterion.setNamespace(namespace);
// Do the request
CriterionTestResult testResult = performDetectionTest(
hostname,
configuration,
tentativeCriterion,
telemetryManager
);
// If the result matched then the namespace is selected
if (testResult.isSuccess()) {
namespaces.put(namespace, testResult);
} else {
// If the test failed with an exception, we probably don't need to go further
Throwable e = testResult.getException();
if (e != null && !wbemRequestExecutor.isAcceptableException(e)) {
// This error indicates that the CIM server will probably never respond to anything
// (timeout, or bad credentials), so there's no point in pursuing our efforts here.
log.debug(
"Hostname {} - Does not respond to {} requests. {}: {}\nCancelling namespace detection.",
hostname,
criterion.getClass().getSimpleName(),
e.getClass().getSimpleName(),
e.getMessage()
);
return NamespaceResult.builder().result(testResult).build();
}
}
}
// No namespace => failure
if (namespaces.isEmpty()) {
String formattedNamespaceList = possibleNamespaces.stream().collect(Collectors.joining("\n- "));
return NamespaceResult
.builder()
.result(
CriterionTestResult.failure(
criterion,
"None of the possible namespaces match the criterion:" + formattedNamespaceList
)
)
.build();
}
// So, now we have a list of working namespaces.
// We'd better have only one, but you never know, so try to be clever here...
// If we have several matching namespaces, including root/cimv2, then exclude this one
// because it's one that we find in many places and not necessarily with anything useful in it
// especially if there are other matching namespaces.
if (namespaces.size() > 1) {
namespaces.remove("root/cimv2");
namespaces.remove("root\\cimv2");
}
// Okay, so even if we have several, select a single one
final String detectedNamespace = namespaces.keySet().stream().findFirst().orElseThrow();
return NamespaceResult.builder().namespace(detectedNamespace).result(namespaces.get(detectedNamespace)).build();
}
/**
* Find the possible WBEM namespaces using the configured {@link WbemConfiguration}.
*
* @param hostname The hostname of the host device.
* @param configuration The user's configured {@link WbemConfiguration}.
* @return A {@link PossibleNamespacesResult} wrapping the success state, the message in case of errors
* and the possibleWbemNamespaces {@link Set}.
*/
private PossibleNamespacesResult findPossibleNamespaces(
final String hostname,
final WbemConfiguration configuration,
final TelemetryManager telemetryManager
) {
// If the user specified a namespace, we return it as if it was the only namespace available
// and for which we're going to test our connector
if (configuration.getNamespace() != null && !configuration.getNamespace().isBlank()) {
return PossibleNamespacesResult
.builder()
.possibleNamespaces(Collections.singleton(configuration.getNamespace()))
.success(true)
.build();
}
// Possible namespace will be stored in this set
Set possibleWbemNamespaces = new TreeSet<>();
// Try all "interop" queries that could retrieve a list of namespaces in this CIM server
for (WqlQuery interopQuery : WBEM_INTEROP_QUERIES) {
try {
wbemRequestExecutor
.executeWbem(hostname, configuration, interopQuery.getWql(), interopQuery.getNamespace(), telemetryManager)
.stream()
.filter(row -> !row.isEmpty())
.map(row -> row.get(0))
.filter(Objects::nonNull)
.filter(namespace -> !namespace.isBlank())
.filter(namespace -> !namespace.toLowerCase().contains(INTEROP_LOWER_CASE))
.filter(namespace -> !IGNORED_WBEM_NAMESPACES.contains(namespace))
.map(namespace -> ROOT_SLASH + namespace)
.forEach(possibleWbemNamespaces::add);
} catch (final ClientException e) {
// If the CIM server doesn't know the requested class, we will get a WBEM exception
// saying so. Such exceptions are okay and will not fail the detection.
// That's why we return in failure if and only if the error type is neither "invalid namespace",
// That's why we return in failure if and only if the error type is neither "invalid namespace",
// nor "invalid class", nor "not found".
if (!wbemRequestExecutor.isAcceptableException(e)) {
// This error indicates that the CIM server will probably never respond to anything
// (timeout, or bad credentials), so there's no point in pursuing our efforts here.
Throwable cause = e.getCause();
final String messageFormat =
"Hostname %s - Does not respond to WBEM requests. %s: %s\nCancelling namespace detection.";
String message = String.format(
messageFormat,
hostname,
cause != null ? cause.getClass().getSimpleName() : e.getClass().getSimpleName(),
cause != null ? cause.getMessage() : e.getMessage()
);
log.debug(message);
return PossibleNamespacesResult.builder().errorMessage(message).success(false).build();
}
}
}
// No namespace love?
if (possibleWbemNamespaces.isEmpty()) {
return PossibleNamespacesResult
.builder()
.errorMessage("No suitable namespace could be found to query host " + hostname + ".")
.success(false)
.build();
}
// Yay!
return PossibleNamespacesResult.builder().possibleNamespaces(possibleWbemNamespaces).success(true).build();
}
/**
* Perform the specified WQL detection test, on the specified WBEM protocol configuration.
*
* Note: "Automatic" namespace is not supported in this method.
*
*
* @param hostname Host name
* @param configuration WBEM configuration (credentials, timeout)
* @param criterion WQL detection properties (WQL, namespace, expected result)
* @return {@link CriterionTestResult} which indicates if the check has succeeded or not.
*/
private CriterionTestResult performDetectionTest(
final String hostname,
@NonNull final WbemConfiguration configuration,
@NonNull final WqlCriterion criterion,
@NonNull final TelemetryManager telemetryManager
) {
// Make the WBEM query
final List> queryResult;
try {
queryResult =
wbemRequestExecutor.executeWbem(
hostname,
configuration,
criterion.getQuery(),
criterion.getNamespace(),
telemetryManager
);
} catch (ClientException e) {
return CriterionTestResult.error(criterion, e);
}
// Serialize the result as a CSV
String actualResult = SourceTable.tableToCsv(queryResult, TABLE_SEP, true);
// Empty result? ==> failure
if (actualResult == null || actualResult.isBlank()) {
return CriterionTestResult.failure(criterion, "No result.");
}
// No expected result (and non-empty result)? ==> success
if (criterion.getExpectedResult() == null || criterion.getExpectedResult().isBlank()) {
return CriterionTestResult.success(criterion, actualResult);
}
// Search for the expected result
final Matcher matcher = Pattern
.compile(PslUtils.psl2JavaRegex(criterion.getExpectedResult()), Pattern.CASE_INSENSITIVE | Pattern.MULTILINE)
.matcher(actualResult);
// If the expected result is found ==> success
if (matcher.find()) {
return CriterionTestResult.success(criterion, matcher.group());
}
// No match!
return CriterionTestResult.failure(criterion, actualResult);
}
/**
* Process the given {@link WbemCriterion} and return the resulting {@link CriterionTestResult}
*
* @param wbemCriterion {@link WbemCriterion} instance we wish to run
* @param telemetryManager {@link TelemetryManager} instance from which we fetch the hostname and related configuration
* @return {@link CriterionTestResult} instance
*/
public CriterionTestResult process(WbemCriterion wbemCriterion, TelemetryManager telemetryManager) {
// Sanity check
if (wbemCriterion == null) {
return CriterionTestResult.error(wbemCriterion, "Malformed criterion. Cannot perform detection.");
}
// Gather the necessary info on the test that needs to be performed
final WbemConfiguration wbemConfiguration = (WbemConfiguration) telemetryManager
.getHostConfiguration()
.getConfigurations()
.get(WbemConfiguration.class);
if (wbemConfiguration == null) {
return CriterionTestResult.error(wbemCriterion, "The WBEM credentials are not configured for this host.");
}
// Retrieve the hostname from the WbemConfiguration, otherwise from the telemetryManager
final String hostname = telemetryManager.getHostname(List.of(WbemConfiguration.class));
// If namespace is specified as "Automatic"
if (AUTOMATIC_NAMESPACE.equalsIgnoreCase(wbemCriterion.getNamespace())) {
final String cachedNamespace = telemetryManager
.getHostProperties()
.getConnectorNamespace(connectorId)
.getAutomaticWbemNamespace();
// If not detected already, find the namespace
if (cachedNamespace == null) {
return findNamespace(hostname, wbemConfiguration, wbemCriterion, telemetryManager, connectorId);
}
// Update the criterion with the cached namespace
WqlCriterion cachedNamespaceCriterion = wbemCriterion.copy();
cachedNamespaceCriterion.setNamespace(cachedNamespace);
// Run the test
return performDetectionTest(hostname, wbemConfiguration, cachedNamespaceCriterion, telemetryManager);
}
// Run the test
return performDetectionTest(hostname, wbemConfiguration, wbemCriterion, telemetryManager);
}
/**
* Represents a WQL Query (i.e. a query in a namespace)
*/
@Data
static class WqlQuery {
private String wql;
private String namespace;
WqlQuery(final String wql, final String namespace) {
this.wql = wql;
this.namespace = namespace;
}
}
/**
* Data class representing the result of querying for possible namespaces.
* Provides information about the possible namespaces, success status, and an error message if applicable.
*/
@Data
@Builder
public static class PossibleNamespacesResult {
private Set possibleNamespaces;
private boolean success;
private String errorMessage;
}
/**
* Data class representing the result for a specific namespace.
* Contains information about the namespace itself and a CriterionTestResult.
*/
@Data
@Builder
public static class NamespaceResult {
private String namespace;
private CriterionTestResult result;
}
}