diff --git a/opennms-assemblies/xsds/src/license/THIRD-PARTY.properties b/opennms-assemblies/xsds/src/license/THIRD-PARTY.properties index 9ef7bdfabe85..f56888e939ff 100644 --- a/opennms-assemblies/xsds/src/license/THIRD-PARTY.properties +++ b/opennms-assemblies/xsds/src/license/THIRD-PARTY.properties @@ -1,4 +1,8 @@ avalon-framework--avalon-framework--4.1.5=apache_v1_1 +com.hubspot--algebra--1.5=apache_v2 +com.hubspot.immutables--hubspot-style--1.4=apache_v2 +com.hubspot.immutables--immutable-collection-encodings--1.4=apache_v2 +com.hubspot.immutables--immutables-exceptions--1.9=apache_v2 javax.transaction--jta--1.1=cddl_v1_1 xalan--serializer--2.7.3=apache_v2 xalan--xalan--2.7.3=apache_v2 diff --git a/opennms-full-assembly/src/license/THIRD-PARTY.properties b/opennms-full-assembly/src/license/THIRD-PARTY.properties index f8c847053b3d..10758a32d478 100644 --- a/opennms-full-assembly/src/license/THIRD-PARTY.properties +++ b/opennms-full-assembly/src/license/THIRD-PARTY.properties @@ -2,6 +2,10 @@ alt.dev.jmta--jmta--1.0=Raphael Szwarc Public Source License, Version 1.0 avalon-framework--avalon-framework--4.1.5=apache_v1_1 bsh--bsh--1.3.0=spl_v1_0 colt--colt--1.2.0=mit +com.hubspot--algebra--1.5=apache_v2 +com.hubspot.immutables--hubspot-style--1.4=apache_v2 +com.hubspot.immutables--immutable-collection-encodings--1.4=apache_v2 +com.hubspot.immutables--immutables-exceptions--1.9=apache_v2 commons-discovery--commons-discovery--0.2=apache_v2 findbugs--annotations--1.0.0=lgpl_v2_1 jakarta-regexp--jakarta-regexp--1.4=apache_v2 diff --git a/opennms-webapp-rest/pom.xml b/opennms-webapp-rest/pom.xml index b41e9b24e340..ee83143092b6 100644 --- a/opennms-webapp-rest/pom.xml +++ b/opennms-webapp-rest/pom.xml @@ -205,6 +205,43 @@ org.opennms.core.config ${onmsLibScope} + + + org.opennms.features.measurements + org.opennms.features.measurements.api + ${onmsLibScope} + + + com.hubspot.jinjava + jinjava + 2.8.3 + + + + com.fasterxml.jackson.core + * + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + org.javassist + javassist + + + commons-net + commons-net + + + org.slf4j slf4j-api diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/EdgeInfo.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/EdgeInfo.java new file mode 100644 index 000000000000..0f81d83c26d0 --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/EdgeInfo.java @@ -0,0 +1,96 @@ +/* + * 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.web.rest.v2.infopanel; + +import org.opennms.netmgt.model.OnmsNode; +import org.opennms.netmgt.model.OnmsSnmpInterface; + +/** + * The {@code edge} variable for edge-scoped info-panel templates: the + * Vaadin-free counterpart of the legacy {@code LinkdEdge} the old map put + * into the template context. The legacy surface templates used -- + * {@code edge.discoveredBy}, {@code edge.sourcePort.ifIndex}, + * {@code edge.sourcePort.vertex} (the node) -- maps onto this shape as + * {@code edge.discoveredBy}, {@code edge.sourcePort.ifIndex} and + * {@code edge.sourcePort.node}. + * + *

Each side also exposes the resolved {@link OnmsSnmpInterface} (when the + * port name matches one), which is the identity that future link status / + * metrics work keys on as well -- extend here, not in a parallel model. + */ +public class EdgeInfo { + + /** One endpoint of the edge: a node plus (optionally) a resolved port. */ + public static class Port { + private final OnmsNode node; + private final String ifName; + private final OnmsSnmpInterface snmpInterface; + + public Port(final OnmsNode node, final String ifName, final OnmsSnmpInterface snmpInterface) { + this.node = node; + this.ifName = ifName; + this.snmpInterface = snmpInterface; + } + + public OnmsNode getNode() { + return node; + } + + /** The port label persisted with the link (typically the ifName). */ + public String getIfName() { + return ifName; + } + + /** Resolved SNMP interface for the port name, or null when unmatched. */ + public OnmsSnmpInterface getSnmpInterface() { + return snmpInterface; + } + + /** Legacy-parity convenience ({@code LinkdPort.getIfIndex()}). */ + public Integer getIfIndex() { + return snmpInterface != null ? snmpInterface.getIfIndex() : null; + } + } + + private final String discoveredBy; + private final Port sourcePort; + private final Port targetPort; + + public EdgeInfo(final String discoveredBy, final Port sourcePort, final Port targetPort) { + this.discoveredBy = discoveredBy; + this.sourcePort = sourcePort; + this.targetPort = targetPort; + } + + /** Discovery protocol (lldp, cdp, ospf, isis, bridge), or null for drawn links. */ + public String getDiscoveredBy() { + return discoveredBy; + } + + public Port getSourcePort() { + return sourcePort; + } + + public Port getTargetPort() { + return targetPort; + } +} diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/InfoPanelItem.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/InfoPanelItem.java new file mode 100644 index 000000000000..25d6954f10a5 --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/InfoPanelItem.java @@ -0,0 +1,75 @@ +/* + * 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.web.rest.v2.infopanel; + +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * One rendered info-panel item: a titled HTML fragment produced from an + * {@code etc/infopanel/} Jinjava template. {@code order} controls display + * sequence (ascending). The HTML is operator-authored; the consuming client is + * responsible for sanitizing it before injecting into the DOM. + */ +public class InfoPanelItem { + + @JsonProperty("title") + private String title; + + @JsonProperty("order") + private int order; + + @JsonProperty("html") + private String html; + + public InfoPanelItem() { + } + + public InfoPanelItem(final String title, final int order, final String html) { + this.title = title; + this.order = order; + this.html = html; + } + + public String getTitle() { + return title; + } + + public void setTitle(final String title) { + this.title = title; + } + + public int getOrder() { + return order; + } + + public void setOrder(final int order) { + this.order = order; + } + + public String getHtml() { + return html; + } + + public void setHtml(final String html) { + this.html = html; + } +} diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/InfoPanelRenderer.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/InfoPanelRenderer.java new file mode 100644 index 000000000000..9c65501d4ccc --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/InfoPanelRenderer.java @@ -0,0 +1,198 @@ +/* + * 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.web.rest.v2.infopanel; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.opennms.netmgt.dao.api.NodeDao; +import org.opennms.netmgt.dao.api.ResourceDao; +import org.opennms.netmgt.measurements.api.MeasurementsService; +import org.opennms.netmgt.model.OnmsNode; +import org.opennms.netmgt.model.OnmsResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.interpret.RenderResult; +import com.hubspot.jinjava.interpret.TemplateError; +import com.hubspot.jinjava.lib.fn.ELFunctionDefinition; + +/** + * Renders operator-authored Jinjava templates from {@code $OPENNMS_HOME/etc/infopanel/} + * into structured {@link InfoPanelItem}s for a selected node. This is the + * Vaadin-free counterpart of the legacy topology map's + * {@code GenericInfoPanelItemProvider}: it builds an equivalent template context + * (so existing templates render unchanged) but returns HTML rather than Vaadin + * components, for the REST layer. + * + *

Node selections render with the {@code node}/{@code nodeResource} + * context; edge selections render with the {@code edge} context (see + * {@link EdgeInfo} for how the legacy {@code LinkdEdge} surface maps). The + * legacy {@code vertex} variable is not populated; templates guard their use + * with {@code vertex != null}, so vertex-only templates are simply not shown. + */ +public class InfoPanelRenderer { + + private static final Logger LOG = LoggerFactory.getLogger(InfoPanelRenderer.class); + + private final NodeDao nodeDao; + private final ResourceDao resourceDao; + private final MeasurementsService measurementsService; // may be null + private final Path templateDir; + + private final Jinjava jinjava; + private final NodeDaoWrapper nodeDaoWrapper; + private final ResourceDaoWrapper resourceDaoWrapper; + + public InfoPanelRenderer(final NodeDao nodeDao, + final ResourceDao resourceDao, + final MeasurementsService measurementsService, + final Path templateDir) { + this.nodeDao = Objects.requireNonNull(nodeDao); + this.resourceDao = Objects.requireNonNull(resourceDao); + this.measurementsService = measurementsService; + this.templateDir = Objects.requireNonNull(templateDir); + + this.jinjava = withClassLoaderFix(Jinjava::new); + this.jinjava.getGlobalContext().registerFunction( + new ELFunctionDefinition("System", "currentTimeMillis", System.class, "currentTimeMillis")); + + this.nodeDaoWrapper = new NodeDaoWrapper(nodeDao); + this.resourceDaoWrapper = new ResourceDaoWrapper(resourceDao); + } + + /** + * Render every applicable {@code etc/infopanel/} template for the given node, + * returning the visible, error-free items sorted by their {@code order}. + * Returns an empty list when no template directory exists (the common case). + */ + public List renderForNode(final OnmsNode node) { + Objects.requireNonNull(node); + return renderAll(createContext(node)); + } + + /** + * Render every applicable template for the given edge ({@code edge} in the + * template context, alongside the shared DAO/measurements helpers), same + * visible/title/order contract as {@link #renderForNode}. + */ + public List renderForEdge(final EdgeInfo edge) { + Objects.requireNonNull(edge); + final Map context = sharedContext(); + context.put("edge", edge); + return renderAll(context); + } + + private List renderAll(final Map context) { + if (!Files.isDirectory(templateDir)) { + return Collections.emptyList(); + } + final List items = new ArrayList<>(); + try (final DirectoryStream stream = Files.newDirectoryStream(templateDir, "*.html")) { + for (final Path path : stream) { + try { + final RenderResult result = render(path, context); + final boolean fatal = result.getErrors().stream() + .anyMatch(e -> e.getSeverity() == TemplateError.ErrorType.FATAL); + if (fatal) { + LOG.warn("Skipping info-panel template {} due to fatal render error(s): {}", path, result.getErrors()); + continue; + } + if (Boolean.TRUE.equals(result.getContext().getOrDefault("visible", false))) { + final String title = String.valueOf(result.getContext().getOrDefault("title", "No title defined")); + // Templates control this value; tolerate non-numeric junk. + final Object rawOrder = result.getContext().getOrDefault("order", 0L); + final int order = rawOrder instanceof Number ? ((Number) rawOrder).intValue() : 0; + items.add(new InfoPanelItem(title, order, result.getOutput())); + } + } catch (final IOException | RuntimeException e) { + // A broken template skips itself, never the whole request. + LOG.warn("Failed to render info-panel template {}: {}", path, e.getMessage()); + } + } + } catch (final IOException e) { + LOG.warn("Failed to read info-panel template directory {}: {}", templateDir, e.getMessage()); + return Collections.emptyList(); + } + items.sort(Comparator.comparingInt(InfoPanelItem::getOrder)); + return items; + } + + private RenderResult render(final Path path, final Map context) throws IOException { + // Fresh map per template: templates write visible/title/order into it. + final Map templateContext = new HashMap<>(context); + try (final Stream lines = Files.lines(path, StandardCharsets.UTF_8)) { + final String template = lines.collect(Collectors.joining("\n")); + return withClassLoaderFix(() -> jinjava.renderForResult(template, templateContext)); + } + } + + private Map createContext(final OnmsNode node) { + final Map context = sharedContext(); + context.put("node", node); + final OnmsResource resource = resourceDao.getResourceForNode(node); + if (resource != null) { + context.put("nodeResource", resource); + } + return context; + } + + /** Context entries common to node- and edge-scoped renders. */ + private Map sharedContext() { + final Map context = new HashMap<>(); + context.put("nodeDao", nodeDaoWrapper); + context.put("resourceDao", resourceDaoWrapper); + if (measurementsService != null) { + context.put("measurements", new MeasurementsWrapper(measurementsService)); + } + return context; + } + + /** + * Jinjava's JUEL expression engine resolves helper classes via the thread + * context classloader; pin it to this class's loader for the duration of the + * call so it finds the bundled dependencies (mirrors the legacy provider). + */ + private static T withClassLoaderFix(final Supplier supplier) { + final ClassLoader previous = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(InfoPanelRenderer.class.getClassLoader()); + return supplier.get(); + } finally { + Thread.currentThread().setContextClassLoader(previous); + } + } +} diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/MeasurementsWrapper.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/MeasurementsWrapper.java new file mode 100644 index 000000000000..aadcf023bdb6 --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/MeasurementsWrapper.java @@ -0,0 +1,215 @@ +/* + * 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.web.rest.v2.infopanel; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.opennms.netmgt.measurements.api.MeasurementsService; +import org.opennms.netmgt.measurements.api.exceptions.MeasurementException; +import org.opennms.netmgt.measurements.model.QueryRequest; +import org.opennms.netmgt.measurements.model.QueryResponse; +import org.opennms.netmgt.measurements.model.Source; +import org.opennms.netmgt.model.OnmsNode; +import org.opennms.netmgt.model.OnmsSnmpInterface; + +import com.google.common.primitives.Doubles; + +/** + * Wrapper around {@link MeasurementsService} placed into the Jinjava template + * context as {@code measurements}, so templates can embed last-values, + * arbitrary queries, and interface utilization. + * + *

Mirrors the legacy topology map's wrapper of the same name so existing + * {@code etc/infopanel/} templates render unchanged. + */ +public class MeasurementsWrapper { + + private final MeasurementsService measurementsService; + + public MeasurementsWrapper(final MeasurementsService measurementsService) { + this.measurementsService = measurementsService; + } + + /** Last value for a resource/attribute over the default 15-minute window (AVERAGE). */ + public double getLastValue(final String resource, final String attribute) throws MeasurementException { + return getLastValue(resource, attribute, "AVERAGE"); + } + + /** Last value for a resource/attribute with the given aggregation. */ + public double getLastValue(final String resource, final String attribute, final String aggregation) throws MeasurementException { + return getLastValue(resource, attribute, aggregation, true); + } + + public double getLastValue(final String resource, final String attribute, final String aggregation, final boolean relaxed) throws MeasurementException { + final long end = System.currentTimeMillis(); + final long start = end - (15 * 60 * 1000); + + final QueryResponse.WrappedPrimitive[] columns = queryInt(resource, attribute, start, end, 300000, aggregation, relaxed).getColumns(); + + if (columns.length > 0) { + final double[] values = columns[0].getList(); + for (int i = values.length - 1; i >= 0; i--) { + if (!Double.isNaN(values[i])) { + return values[i]; + } + } + } + return Double.NaN; + } + + /** Query a resource/attribute over an explicit window, returning the series. */ + public List query(final String resource, final String attribute, final long start, final long end, final long step, final String aggregation, final boolean relaxed) throws MeasurementException { + final QueryResponse.WrappedPrimitive[] columns = queryInt(resource, attribute, start, end, step, aggregation, relaxed).getColumns(); + if (columns.length > 0) { + return Doubles.asList(columns[0].getList()); + } + return Collections.emptyList(); + } + + /** + * Compute the in/out percentage utilization of a node interface (by ifName), + * using HC octet attributes with non-HC fallbacks. + */ + public List computeUtilization(final OnmsNode node, final String ifName) throws MeasurementException { + final long end = System.currentTimeMillis(); + final long start = end - (15 * 60 * 1000); + + for (final OnmsSnmpInterface snmpInterface : node.getSnmpInterfaces()) { + if (ifName.equals(snmpInterface.getIfName())) { + final String resourceId = "node[" + node.getId() + "].interfaceSnmp[" + snmpInterface.computeLabelForRRD() + "]"; + return computeUtilization(resourceId, start, end, 300000, "AVERAGE"); + } + } + return Arrays.asList(Double.NaN, Double.NaN); + } + + /** + * Compute the in/out percentage utilization of an interface resource over a + * window. The rates are queried as plain columns and the percentages are + * computed here from the interface's {@code ifHighSpeed} constant (exposed + * in the query response, prefixed by the source label) -- the legacy JEXL + * expressions referenced misspelled variables and relied on engine quirks. + */ + public List computeUtilization(final String resource, final long start, final long end, final long step, final String aggregation) throws MeasurementException { + final QueryRequest request = new QueryRequest(); + request.setRelaxed(true); + request.setStart(start); + request.setEnd(end); + request.setStep(step); + + final Source sourceIn = new Source(); + sourceIn.setAggregation(aggregation); + sourceIn.setTransient(false); + sourceIn.setAttribute("ifHCInOctets"); + sourceIn.setFallbackAttribute("ifInOctets"); + sourceIn.setResourceId(resource); + sourceIn.setLabel("ifInOctets"); + + final Source sourceOut = new Source(); + sourceOut.setAggregation(aggregation); + sourceOut.setTransient(false); + sourceOut.setAttribute("ifHCOutOctets"); + sourceOut.setFallbackAttribute("ifOutOctets"); + sourceOut.setResourceId(resource); + sourceOut.setLabel("ifOutOctets"); + + request.setSources(Arrays.asList(sourceIn, sourceOut)); + + final QueryResponse response = measurementsService.query(request); + final double speedMbps = constantAsDouble(response, "ifInOctets.ifHighSpeed", "ifOutOctets.ifHighSpeed"); + if (Double.isNaN(speedMbps) || speedMbps <= 0) { + return Arrays.asList(Double.NaN, Double.NaN); + } + + final int inIndex = indexOfLabel(response, "ifInOctets"); + final int outIndex = indexOfLabel(response, "ifOutOctets"); + if (inIndex < 0 || outIndex < 0) { + return Arrays.asList(Double.NaN, Double.NaN); + } + final double[] valuesIn = response.getColumns()[inIndex].getList(); + final double[] valuesOut = response.getColumns()[outIndex].getList(); + + for (int i = Math.min(valuesIn.length, valuesOut.length) - 1; i >= 0; i--) { + if (!Double.isNaN(valuesIn[i]) && !Double.isNaN(valuesOut[i])) { + // octets/s -> Mbit/s, as a percentage of the interface speed (Mbit/s) + return Arrays.asList( + (8 * valuesIn[i] / 1_000_000d) / speedMbps * 100d, + (8 * valuesOut[i] / 1_000_000d) / speedMbps * 100d); + } + } + return Arrays.asList(Double.NaN, Double.NaN); + } + + /** First parseable value among the named response constants, else NaN. */ + private static double constantAsDouble(final QueryResponse response, final String... keys) { + if (response.getConstants() == null) { + return Double.NaN; + } + for (final String key : keys) { + for (final QueryResponse.QueryConstant constant : response.getConstants()) { + if (key.equals(constant.getKey()) && constant.getValue() != null) { + try { + return Double.parseDouble(constant.getValue()); + } catch (final NumberFormatException ignored) { + // fall through to the next candidate + } + } + } + } + return Double.NaN; + } + + private static int indexOfLabel(final QueryResponse response, final String label) { + final String[] labels = response.getLabels(); + for (int i = 0; labels != null && i < labels.length; i++) { + if (label.equals(labels[i])) { + return i; + } + } + return -1; + } + + /** Direct pass-through to the measurements query API. */ + public QueryResponse query(final QueryRequest request) throws MeasurementException { + return measurementsService.query(request); + } + + private QueryResponse queryInt(final String resource, final String attribute, final long start, final long end, final long step, final String aggregation, final boolean relaxed) throws MeasurementException { + final QueryRequest request = new QueryRequest(); + request.setRelaxed(relaxed); + request.setStart(start); + request.setEnd(end); + request.setStep(step); + + final Source source = new Source(); + source.setAggregation(aggregation); + source.setTransient(false); + source.setAttribute(attribute); + source.setResourceId(resource); + source.setLabel(attribute); + + request.setSources(Collections.singletonList(source)); + return measurementsService.query(request); + } +} diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/NodeDaoWrapper.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/NodeDaoWrapper.java new file mode 100644 index 000000000000..5eda18d66b95 --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/NodeDaoWrapper.java @@ -0,0 +1,117 @@ +/* + * 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.web.rest.v2.infopanel; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.opennms.core.criteria.Criteria; +import org.opennms.netmgt.dao.api.NodeDao; +import org.opennms.netmgt.model.OnmsNode; + +/** + * Read-only wrapper around {@link NodeDao} placed into the Jinjava template + * context used by {@link InfoPanelRenderer}. Only query/lookup methods are + * delegated; mutating operations are deliberately not exposed. + * + *

Because Jinjava resolves methods via reflection on the runtime class, + * keeping this as a concrete POJO (rather than a {@code NodeDao} subclass) + * guarantees only the methods declared here are reachable from templates. + * + *

Mirrors the legacy topology map's wrapper of the same name so existing + * {@code etc/infopanel/} templates render unchanged. + */ +public class NodeDaoWrapper { + + private final NodeDao delegate; + + public NodeDaoWrapper(final NodeDao delegate) { + this.delegate = Objects.requireNonNull(delegate); + } + + /** Look up a node by its integer database ID. */ + public OnmsNode get(final Integer id) { + return delegate.get(id); + } + + /** Look up a node by criteria string (nodeId or foreignSource:foreignId). */ + public OnmsNode get(final String lookupCriteria) { + return delegate.get(lookupCriteria); + } + + /** Load a node by ID; throws if not found. */ + public OnmsNode load(final Integer id) { + return delegate.load(id); + } + + /** Map of every nodeId → nodeLabel. Cheaper than {@code findAll()}. */ + public Map getAllLabelsById() { + return delegate.getAllLabelsById(); + } + + /** Return just the label for one node. */ + public String getLabelForId(final Integer id) { + return delegate.getLabelForId(id); + } + + /** Return the monitoring-location name for one node. */ + public String getLocationForId(final Integer id) { + return delegate.getLocationForId(id); + } + + /** Return all nodes (expensive on large systems -- use sparingly). */ + public List findAll() { + return delegate.findAll(); + } + + /** Find nodes whose label matches exactly. */ + public List findByLabel(final String label) { + return delegate.findByLabel(label); + } + + /** Count all nodes. */ + public int countAll() { + return delegate.countAll(); + } + + /** Count nodes matching the supplied {@link Criteria}. */ + public int countMatching(final Criteria criteria) { + return delegate.countMatching(criteria); + } + + /** Return nodes matching the supplied {@link Criteria}. */ + public List findMatching(final Criteria criteria) { + return delegate.findMatching(criteria); + } + + /** Map of foreignId → nodeId for every node that has a foreignSource. */ + public Map getForeignIdToNodeIdMap(final String foreignSource) { + return delegate.getForeignIdToNodeIdMap(foreignSource); + } + + /** Map of foreignSource → set-of-foreignIds. */ + public Map> getForeignIdsPerForeignSourceMap() { + return delegate.getForeignIdsPerForeignSourceMap(); + } +} diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/ResourceDaoWrapper.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/ResourceDaoWrapper.java new file mode 100644 index 000000000000..13e5e37e6e50 --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/ResourceDaoWrapper.java @@ -0,0 +1,68 @@ +/* + * 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.web.rest.v2.infopanel; + +import java.util.List; +import java.util.Objects; + +import org.opennms.netmgt.dao.api.ResourceDao; +import org.opennms.netmgt.model.OnmsNode; +import org.opennms.netmgt.model.OnmsResource; +import org.opennms.netmgt.model.ResourceId; + +/** + * Read-only wrapper around {@link ResourceDao} placed into the Jinjava template + * context used by {@link InfoPanelRenderer}. Only query/lookup methods are + * delegated; the mutating {@code deleteResourceById} is not exposed. + * + *

Mirrors the legacy topology map's wrapper of the same name so existing + * {@code etc/infopanel/} templates render unchanged. + */ +public class ResourceDaoWrapper { + + private final ResourceDao delegate; + + public ResourceDaoWrapper(final ResourceDao delegate) { + this.delegate = Objects.requireNonNull(delegate); + } + + /** + * Retrieve a resource by its unique {@link ResourceId} + * (e.g. {@code node[1].nodeSnmp[]}), or {@code null} if not found. + */ + public OnmsResource getResourceById(final ResourceId id) { + return delegate.getResourceById(id); + } + + /** Retrieve the resource tree rooted at the given node (children populated). */ + public OnmsResource getResourceForNode(final OnmsNode node) { + return delegate.getResourceForNode(node); + } + + /** + * Return all top-level resources (typically one per monitored node). + * Can be expensive -- use with care in templates. + */ + public List findTopLevelResources() { + return delegate.findTopLevelResources(); + } +} diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/TopologyInfopanelRestService.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/TopologyInfopanelRestService.java new file mode 100644 index 000000000000..0a28f9da6f42 --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/infopanel/TopologyInfopanelRestService.java @@ -0,0 +1,147 @@ +/* + * 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.web.rest.v2.infopanel; + +import java.nio.file.Paths; +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.opennms.netmgt.dao.api.NodeDao; +import org.opennms.netmgt.dao.api.ResourceDao; +import org.opennms.netmgt.measurements.api.MeasurementsService; +import org.opennms.netmgt.model.OnmsNode; +import org.opennms.netmgt.model.OnmsSnmpInterface; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import io.swagger.v3.oas.annotations.tags.Tag; + +/** + * Serves operator-configured topology info-panel content for a node: the + * rendered {@code $OPENNMS_HOME/etc/infopanel/} Jinjava templates as + * {@code [{title, order, html}]}. The new Vue topology Inspector appends these + * below its native fields, so the existing template investment carries over. + * + *

Read-only and open to any authenticated user via the default + * {@code /api/v2/**} security rules. {@code @Transactional} keeps a Hibernate + * session open so templates can traverse lazy node/resource associations. + */ +@Component +@Path("topology/infopanel") +@Tag(name = "Topology", description = "Topology map APIs") +@Produces(MediaType.APPLICATION_JSON) +public class TopologyInfopanelRestService { + + @Autowired + private NodeDao m_nodeDao; + + @Autowired + private ResourceDao m_resourceDao; + + // Optional: templates that embed metrics use it; absent installs degrade + // gracefully (those template sections are skipped, not fatal to the rest). + @Autowired(required = false) + private MeasurementsService m_measurementsService; + + private volatile InfoPanelRenderer m_renderer; + + @GET + @Transactional(readOnly = true) + public List getForNode(@QueryParam("nodeId") final Integer nodeId) { + if (nodeId == null) { + throw webException(Response.Status.BAD_REQUEST, "A nodeId query parameter is required"); + } + final OnmsNode node = m_nodeDao.get(nodeId); + if (node == null) { + throw webException(Response.Status.NOT_FOUND, "No node with id " + nodeId); + } + return renderer().renderForNode(node); + } + + /** + * Edge-scoped panels for a link between two nodes: templates render with + * an {@code edge} context (see {@link EdgeInfo}). Port names and protocol + * are optional -- they come from a link's discovery binding when present. + */ + @GET + @javax.ws.rs.Path("edge") + @Transactional(readOnly = true) + public List getForEdge(@QueryParam("sourceNodeId") final Integer sourceNodeId, + @QueryParam("targetNodeId") final Integer targetNodeId, + @QueryParam("sourcePort") final String sourcePort, + @QueryParam("targetPort") final String targetPort, + @QueryParam("protocol") final String protocol) { + if (sourceNodeId == null || targetNodeId == null) { + throw webException(Response.Status.BAD_REQUEST, "sourceNodeId and targetNodeId query parameters are required"); + } + final OnmsNode source = m_nodeDao.get(sourceNodeId); + final OnmsNode target = m_nodeDao.get(targetNodeId); + if (source == null || target == null) { + throw webException(Response.Status.NOT_FOUND, + "No node with id " + (source == null ? sourceNodeId : targetNodeId)); + } + final EdgeInfo edge = new EdgeInfo(protocol, + new EdgeInfo.Port(source, sourcePort, resolveSnmpInterface(source, sourcePort)), + new EdgeInfo.Port(target, targetPort, resolveSnmpInterface(target, targetPort))); + return renderer().renderForEdge(edge); + } + + /** + * Match a persisted port label against the node's SNMP interfaces by + * ifName, then ifDescr (the two labels discovery writes). Null when the + * port is unknown or nothing matches -- templates handle a null + * {@code snmpInterface}/{@code ifIndex} themselves. + */ + private static OnmsSnmpInterface resolveSnmpInterface(final OnmsNode node, final String port) { + if (port == null || port.isBlank()) { + return null; + } + return node.getSnmpInterfaces().stream() + .filter(s -> port.equals(s.getIfName()) || port.equals(s.getIfDescr())) + .findFirst() + .orElse(null); + } + + private InfoPanelRenderer renderer() { + if (m_renderer == null) { + synchronized (this) { + if (m_renderer == null) { + final java.nio.file.Path dir = Paths.get(System.getProperty("opennms.home", "."), "etc", "infopanel"); + m_renderer = new InfoPanelRenderer(m_nodeDao, m_resourceDao, m_measurementsService, dir); + } + } + } + return m_renderer; + } + + private static javax.ws.rs.WebApplicationException webException(final Response.Status status, final String message) { + return new javax.ws.rs.WebApplicationException( + Response.status(status).entity(message).type(MediaType.TEXT_PLAIN).build()); + } +} diff --git a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/infopanel/InfoPanelRendererTest.java b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/infopanel/InfoPanelRendererTest.java new file mode 100644 index 000000000000..ed4b298fafcf --- /dev/null +++ b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/infopanel/InfoPanelRendererTest.java @@ -0,0 +1,145 @@ +/* + * 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.web.rest.v2.infopanel; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.opennms.netmgt.dao.api.NodeDao; +import org.opennms.netmgt.dao.api.ResourceDao; +import org.opennms.netmgt.model.OnmsNode; +import org.opennms.netmgt.model.OnmsSnmpInterface; + +/** + * Unit-tests the Jinjava info-panel rendering against a temporary template + * directory and mocked DAOs -- no Spring context or database. + */ +public class InfoPanelRendererTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + private NodeDao nodeDao; + private ResourceDao resourceDao; + private OnmsNode node; + + @Before + public void setUp() { + nodeDao = mock(NodeDao.class); + resourceDao = mock(ResourceDao.class); + when(resourceDao.getResourceForNode(org.mockito.ArgumentMatchers.any())).thenReturn(null); + node = new OnmsNode(); + node.setId(42); + node.setLabel("test-node"); + } + + private void writeTemplate(final String name, final String body) throws Exception { + final Path p = tmp.getRoot().toPath().resolve(name); + Files.write(p, body.getBytes(StandardCharsets.UTF_8)); + } + + private InfoPanelRenderer renderer() { + return new InfoPanelRenderer(nodeDao, resourceDao, null, tmp.getRoot().toPath()); + } + + @Test + public void rendersVisibleTemplatesSortedByOrderWithNodeContext() throws Exception { + writeTemplate("a-second.html", + "{% set visible = true %}{% set title = \"Second\" %}{% set order = 10 %}" + + "{{ node.label }} (id {{ node.id }})"); + writeTemplate("b-first.html", + "{% set visible = true %}{% set title = \"First\" %}{% set order = 5 %}hello"); + + final List items = renderer().renderForNode(node); + + assertThat(items, hasSize(2)); + // sorted by order ascending regardless of filename + assertThat(items.stream().map(InfoPanelItem::getTitle).collect(Collectors.toList()), + contains("First", "Second")); + assertThat(items.get(0).getOrder(), is(5)); + assertThat(items.get(1).getOrder(), is(10)); + // node context resolved + assertThat(items.get(1).getHtml(), containsString("test-node (id 42)")); + } + + @Test + public void skipsTemplatesThatAreNotVisible() throws Exception { + writeTemplate("hidden.html", "{% set visible = false %}{% set title = \"Nope\" %}should not show"); + + assertThat(renderer().renderForNode(node), is(empty())); + } + + @Test + public void missingTemplateDirYieldsNoItems() { + final Path absent = tmp.getRoot().toPath().resolve("does-not-exist"); + final InfoPanelRenderer r = new InfoPanelRenderer(nodeDao, resourceDao, null, absent); + assertThat(r.renderForNode(node), is(empty())); + } + + @Test + public void rendersEdgeScopedTemplatesWithEdgeContext() throws Exception { + writeTemplate("edge-panel.html", + "{% if edge %}{% set visible = true %}{% set title = \"Link\" %}" + + "{{ edge.discoveredBy }}: {{ edge.sourcePort.node.label }}/{{ edge.sourcePort.ifName }}" + + " -> {{ edge.targetPort.node.label }}/{{ edge.targetPort.ifIndex }}{% endif %}"); + writeTemplate("node-panel.html", + "{% if node %}{% set visible = true %}{% set title = \"Node\" %}{{ node.label }}{% endif %}"); + + final OnmsNode target = new OnmsNode(); + target.setId(43); + target.setLabel("far-node"); + final OnmsSnmpInterface snmp = new OnmsSnmpInterface(); + snmp.setIfIndex(7); + final EdgeInfo edge = new EdgeInfo("lldp", + new EdgeInfo.Port(node, "eth0", null), + new EdgeInfo.Port(target, "eth7", snmp)); + + final List items = renderer().renderForEdge(edge); + // the node-scoped template guards on `node` and must not render + assertThat(items, hasSize(1)); + assertThat(items.get(0).getTitle(), is("Link")); + assertThat(items.get(0).getHtml(), is("lldp: test-node/eth0 -> far-node/7")); + } + + @Test + public void nodeScopedRenderDoesNotExposeEdge() throws Exception { + writeTemplate("edge-panel.html", + "{% if edge %}{% set visible = true %}{% set title = \"Link\" %}x{% endif %}"); + assertThat(renderer().renderForNode(node), is(empty())); + } +} diff --git a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/infopanel/TopologyInfopanelRestServiceIT.java b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/infopanel/TopologyInfopanelRestServiceIT.java new file mode 100644 index 000000000000..e08b725303c2 --- /dev/null +++ b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/infopanel/TopologyInfopanelRestServiceIT.java @@ -0,0 +1,118 @@ +/* + * 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.web.rest.v2.infopanel; + +import static org.junit.Assert.assertEquals; + +import org.json.JSONArray; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.opennms.core.test.MockLogAppender; +import org.opennms.core.test.OpenNMSJUnit4ClassRunner; +import org.opennms.core.test.db.annotations.JUnitTemporaryDatabase; +import org.opennms.core.test.rest.AbstractSpringJerseyRestTestCase; +import org.opennms.test.JUnitConfigurationEnvironment; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; + +/** + * Integration test for {@code GET /api/v2/topology/infopanel}. Exercises the + * resource through the REST stack: that it is wired (beans autowire), validates + * its input (400 without a nodeId), 404s an unknown node, and returns a JSON + * array for a real node. The rendering of {@code etc/infopanel} templates is + * covered by {@code InfoPanelRendererTest}; a stock test environment has no + * such templates, so a real node yields an empty array here. + */ +@RunWith(OpenNMSJUnit4ClassRunner.class) +@WebAppConfiguration +@ContextConfiguration(locations = { + "classpath:/META-INF/opennms/applicationContext-soa.xml", + "classpath:/META-INF/opennms/applicationContext-commonConfigs.xml", + "classpath:/META-INF/opennms/applicationContext-minimal-conf.xml", + "classpath:/META-INF/opennms/applicationContext-dao.xml", + "classpath:/META-INF/opennms/applicationContext-mockConfigManager.xml", + "classpath*:/META-INF/opennms/component-service.xml", + "classpath*:/META-INF/opennms/component-dao.xml", + "classpath:/META-INF/opennms/applicationContext-databasePopulator.xml", + "classpath:/META-INF/opennms/mockEventIpcManager.xml", + "file:src/main/webapp/WEB-INF/applicationContext-svclayer.xml", + "file:src/main/webapp/WEB-INF/applicationContext-cxf-common.xml" +}) +@JUnitConfigurationEnvironment(systemProperties = "org.opennms.timeseries.strategy=integration") +@JUnitTemporaryDatabase +public class TopologyInfopanelRestServiceIT extends AbstractSpringJerseyRestTestCase { + + public TopologyInfopanelRestServiceIT() { + super(CXF_REST_V2_CONTEXT_PATH); + } + + @Override + protected void afterServletStart() throws Exception { + MockLogAppender.setupLogging(true, "DEBUG"); + } + + @Test + @JUnitTemporaryDatabase + public void infopanelEndpoint() throws Exception { + // A nodeId is required. + sendRequest(GET, "/topology/infopanel", 400); + + // Unknown node -> 404 (before any template is created). + sendRequest(GET, "/topology/infopanel", parseParamData("nodeId=999999"), 404); + + // Create a node, then the endpoint returns a JSON array for it. With no + // etc/infopanel templates in the test environment, the array is empty. + final String node = "" + + "H" + + "TestMachine1" + + ".1.3.6.1.4.1.8072.3.2.255" + + ""; + sendPost("/nodes", node, 201); + + final String body = sendRequest(GET, "/topology/infopanel", parseParamData("nodeId=1"), 200); + assertEquals(0, new JSONArray(body).length()); + } + + @Test + @JUnitTemporaryDatabase + public void edgeEndpointValidatesAndRenders() throws Exception { + // Both node ids are required. + sendRequest(GET, "/topology/infopanel/edge", 400); + sendRequest(GET, "/topology/infopanel/edge", parseParamData("sourceNodeId=1"), 400); + + // Unknown nodes are a 404. + sendRequest(GET, "/topology/infopanel/edge", parseParamData("sourceNodeId=1&targetNodeId=999999"), 404); + + // Happy path: create a node, then the endpoint renders for a link with + // that node on both ends. With no etc/infopanel templates in the test + // environment the rendered array is empty. + final String node = "" + + "H" + + "TestMachine1" + + ".1.3.6.1.4.1.8072.3.2.255" + + ""; + sendPost("/nodes", node, 201); + final String body = sendRequest(GET, "/topology/infopanel/edge", + parseParamData("sourceNodeId=1&targetNodeId=1&sourcePort=eth0&targetPort=eth1&protocol=lldp"), 200); + assertEquals(0, new JSONArray(body).length()); + } +} diff --git a/opennms-webapp/pom.xml b/opennms-webapp/pom.xml index 575b648b63b0..26b3c0410393 100644 --- a/opennms-webapp/pom.xml +++ b/opennms-webapp/pom.xml @@ -836,6 +836,41 @@ org.apache.servicemix.bundles.jdom 1.1_4 + + + + com.hubspot.jinjava + jinjava + 2.8.3 + + + + com.fasterxml.jackson.core + * + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + org.javassist + javassist + + + commons-net + commons-net + + + org.opennms.extremecomponents extremecomponents