opennms-model
opennms-graph-service
diff --git a/features/graph/provider/pathoutage/pom.xml b/features/graph/provider/pathoutage/pom.xml
new file mode 100644
index 000000000000..1283805a3bf5
--- /dev/null
+++ b/features/graph/provider/pathoutage/pom.xml
@@ -0,0 +1,129 @@
+
+
+
+ org.opennms.features.graph
+ org.opennms.features.graph.provider
+ 36.0.2-SNAPSHOT
+
+ 4.0.0
+ org.opennms.features.graph.provider
+ org.opennms.features.graph.provider.pathoutage
+ OpenNMS :: Features :: Graph :: Provider :: Path Outage
+ Builds a graph from the node parent / critical-path (path outage) relationships.
+ bundle
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+
+
+
+ org.osgi
+ osgi.core
+ ${osgiVersion}
+ provided
+
+
+ org.osgi
+ osgi.cmpn
+ provided
+
+
+ org.opennms.features.graph
+ org.opennms.features.graph.api
+ ${project.version}
+
+
+ org.opennms
+ opennms-dao-api
+ ${project.version}
+ provided
+
+
+ org.opennms
+ opennms-model
+ ${project.version}
+ provided
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+ org.opennms
+ opennms-config
+ test
+
+
+ org.opennms
+ opennms-dao
+ test
+
+
+ org.opennms.core.test-api
+ org.opennms.core.test-api.lib
+ test
+
+
+ org.opennms.core.test-api
+ org.opennms.core.test-api.db
+ test
+
+
+ org.opennms.core.test-api
+ org.opennms.core.test-api.services
+ test
+
+
+ org.opennms
+ opennms-rrdtool-api
+ test
+
+
+ org.opennms.features.collection
+ org.opennms.features.collection.persistence.osgi
+ test
+
+
+ org.opennms.features.collection
+ org.opennms.features.collection.persistence.rrd
+ test
+
+
+ org.opennms.dependencies
+ jrrd2-dependencies
+ pom
+ test
+
+
+ org.opennms.features.config
+ org.opennms.features.config.mock
+ ${project.version}
+ test
+
+
+ org.opennms.features.config
+ org.opennms.features.config.upgrade
+ ${project.version}
+ test
+
+
+ org.hibernate
+ hibernate-validator
+ test
+
+
+
diff --git a/features/graph/provider/pathoutage/src/main/java/org/opennms/netmgt/graph/provider/pathoutage/PathOutageGraphProvider.java b/features/graph/provider/pathoutage/src/main/java/org/opennms/netmgt/graph/provider/pathoutage/PathOutageGraphProvider.java
new file mode 100644
index 000000000000..3e5a37b7b658
--- /dev/null
+++ b/features/graph/provider/pathoutage/src/main/java/org/opennms/netmgt/graph/provider/pathoutage/PathOutageGraphProvider.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to The OpenNMS Group, Inc (TOG) under one or more
+ * contributor license agreements. See the LICENSE.md file
+ * distributed with this work for additional information
+ * regarding copyright ownership.
+ *
+ * TOG licenses this file to You under the GNU Affero General
+ * Public License Version 3 (the "License") or (at your option)
+ * any later version. You may not use this file except in
+ * compliance with the License. You may obtain a copy of the
+ * License at:
+ *
+ * https://www.gnu.org/licenses/agpl-3.0.txt
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package org.opennms.netmgt.graph.provider.pathoutage;
+
+import java.util.LinkedHashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.opennms.netmgt.dao.api.NodeDao;
+import org.opennms.netmgt.dao.api.SessionUtils;
+import org.opennms.netmgt.graph.api.ImmutableGraph;
+import org.opennms.netmgt.graph.api.generic.GenericEdge;
+import org.opennms.netmgt.graph.api.generic.GenericGraph;
+import org.opennms.netmgt.graph.api.generic.GenericProperties;
+import org.opennms.netmgt.graph.api.generic.GenericVertex;
+import org.opennms.netmgt.graph.api.info.DefaultGraphInfo;
+import org.opennms.netmgt.graph.api.info.GraphInfo;
+import org.opennms.netmgt.graph.api.service.GraphProvider;
+import org.opennms.netmgt.model.OnmsNode;
+
+import com.google.common.collect.Maps;
+
+/**
+ * Builds a read-only graph from the node parent / critical-path relationships
+ * ({@link OnmsNode#getParent()} -- the {@code nodeParentID} column), the same
+ * data the legacy Vaadin "Path Outage" topology renders. Each participating
+ * node becomes a vertex and each parent points to its child with a directed
+ * edge, so the result is the node-parent forest (one tree per top-level node).
+ *
+ * Only nodes that take part in a parent-child relationship are included --
+ * a node with neither a parent nor any children would render as an isolated
+ * dot that conveys nothing here. On an installation where no node parents are
+ * set the graph is empty, and the UI shows its discovered-empty state.
+ *
+ * Registered as a bare {@link GraphProvider}; the graph service wraps it in
+ * a single-graph container whose id defaults to the namespace, so it is served
+ * at {@code /api/v2/graphs/pathoutage/pathoutage}.
+ */
+public class PathOutageGraphProvider implements GraphProvider {
+
+ public static final String NAMESPACE = "pathoutage";
+ private static final String LABEL = "Path Outage";
+ private static final String DESCRIPTION =
+ "Node parent / critical-path hierarchy derived from each node's parent relationship.";
+
+ private final NodeDao nodeDao;
+ private final SessionUtils sessionUtils;
+
+ public PathOutageGraphProvider(final NodeDao nodeDao, final SessionUtils sessionUtils) {
+ this.nodeDao = Objects.requireNonNull(nodeDao);
+ this.sessionUtils = Objects.requireNonNull(sessionUtils);
+ }
+
+ @Override
+ public ImmutableGraph, ?> loadGraph() {
+ // OnmsNode.getParent() is a LAZY @ManyToOne, so the whole build -- including
+ // reading each parent's id/label -- must run inside a read transaction.
+ return sessionUtils.withReadOnlyTransaction(() -> {
+ final GenericGraph.GenericGraphBuilder builder = GenericGraph.builder()
+ .graphInfo(getGraphInfo())
+ .id(NAMESPACE)
+ // Resolve node + alarm-based status for the vertices, like the other
+ // node-backed providers; the SPA also colors by nodeID independently.
+ .property(GenericProperties.Enrichment.RESOLVE_NODES, true)
+ .property(GenericProperties.Enrichment.DEFAULT_STATUS, true);
+
+ final List nodes = nodeDao.findAll();
+
+ // First pass: collect the participating node ids (children with a parent,
+ // plus those parents) and their labels, and remember the parent->child pairs.
+ final Map labelById = Maps.newHashMap();
+ final Set participating = new LinkedHashSet<>();
+ // child -> parent: value-based (unlike a Set of arrays), and a child
+ // has exactly one parent, so duplicates are structurally impossible.
+ final Map parentByChild = new LinkedHashMap<>();
+ for (final OnmsNode node : nodes) {
+ final OnmsNode parent = node.getParent();
+ if (parent == null) {
+ continue;
+ }
+ final int childId = node.getId();
+ final int parentId = parent.getId();
+ labelById.put(childId, node.getLabel());
+ labelById.put(parentId, parent.getLabel());
+ participating.add(childId);
+ participating.add(parentId);
+ parentByChild.put(childId, parentId);
+ }
+
+ // Vertices first (an edge requires its endpoints to already be present).
+ for (final Integer nodeId : participating) {
+ builder.addVertex(GenericVertex.builder()
+ .namespace(NAMESPACE)
+ .id(String.valueOf(nodeId))
+ .property(GenericProperties.LABEL, labelById.get(nodeId))
+ .property(GenericProperties.NODE_ID, String.valueOf(nodeId))
+ .build());
+ }
+
+ // No explicit edge id: GenericEdge derives a deterministic one from
+ // source->target, stable across loads (a counter would depend on
+ // NodeDao.findAll() ordering).
+ for (final Map.Entry pc : parentByChild.entrySet()) {
+ builder.addEdge(GenericEdge.builder()
+ .namespace(NAMESPACE)
+ .source(NAMESPACE, String.valueOf(pc.getValue()))
+ .target(NAMESPACE, String.valueOf(pc.getKey()))
+ .build());
+ }
+
+ // Show the whole hierarchy by default; the SPA frames it to fit.
+ builder.focus().all().apply();
+ return builder.build();
+ });
+ }
+
+ @Override
+ public GraphInfo getGraphInfo() {
+ final DefaultGraphInfo graphInfo = new DefaultGraphInfo(NAMESPACE);
+ graphInfo.setLabel(LABEL);
+ graphInfo.setDescription(DESCRIPTION);
+ return graphInfo;
+ }
+}
diff --git a/features/graph/provider/pathoutage/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/features/graph/provider/pathoutage/src/main/resources/OSGI-INF/blueprint/blueprint.xml
new file mode 100644
index 000000000000..9db35dcd9d6d
--- /dev/null
+++ b/features/graph/provider/pathoutage/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/features/graph/provider/pathoutage/src/test/java/org/opennms/netmgt/graph/provider/pathoutage/PathOutageGraphProviderIT.java b/features/graph/provider/pathoutage/src/test/java/org/opennms/netmgt/graph/provider/pathoutage/PathOutageGraphProviderIT.java
new file mode 100644
index 000000000000..3f916ed9a96c
--- /dev/null
+++ b/features/graph/provider/pathoutage/src/test/java/org/opennms/netmgt/graph/provider/pathoutage/PathOutageGraphProviderIT.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to The OpenNMS Group, Inc (TOG) under one or more
+ * contributor license agreements. See the LICENSE.md file
+ * distributed with this work for additional information
+ * regarding copyright ownership.
+ *
+ * TOG licenses this file to You under the GNU Affero General
+ * Public License Version 3 (the "License") or (at your option)
+ * any later version. You may not use this file except in
+ * compliance with the License. You may obtain a copy of the
+ * License at:
+ *
+ * https://www.gnu.org/licenses/agpl-3.0.txt
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package org.opennms.netmgt.graph.provider.pathoutage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.opennms.core.test.OpenNMSJUnit4ClassRunner;
+import org.opennms.core.test.db.annotations.JUnitTemporaryDatabase;
+import org.opennms.netmgt.dao.api.MonitoringLocationDao;
+import org.opennms.netmgt.dao.api.NodeDao;
+import org.opennms.netmgt.dao.api.SessionUtils;
+import org.opennms.netmgt.graph.api.generic.GenericEdge;
+import org.opennms.netmgt.graph.api.generic.GenericGraph;
+import org.opennms.netmgt.graph.api.generic.GenericProperties;
+import org.opennms.netmgt.graph.api.generic.GenericVertex;
+import org.opennms.netmgt.model.OnmsNode;
+import org.opennms.test.JUnitConfigurationEnvironment;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+
+/**
+ * Exercises {@link PathOutageGraphProvider} against the real Hibernate
+ * {@link NodeDao} on a temporary database. The mockito unit test can't cover
+ * the one risky part: {@code OnmsNode.getParent()} is a LAZY many-to-one, so
+ * the provider must traverse it inside its own read-only transaction. This
+ * test therefore deliberately runs WITHOUT a test-managed transaction --
+ * {@code loadGraph()} is called the way the Graph REST service calls it, and
+ * would die with a LazyInitializationException if the provider's transaction
+ * wrapping were missing.
+ */
+@RunWith(OpenNMSJUnit4ClassRunner.class)
+@ContextConfiguration(locations = {
+ "classpath:/META-INF/opennms/applicationContext-commonConfigs.xml",
+ "classpath:/META-INF/opennms/applicationContext-minimal-conf.xml",
+ "classpath:/META-INF/opennms/applicationContext-soa.xml",
+ "classpath:/META-INF/opennms/applicationContext-dao.xml",
+ "classpath:/META-INF/opennms/applicationContext-mockSnmpPeerFactory.xml",
+ "classpath:/META-INF/opennms/applicationContext-mockConfigManager.xml" })
+@JUnitConfigurationEnvironment
+@JUnitTemporaryDatabase(reuseDatabase = false)
+public class PathOutageGraphProviderIT {
+
+ @Autowired
+ private NodeDao nodeDao;
+
+ @Autowired
+ private MonitoringLocationDao locationDao;
+
+ @Autowired
+ private SessionUtils sessionUtils;
+
+ private PathOutageGraphProvider provider;
+
+ private int routerId;
+ private int switchId;
+ private int serverId;
+ private int standaloneId;
+
+ private OnmsNode node(final String label, final OnmsNode parent) {
+ final OnmsNode node = new OnmsNode(locationDao.getDefaultLocation(), label);
+ node.setParent(parent);
+ nodeDao.save(node);
+ return node;
+ }
+
+ @Before
+ public void setUp() {
+ // router -> switch -> server, plus a standalone node outside the hierarchy.
+ sessionUtils.withTransaction(() -> {
+ final OnmsNode router = node("router", null);
+ final OnmsNode sw = node("switch", router);
+ final OnmsNode server = node("server", sw);
+ final OnmsNode standalone = node("standalone", null);
+ routerId = router.getId();
+ switchId = sw.getId();
+ serverId = server.getId();
+ standaloneId = standalone.getId();
+ });
+ provider = new PathOutageGraphProvider(nodeDao, sessionUtils);
+ }
+
+ @Test
+ public void loadsTheParentHierarchyWithoutACallerTransaction() {
+ // No surrounding transaction here, on purpose -- see the class javadoc.
+ final GenericGraph graph = provider.loadGraph().asGenericGraph();
+
+ assertEquals(PathOutageGraphProvider.NAMESPACE, graph.getNamespace());
+
+ final List vertexIds = graph.getVertices().stream()
+ .map(GenericVertex::getId)
+ .collect(Collectors.toList());
+ assertTrue(vertexIds.containsAll(List.of(
+ String.valueOf(routerId), String.valueOf(switchId), String.valueOf(serverId))));
+ // The standalone node takes part in no parent-child relation -> excluded.
+ assertEquals(3, graph.getVertices().size());
+ assertTrue(!vertexIds.contains(String.valueOf(standaloneId)));
+
+ // The parent's label was read through the lazy proxy.
+ final GenericVertex switchVertex = graph.getVertex(String.valueOf(switchId));
+ assertEquals("switch", switchVertex.getProperty(GenericProperties.LABEL));
+ assertEquals(String.valueOf(switchId), switchVertex.getProperty(GenericProperties.NODE_ID));
+
+ // Directed parent -> child: router -> switch and switch -> server.
+ assertEquals(2, graph.getEdges().size());
+ for (final GenericEdge edge : graph.getEdges()) {
+ if (edge.getTarget().getId().equals(String.valueOf(switchId))) {
+ assertEquals(String.valueOf(routerId), edge.getSource().getId());
+ } else {
+ assertEquals(String.valueOf(switchId), edge.getSource().getId());
+ assertEquals(String.valueOf(serverId), edge.getTarget().getId());
+ }
+ }
+ }
+}
diff --git a/features/graph/provider/pathoutage/src/test/java/org/opennms/netmgt/graph/provider/pathoutage/PathOutageGraphProviderTest.java b/features/graph/provider/pathoutage/src/test/java/org/opennms/netmgt/graph/provider/pathoutage/PathOutageGraphProviderTest.java
new file mode 100644
index 000000000000..7b3bae89239c
--- /dev/null
+++ b/features/graph/provider/pathoutage/src/test/java/org/opennms/netmgt/graph/provider/pathoutage/PathOutageGraphProviderTest.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to The OpenNMS Group, Inc (TOG) under one or more
+ * contributor license agreements. See the LICENSE.md file
+ * distributed with this work for additional information
+ * regarding copyright ownership.
+ *
+ * TOG licenses this file to You under the GNU Affero General
+ * Public License Version 3 (the "License") or (at your option)
+ * any later version. You may not use this file except in
+ * compliance with the License. You may obtain a copy of the
+ * License at:
+ *
+ * https://www.gnu.org/licenses/agpl-3.0.txt
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package org.opennms.netmgt.graph.provider.pathoutage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.opennms.netmgt.dao.api.NodeDao;
+import org.opennms.netmgt.dao.api.SessionUtils;
+import org.opennms.netmgt.graph.api.generic.GenericEdge;
+import org.opennms.netmgt.graph.api.generic.GenericGraph;
+import org.opennms.netmgt.graph.api.generic.GenericProperties;
+import org.opennms.netmgt.graph.api.generic.GenericVertex;
+import org.opennms.netmgt.model.OnmsNode;
+
+public class PathOutageGraphProviderTest {
+
+ private NodeDao nodeDao;
+ private PathOutageGraphProvider provider;
+
+ @Before
+ public void setUp() {
+ nodeDao = mock(NodeDao.class);
+ final SessionUtils sessionUtils = mock(SessionUtils.class);
+ // Run the supplier directly; the transaction wrapper is irrelevant to the mock-backed test.
+ when(sessionUtils.withReadOnlyTransaction(any(Supplier.class)))
+ .thenAnswer(invocation -> invocation.getArgument(0, Supplier.class).get());
+ provider = new PathOutageGraphProvider(nodeDao, sessionUtils);
+ }
+
+ private static OnmsNode node(int id, String label, OnmsNode parent) {
+ final OnmsNode node = new OnmsNode();
+ node.setId(id);
+ node.setLabel(label);
+ node.setParent(parent);
+ return node;
+ }
+
+ @Test
+ public void graphInfoUsesThePathoutageNamespace() {
+ assertEquals(PathOutageGraphProvider.NAMESPACE, provider.getGraphInfo().getNamespace());
+ assertNotNull(provider.getGraphInfo().getLabel());
+ }
+
+ @Test
+ public void buildsParentChildForestAndSkipsIsolatedNodes() {
+ final OnmsNode router = node(1, "router", null);
+ final OnmsNode sw = node(2, "switch", router);
+ final OnmsNode server = node(3, "server", sw);
+ final OnmsNode standalone = node(4, "standalone", null);
+ when(nodeDao.findAll()).thenReturn(Arrays.asList(router, sw, server, standalone));
+
+ final GenericGraph graph = provider.loadGraph().asGenericGraph();
+
+ assertEquals(PathOutageGraphProvider.NAMESPACE, graph.getNamespace());
+ // standalone has neither parent nor children -> excluded
+ assertEquals(Arrays.asList("1", "2", "3"),
+ graph.getVertices().stream().map(GenericVertex::getId).sorted().collect(Collectors.toList()));
+ assertEquals(2, graph.getEdges().size());
+
+ // Vertices carry the node linkage + label the REST consumers rely on
+ final GenericVertex switchVertex = graph.getVertex("2");
+ assertEquals("switch", switchVertex.getProperty(GenericProperties.LABEL));
+ assertEquals("2", switchVertex.getProperty(GenericProperties.NODE_ID));
+
+ // Edges are directed parent -> child
+ for (final GenericEdge edge : graph.getEdges()) {
+ if (edge.getTarget().getId().equals("2")) {
+ assertEquals("1", edge.getSource().getId());
+ } else {
+ assertEquals("2", edge.getSource().getId());
+ assertEquals("3", edge.getTarget().getId());
+ }
+ }
+ }
+
+ @Test
+ public void emptyWhenNoParentsAreConfigured() {
+ when(nodeDao.findAll()).thenReturn(Arrays.asList(node(1, "a", null), node(2, "b", null)));
+ final GenericGraph graph = provider.loadGraph().asGenericGraph();
+ assertTrue(graph.getVertices().isEmpty());
+ assertTrue(graph.getEdges().isEmpty());
+ }
+
+ @Test
+ public void emptyWhenThereAreNoNodes() {
+ when(nodeDao.findAll()).thenReturn(Collections.emptyList());
+ final GenericGraph graph = provider.loadGraph().asGenericGraph();
+ assertTrue(graph.getVertices().isEmpty());
+ assertTrue(graph.getEdges().isEmpty());
+ }
+}
diff --git a/features/graph/provider/pom.xml b/features/graph/provider/pom.xml
index 2ea9b6caf4a2..8e663d53138f 100644
--- a/features/graph/provider/pom.xml
+++ b/features/graph/provider/pom.xml
@@ -16,6 +16,7 @@
bsm
graphml
legacy
+ pathoutage
persistence-test
topology