diff --git a/container/features/pom.xml b/container/features/pom.xml
index 420582065104..e31465248a22 100644
--- a/container/features/pom.xml
+++ b/container/features/pom.xml
@@ -307,6 +307,8 @@
opennms-poller-monitors-coreopennms-telemetry-daemonopennms-timeseries-api
+ opennms-timeseries-api
+ opennms-grpc-exportervaadin
@@ -812,6 +814,12 @@
${project.version}provided
+
+ org.opennms.features.grpc
+ org.opennms.features.grpc.exporter
+ ${project.version}
+ provided
+ org.opennms.containerextender
diff --git a/container/features/src/main/resources/features.xml b/container/features/src/main/resources/features.xml
index 09f6e250214a..e7c663c0d539 100644
--- a/container/features/src/main/resources/features.xml
+++ b/container/features/src/main/resources/features.xml
@@ -2060,4 +2060,12 @@
mvn:org.opennms.features/org.opennms.features.newts/${project.version}
+
+ opennms-integration-api
+ mvn:com.google.protobuf/protobuf-java/${protobufVersion}
+ mvn:org.mapstruct/mapstruct/${mapstructVersion}
+ mvn:org.opennms.core.grpc/org.opennms.core.grpc.osgi/${project.version}
+ mvn:org.opennms.features.grpc/org.opennms.features.grpc.exporter/${project.version}
+
+
diff --git a/core/snmp/impl-joesnmp/src/test/java/org/opennms/netmgt/snmp/joesnmp/NMS16395Test.java b/core/snmp/impl-joesnmp/src/test/java/org/opennms/netmgt/snmp/joesnmp/NMS16395Test.java
index fcd18a1eb761..829178209655 100644
--- a/core/snmp/impl-joesnmp/src/test/java/org/opennms/netmgt/snmp/joesnmp/NMS16395Test.java
+++ b/core/snmp/impl-joesnmp/src/test/java/org/opennms/netmgt/snmp/joesnmp/NMS16395Test.java
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/core/snmp/impl-snmp4j/src/test/java/org/opennms/netmgt/snmp/snmp4j/NMS16395Test.java b/core/snmp/impl-snmp4j/src/test/java/org/opennms/netmgt/snmp/snmp4j/NMS16395Test.java
index 000b7b29dcd5..11ba46ef767a 100644
--- a/core/snmp/impl-snmp4j/src/test/java/org/opennms/netmgt/snmp/snmp4j/NMS16395Test.java
+++ b/core/snmp/impl-snmp4j/src/test/java/org/opennms/netmgt/snmp/snmp4j/NMS16395Test.java
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/core/snmp/proxy-rpc-impl/src/main/java/org/opennms/netmgt/snmp/proxy/common/SNMPSetBuilder.java b/core/snmp/proxy-rpc-impl/src/main/java/org/opennms/netmgt/snmp/proxy/common/SNMPSetBuilder.java
index 8cda943195b0..7156644875bd 100644
--- a/core/snmp/proxy-rpc-impl/src/main/java/org/opennms/netmgt/snmp/proxy/common/SNMPSetBuilder.java
+++ b/core/snmp/proxy-rpc-impl/src/main/java/org/opennms/netmgt/snmp/proxy/common/SNMPSetBuilder.java
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 1999-2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 1999-2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/core/snmp/proxy-rpc-impl/src/main/java/org/opennms/netmgt/snmp/proxy/common/SnmpSetRequestDTO.java b/core/snmp/proxy-rpc-impl/src/main/java/org/opennms/netmgt/snmp/proxy/common/SnmpSetRequestDTO.java
index 3d78b4f4f80b..aa890a54ac9e 100644
--- a/core/snmp/proxy-rpc-impl/src/main/java/org/opennms/netmgt/snmp/proxy/common/SnmpSetRequestDTO.java
+++ b/core/snmp/proxy-rpc-impl/src/main/java/org/opennms/netmgt/snmp/proxy/common/SnmpSetRequestDTO.java
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 1999-2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 1999-2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/debian/changelog b/debian/changelog
index d9e4dc772aec..901562fc27ca 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,12 @@
+opennms (33.1.2-1) stable; urgency=medium
+
+ * Release 33.1.2 contains a bug fix and a new feature.
+
+ For details on what has changed, see:
+ https://docs.opennms.com/horizon/33.1.2/index.html
+
+ -- OpenNMS Release Manager Tue, 07 Jan 2024 10:30:00 -0500
+
opennms (33.1.1-1) stable; urgency=medium
* Release 33.1.1 contains bug fixes, security updates and new features.
diff --git a/docs/modules/deployment/pages/minion/message-broker/kafka.adoc b/docs/modules/deployment/pages/minion/message-broker/kafka.adoc
index a1d25360de5f..76b67cacc44b 100644
--- a/docs/modules/deployment/pages/minion/message-broker/kafka.adoc
+++ b/docs/modules/deployment/pages/minion/message-broker/kafka.adoc
@@ -65,7 +65,7 @@ The following example uses SASL/SCRAM with TLS:
----
config:edit org.opennms.core.ipc.kafka
config:property-set bootstrap.servers my-kafka-ip-1:9096,my-kafka-ip-2:9096
-config:property-set security.protocol=SASL_SSL
+config:property-set security.protocol SASL_SSL
config:property-set sasl.mechanism SCRAM-SHA-512
config:property-set sasl.jaas.config 'org.apache.kafka.common.security.scram.ScramLoginModule required username="opennms-ipc" password="kafka";'
config:update
diff --git a/docs/modules/deployment/pages/minion/system-requirements.adoc b/docs/modules/deployment/pages/minion/system-requirements.adoc
index a8f9c3c1690a..b8acf14ea754 100644
--- a/docs/modules/deployment/pages/minion/system-requirements.adoc
+++ b/docs/modules/deployment/pages/minion/system-requirements.adoc
@@ -35,12 +35,11 @@ OpenNMS {page-component-title} runs on the following operating systems:
| Operating System | Supported Versions (64-bit)
| RHEL
-| {compatible-rhel7} +
-{compatible-rhel8}
+| {compatible-rhel8} +
+{compatible-rhel9}
| CentOS
-| {compatible-centos7} +
-{compatible-centos-stream}
+| {compatible-centos-stream}
ifeval::["{page-component-title}" == "Horizon"]
| Debian
diff --git a/docs/modules/deployment/pages/time-series-storage/newts/newts.adoc b/docs/modules/deployment/pages/time-series-storage/newts/newts.adoc
index 1b19961bcb72..7f96a849687a 100644
--- a/docs/modules/deployment/pages/time-series-storage/newts/newts.adoc
+++ b/docs/modules/deployment/pages/time-series-storage/newts/newts.adoc
@@ -9,65 +9,80 @@ This section describes how to configure your {page-component-title} instance to
Follow these steps to set up Newts on your {page-component-title} instance:
-. Create a configuration file with your time series database settings:
-+
+.Create a configuration file with the time series storage settings
[source, console]
+----
sudo vi etc/opennms.properties.d/timeseries.properties
+----
-. Configure the storage strategy:
-+
+.Configure Newts as the time series strategy
[source, properties]
----
-org.opennms.rrd.storeByForeignSource=true <1>
-org.opennms.timeseries.strategy=newts <2>
+# Configure storage strategy
+org.opennms.rrd.storeByForeignSource=true<1>
+org.opennms.timeseries.strategy=newts<2>
+
+# One year in seconds
+org.opennms.newts.config.ttl=31540000<3>
+
+# Seven days in seconds
+org.opennms.newts.config.resource_shard=604800<4>
+
+# Configure Newts time series storage connection
+org.opennms.newts.config.driver_settings_file=/opt/opennms/etc/cassandra-driver.conf<5>
----
-<1> Associate time series data using the foreign source and ID instead of the database-generated node ID.
-<2> Set time series strategy to use Newts.
-If you are enabling the xref:time-series-storage/timeseries/time-series-storage.adoc#ga-dual-write-newts[dual write plugin] on an existing {page-component-title} installation and you want to keep historical metrics, make sure that the written data has expired before you set `org.opennms.timeseries.strategy` to `newts`.
-. Configure the Newts time series storage connection:
-+
-[source, properties]
+<1> Associate time series data by the foreign ID instead of the database-generated Node-ID.
+<2> Set time series strategy to use `newts`.
+<3> Retention rate for the time series data.
+<4> Shard metrics every 7 days.
+<5> The path to your driver configuration file
+
+.Create a configuration file with the time series storage settings
+[source, console]
----
-org.opennms.newts.config.hostname=cassandra-ip1,cassandra-ip2 <1>
-org.opennms.newts.config.keyspace=newts <2>
-org.opennms.newts.config.port=9042 <3>
+sudo vi etc/cassandra-driver.conf
----
-<1> Host or IP addresses of the Cassandra cluster nodes.
-Can be a comma-separated list.
-<2> Name of the keyspace which is initialized and used.
-<3> Port to connect to Cassandra.
-. Set the retention rate and shard rate:
-+
+. Configure the Cassandra driver settings
[source, properties]
----
-# One year in seconds
-org.opennms.newts.config.ttl=31540000 <1>
-
-# Seven days in seconds
-org.opennms.newts.config.resource_shard=604800 <2>
+datastax-java-driver {
+ basic.contact-points = [ "example-node1:9042", "example-node2:9042" ]<1>
+ session-keyspace = "newts"<2>
+ basic.load-balancing-policy {
+ local-datacenter = datacenter1<3>
+ }
+ advanced.auth-provider {
+ class = PlainTextAuthProvider <4>
+ username = cassandra
+ password = cassandra
+ }
+}
----
-<1> Retention rate for the time series data.
-<2> Shard metrics every 7 days.
-. (Optional) If your {page-component-title} data collection or polling intervals have been modified, set the query minimum and heartbeat rates:
+<1> `Hostname:port` or `IP address:port` of one or more Cassandra cluster nodes.
+<2> Name of the keyspace which is initialized and used.
+<3> The local datacenter as defined by your Cassandra configuration
+<4> The authentication provider to use with the supplied credentials
+
+.(Optional) If your {page-component-title} data collection or polling intervals have been modified, set the query minimum and heartbeat rates:
+
[source, properties]
----
org.opennms.newts.query.minimum_step=30000 <1>
-org.opennms.newts.query.heartbeat=45000 <2>
+org.opennms.newts.query.heartbeat=450000 <2>
----
<1> The shortest collection interval configured for any collectable or pollable service, in milliseconds (in this case, 30 seconds).
<2> The communication interval for the Newts service, in milliseconds.
-Should be set to 1.5 times the `maximum` value of the collection interval configured for any collectable or pollable service, in milliseconds (in this case, 45 seconds).
+Should be set to 1.5 times the `maximum` value of the collection interval configured for any collectable or pollable service, in milliseconds (in this case, 450 seconds).
-. Initialize the Newts schema in Cassandra:
+.Initialize the Newts schema in Cassandra:
+
[source, console]
bin/newts init
-. Connect to Cassandra using the CQL shell:
+.Connect to Cassandra using the CQL shell:
+
[source, console]
----
@@ -79,7 +94,7 @@ cd ${CASSANDRA_HOME}/bin
After you have set the time series database to Newts and configured its settings, you must verify your setup and restart {page-component-title}:
-. Verify keyspace initialization:
+.Verify keyspace initialization:
+
[source, console]
----
@@ -88,7 +103,7 @@ describe table terms;
describe table samples;
----
-. Restart {page-component-title} to apply your changes and verify your configuration:
+.Restart {page-component-title} to apply your changes and verify your configuration:
+
[source, console]
systemctl restart opennms
diff --git a/docs/modules/operation/nav.adoc b/docs/modules/operation/nav.adoc
index 6f4f58aa48ab..36f5e886c336 100644
--- a/docs/modules/operation/nav.adoc
+++ b/docs/modules/operation/nav.adoc
@@ -198,6 +198,8 @@
*** xref:deep-dive/kafka-producer/configure-kafka.adoc[]
*** xref:deep-dive/kafka-producer/shell-commands.adoc[]
+** xref:deep-dive/grpc-exporter/grpc-exporter.adoc[]
+
** xref:deep-dive/alarm-correlation/situation-feedback.adoc[]
** xref:deep-dive/meta-data.adoc[]
** xref:deep-dive/search.adoc[]
diff --git a/docs/modules/operation/pages/deep-dive/grpc-exporter/grpc-exporter.adoc b/docs/modules/operation/pages/deep-dive/grpc-exporter/grpc-exporter.adoc
new file mode 100644
index 000000000000..f22cdf0148b3
--- /dev/null
+++ b/docs/modules/operation/pages/deep-dive/grpc-exporter/grpc-exporter.adoc
@@ -0,0 +1,41 @@
+= Grpc Exporter
+:description: Learn how the gRPC Exporter enables {page-component-title} to forward the status of monitored services to external applications.
+
+The Grpc Exporter feature allows {page-component-title} to forward the status of all monitored services to external applications via the gRPC protocol.
+
+These objects are encoded using link:https://developers.google.com/protocol-buffers/[Google Protocol Buffers (GPB)].
+See `monitored-services.proto` in the corresponding source distribution for the model definitions.
+
+== Configure gRPC Exporter
+
+[source, karaf]
+----
+$ ssh -p 8101 admin@localhost
+...
+admin@opennms()> config:edit org.opennms.features.grpc.exporter
+admin@opennms()> config:property-set host bsm.onmshs.local:1440 <1>
+admin@opennms()> config:property-set tls.cert.path /opt/opennms/etc/tls.cert <2>
+admin@opennms()> config:property-set tls.enabled false <3>
+admin@opennms()> config:property-set snapshot.interval 3600 false <4>
+admin@opennms()> config:update
+----
+
+<1> Set the hostname of the external gRPC application.
+<2> Configure the path to the TLS certificate.
+<3> TLS is enabled by default. For testing purposes, it can be disabled by setting this value to false.
+<4> Set the interval (in seconds) at which the complete snapshot of services will be sent to the gRPC server.
+
+== Enable gRPC Exporter
+
+Install the `opennms-grpc-exporter` feature from the same shell using:
+
+[source, karaf]
+----
+feature:install opennms-grpc-exporter
+----
+
+To ensure the feature is installed on subsequent restarts, add `opennms-grpc-exporter` to a file in featuresBoot.d:
+[source, console]
+----
+echo "opennms-grpc-exporter" | sudo tee ${OPENNMS_HOME}/etc/featuresBoot.d/grpc-exporter.boot
+----
\ No newline at end of file
diff --git a/docs/modules/releasenotes/pages/changelog.adoc b/docs/modules/releasenotes/pages/changelog.adoc
index 9b2cbf3bb43d..b8964ecaa293 100644
--- a/docs/modules/releasenotes/pages/changelog.adoc
+++ b/docs/modules/releasenotes/pages/changelog.adoc
@@ -1,6 +1,23 @@
[[release-33-changelog]]
= Changelog
+[[releasenotes-changelog-33.1.2]]
+
+== Release 33.1.2
+
+Release 33.1.2 contains a bug fix and a new feature.
+
+The codename for Horizon 33.1.2 is https://wikipedia.org/wiki/$$Cotinus$$[_Smoketree_].
+
+=== Bug
+
+* Update apache-commons-io (Issue https://issues.opennms.org/browse/NMS-16638[NMS-16638])
+* File name field in System Reports is not working (Issue https://issues.opennms.org/browse/NMS-16983[NMS-16983])
+
+=== Story
+
+* Move grpc exporter to OpenNMS repository (Issue https://issues.opennms.org/browse/NMS-16991[NMS-16991])
+
[[releasenotes-changelog-33.1.1]]
== Release 33.1.1
diff --git a/features/api-layer/dao-common/src/test/java/org/opennms/features/apilayer/utils/MonitoredServiceMapperTest.java b/features/api-layer/dao-common/src/test/java/org/opennms/features/apilayer/utils/MonitoredServiceMapperTest.java
index 7886affe230f..069055337952 100644
--- a/features/api-layer/dao-common/src/test/java/org/opennms/features/apilayer/utils/MonitoredServiceMapperTest.java
+++ b/features/api-layer/dao-common/src/test/java/org/opennms/features/apilayer/utils/MonitoredServiceMapperTest.java
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 1999-2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 1999-2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/features/datachoices/src/test/java/org/opennms/features/datachoices/internal/usagestatistics/NMS16345Test.java b/features/datachoices/src/test/java/org/opennms/features/datachoices/internal/usagestatistics/NMS16345Test.java
index 48d095e14cb8..e04179d14327 100644
--- a/features/datachoices/src/test/java/org/opennms/features/datachoices/internal/usagestatistics/NMS16345Test.java
+++ b/features/datachoices/src/test/java/org/opennms/features/datachoices/internal/usagestatistics/NMS16345Test.java
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2016-2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 1999-2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2016-2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 1999-2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/features/grpc/exporter/pom.xml b/features/grpc/exporter/pom.xml
new file mode 100644
index 000000000000..1cfd1022f20e
--- /dev/null
+++ b/features/grpc/exporter/pom.xml
@@ -0,0 +1,156 @@
+
+
+ 4.0.0
+
+ org.opennms.features
+ org.opennms.features.grpc
+ 34.0.0-SNAPSHOT
+
+
+ org.opennms.features.grpc
+ org.opennms.features.grpc.exporter
+ OpenNMS :: Features :: Grpc :: Exporter
+ bundle
+
+ 0.6.1
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ JavaSE-1.8
+ *
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ no.entur.mapstruct.spi
+ protobuf-spi-impl
+ 1.45
+
+
+
+
+ -Amapstruct.suppressGeneratorTimestamp=true
+ -Amapstruct.suppressGeneratorVersionInfoComment=true
+ -Amapstruct.verbose=true
+ -s
+ ${project.build.directory}/generated-sources-${maven.build.timestamp}
+
+
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstructVersion}
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 1.4
+
+
+ test
+ generate-sources
+
+ add-source
+
+
+
+
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ ${osMavenPluginVersion}
+
+
+
+ detect
+
+ initialize
+
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ ${protobuf-maven-plugin.version}
+
+ com.google.protobuf:protoc:${protobufVersion}:exe:${os.detected.classifier}
+ grpc-java
+ io.grpc:protoc-gen-grpc-java:${grpcVersion}:exe:${os.detected.classifier}
+
+
+
+
+ compile
+ compile-custom
+
+
+
+
+
+
+
+
+
+
+ org.osgi
+ osgi.core
+
+
+ org.apache.karaf.shell
+ org.apache.karaf.shell.core
+
+
+ org.opennms.integration.api
+ common
+
+
+ org.opennms.integration.api
+ config
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstructVersion}
+
+
+ javax.annotation
+ javax.annotation-api
+ ${javaxAnnotationApiVersion}
+
+
+ org.opennms.core.grpc
+ org.opennms.core.grpc.osgi
+ ${project.version}
+
+
+ com.google.protobuf
+ protobuf-java
+ ${protobufVersion}
+
+
+
diff --git a/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/Callback.java b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/Callback.java
new file mode 100644
index 000000000000..17cf3b4576f2
--- /dev/null
+++ b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/Callback.java
@@ -0,0 +1,28 @@
+/*
+ * 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.features.grpc.exporter;
+
+public interface Callback {
+
+ void sendInventorySnapShot();
+}
diff --git a/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/GrpcExporterClient.java b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/GrpcExporterClient.java
new file mode 100644
index 000000000000..abf4407f1f92
--- /dev/null
+++ b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/GrpcExporterClient.java
@@ -0,0 +1,213 @@
+/*
+ * 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.features.grpc.exporter;
+
+import com.google.protobuf.Empty;
+import io.grpc.ConnectivityState;
+import io.grpc.ManagedChannel;
+import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
+import io.grpc.stub.StreamObserver;
+import org.opennms.plugin.grpc.proto.services.InventoryUpdateList;
+import org.opennms.plugin.grpc.proto.services.ServiceSyncGrpc;
+import org.opennms.plugin.grpc.proto.services.StateUpdateList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.net.ssl.SSLException;
+import java.io.File;
+import java.util.Objects;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+
+public class GrpcExporterClient {
+ private static final Logger LOG = LoggerFactory.getLogger(GrpcExporterClient.class);
+
+ public static final String FOREIGN_TYPE = "OpenNMS";
+
+ private final String host;
+ private final Integer port;
+ private final String tlsCertPath;
+
+ private final boolean tlsEnabled;
+ private ManagedChannel channel;
+
+ private ServiceSyncGrpc.ServiceSyncStub monitoredServiceSyncStub;
+
+ private StreamObserver inventoryUpdateStream;
+ private StreamObserver stateUpdateStream;
+ private ScheduledExecutorService scheduler;
+ private final AtomicBoolean reconnecting = new AtomicBoolean(false);
+ private final AtomicBoolean stopped = new AtomicBoolean(false);
+ private Callback inventoryCallback;
+
+ public GrpcExporterClient(final String host,
+ final Integer port,
+ final String tlsCertPath,
+ final boolean tlsEnabled) {
+ this.host = Objects.requireNonNull(host);
+ this.port = Objects.requireNonNull(port);
+ this.tlsCertPath = tlsCertPath;
+ this.tlsEnabled = tlsEnabled;
+ this.scheduler = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("grpc-exporter-connect"));
+ }
+
+ public void start() throws SSLException {
+ final NettyChannelBuilder channelBuilder;
+ if (this.port > 0) {
+ channelBuilder = NettyChannelBuilder.forAddress(this.host, this.port)
+ .keepAliveWithoutCalls(true);
+ } else {
+ channelBuilder = NettyChannelBuilder.forTarget(this.host)
+ .keepAliveWithoutCalls(true);
+ }
+
+ if (tlsEnabled && tlsCertPath != null && !tlsCertPath.isBlank()) {
+ channel = channelBuilder.useTransportSecurity()
+ .sslContext(GrpcSslContexts.forClient()
+ .trustManager(new File(tlsCertPath)).build())
+ .build();
+ LOG.info("TLS enabled with cert at {}", tlsCertPath);
+ } else if (tlsEnabled) {
+ // Use system store specified in javax.net.ssl.trustStore
+ channel = channelBuilder.useTransportSecurity()
+ .build();
+ LOG.info("TLS enabled with certs from system store");
+ } else {
+ channel = channelBuilder.usePlaintext()
+ .build();
+ LOG.info("TLS disabled, using plain text");
+ }
+
+ this.monitoredServiceSyncStub = ServiceSyncGrpc.newStub(this.channel);
+ connectStreams();
+ LOG.info("GrpcExporterClient started connection to {}:{}", this.host, this.port);
+ }
+
+ public void stop() {
+ if (scheduler != null) {
+ scheduler.shutdownNow();
+ }
+ if (channel != null) {
+ channel.shutdownNow();
+ }
+ stopped.set(true);
+ LOG.info("GrpcExporterClient stopped");
+ }
+
+ public void setInventoryCallback(Callback inventoryCallback) {
+ this.inventoryCallback = inventoryCallback;
+ }
+
+ private synchronized void initializeStreams() {
+ if (getChannelState().equals(ConnectivityState.READY)) {
+ try {
+ this.inventoryUpdateStream =
+ this.monitoredServiceSyncStub.inventoryUpdate(new LoggingAckReceiver("monitored_service_inventory_update", this));
+ this.stateUpdateStream =
+ this.monitoredServiceSyncStub.stateUpdate(new LoggingAckReceiver("monitored_service_state_update", this));
+ this.scheduler.shutdown();
+ this.scheduler = null;
+ LOG.info("Streams initialized successfully.");
+ reconnecting.set(false);
+ // While connecting, reconnecting, send callback to inventory service.
+ if (inventoryCallback != null) {
+ inventoryCallback.sendInventorySnapShot();
+ }
+ } catch (Exception e) {
+ LOG.error("Failed to initialize streams", e);
+ }
+ } else {
+ LOG.info("Channel state is not READY, retrying...");
+ }
+ }
+
+ private void connectStreams() {
+ // Schedule connect streams immediately and 30 secs thereafter.
+ scheduler.scheduleAtFixedRate(this::initializeStreams, 30, 30, TimeUnit.SECONDS);
+ }
+
+ private synchronized void reconnectStreams() {
+ // Multiple streams may try to reconnect but should end up using one scheduler which schedules connection after a delay of 30 secs.
+ if (reconnecting.compareAndSet(false, true) && !stopped.get()) {
+ if (scheduler == null || scheduler.isShutdown()) {
+ scheduler = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("grpc-exporter-reconnect"));
+ scheduler.scheduleAtFixedRate(this::initializeStreams, 30, 30, TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ ConnectivityState getChannelState() {
+ return channel.getState(true);
+ }
+
+ public void sendMonitoredServicesInventoryUpdate(final InventoryUpdateList inventoryUpdates) {
+ if (inventoryUpdateStream != null) {
+ this.inventoryUpdateStream.onNext(inventoryUpdates);
+ LOG.info("Sent an monitored service inventory update with {} services", inventoryUpdates.getServicesCount());
+ } else {
+ LOG.warn("Unable to send inventory snapshot since channel is not ready yet");
+ }
+ }
+
+ public void sendMonitoredServicesStatusUpdate(final StateUpdateList stateUpdates) {
+ if (this.stateUpdateStream != null) {
+ this.stateUpdateStream.onNext(stateUpdates);
+ LOG.info("Sent an monitored service state update with {} services", stateUpdates.getUpdatesCount());
+ } else {
+ LOG.warn("Unable to send monitored service status update since channel is not ready yet");
+ }
+ }
+
+ private static class LoggingAckReceiver implements StreamObserver {
+
+ private final String type;
+
+ private final GrpcExporterClient client;
+
+ private LoggingAckReceiver(final String type, GrpcExporterClient client) {
+ this.type = Objects.requireNonNull(type);
+ this.client = client;
+ }
+
+ @Override
+ public void onNext(final Empty value) {
+ LOG.debug("Received ACK {}", this.type);
+ }
+
+ @Override
+ public void onError(final Throwable t) {
+ LOG.error("Received error {}", this.type, t);
+ client.reconnectStreams();
+ }
+
+ @Override
+ public void onCompleted() {
+ LOG.info("Completed {}", this.type);
+ client.reconnectStreams();
+ }
+ }
+}
diff --git a/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/InventoryService.java b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/InventoryService.java
new file mode 100644
index 000000000000..939591dbac88
--- /dev/null
+++ b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/InventoryService.java
@@ -0,0 +1,103 @@
+/*
+ * 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.features.grpc.exporter;
+
+import org.opennms.features.grpc.exporter.common.MonitoredServiceWithMetadata;
+import org.opennms.features.grpc.exporter.mapper.MonitoredServiceMapper;
+import org.opennms.integration.api.v1.dao.NodeDao;
+import org.opennms.integration.api.v1.runtime.RuntimeInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+public class InventoryService {
+ private static final Logger LOG = LoggerFactory.getLogger(InventoryService.class);
+
+ private final NodeDao nodeDao;
+
+ private final RuntimeInfo runtimeInfo;
+
+ private final GrpcExporterClient client;
+
+ private final Duration snapshotInterval;
+
+ private final ScheduledExecutorService scheduler;
+
+ public InventoryService(final NodeDao nodeDao,
+ final RuntimeInfo runtimeInfo,
+ final GrpcExporterClient client,
+ final Duration snapshotInterval) {
+ this.nodeDao = Objects.requireNonNull(nodeDao);
+ this.runtimeInfo = Objects.requireNonNull(runtimeInfo);
+ this.client = Objects.requireNonNull(client);
+ this.snapshotInterval = Objects.requireNonNull(snapshotInterval);
+ this.scheduler = Executors.newSingleThreadScheduledExecutor(
+ new NamedThreadFactory("inventory-service-snapshot-sender"));
+ }
+
+ public InventoryService(final NodeDao nodeDao,
+ final RuntimeInfo runtimeInfo,
+ final GrpcExporterClient client,
+ final long snapshotInterval) {
+ this(nodeDao, runtimeInfo, client, Duration.ofSeconds(snapshotInterval));
+ }
+
+ public void start() {
+ // Start timer to send snapshots
+ this.scheduler.scheduleAtFixedRate(this::sendSnapshot,
+ this.snapshotInterval.getSeconds(),
+ this.snapshotInterval.getSeconds(),
+ TimeUnit.SECONDS);
+ // Set this callback to send snapshot for initial server connect and reconnects
+ this.client.setInventoryCallback(this::sendSnapshot);
+ }
+
+ public void stop() {
+ this.scheduler.shutdown();
+ }
+
+ public void sendAddService(final MonitoredServiceWithMetadata service) {
+ final var inventory = MonitoredServiceMapper.INSTANCE.toInventoryUpdates(List.of(service), this.runtimeInfo, false);
+ this.client.sendMonitoredServicesInventoryUpdate(inventory);
+ }
+
+ public void sendSnapshot() {
+ final var services = this.nodeDao.getNodes().stream()
+ .flatMap(node -> node.getIpInterfaces().stream()
+ .flatMap(iface -> iface.getMonitoredServices().stream()
+ .map(service -> new MonitoredServiceWithMetadata(node, iface, service))))
+ .collect(Collectors.toList());
+
+ LOG.debug("Send snapshot: services={}", services.size());
+
+ final var inventory = MonitoredServiceMapper.INSTANCE.toInventoryUpdates(services, this.runtimeInfo, true);
+ this.client.sendMonitoredServicesInventoryUpdate(inventory);
+ }
+}
diff --git a/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/NamedThreadFactory.java b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/NamedThreadFactory.java
new file mode 100644
index 000000000000..2ebf1c0db976
--- /dev/null
+++ b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/NamedThreadFactory.java
@@ -0,0 +1,44 @@
+/*
+ * 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.features.grpc.exporter;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class NamedThreadFactory implements ThreadFactory {
+
+ private final String namePrefix;
+ private final AtomicInteger threadNumber = new AtomicInteger(1);
+
+ public NamedThreadFactory(String namePrefix) {
+ this.namePrefix = namePrefix;
+ }
+
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r, namePrefix + "-" + threadNumber.getAndIncrement());
+ t.setDaemon(false);
+ return t;
+ }
+}
diff --git a/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/StateService.java b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/StateService.java
new file mode 100644
index 000000000000..61f07dba86d5
--- /dev/null
+++ b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/StateService.java
@@ -0,0 +1,67 @@
+/*
+ * 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.features.grpc.exporter;
+
+import org.opennms.features.grpc.exporter.common.MonitoredServiceWithMetadata;
+import org.opennms.features.grpc.exporter.mapper.MonitoredServiceMapper;
+import org.opennms.integration.api.v1.dao.NodeDao;
+import org.opennms.integration.api.v1.runtime.RuntimeInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class StateService {
+ private static final Logger LOG = LoggerFactory.getLogger(StateService.class);
+
+ private final NodeDao nodeDao;
+
+ private final RuntimeInfo runtimeInfo;
+
+ private final GrpcExporterClient client;
+
+ public StateService(final NodeDao nodeDao,
+ final RuntimeInfo runtimeInfo,
+ final GrpcExporterClient client) {
+ this.nodeDao = Objects.requireNonNull(nodeDao);
+ this.runtimeInfo = Objects.requireNonNull(runtimeInfo);
+ this.client = Objects.requireNonNull(client);
+ }
+
+ public void sendState(final List services) {
+ final var updates = MonitoredServiceMapper.INSTANCE.toStateUpdates(services, this.runtimeInfo);
+ this.client.sendMonitoredServicesStatusUpdate(updates);
+ }
+
+ public void sendAllState() {
+ final var services = this.nodeDao.getNodes().stream()
+ .flatMap(node -> node.getIpInterfaces().stream()
+ .flatMap(iface -> iface.getMonitoredServices().stream()
+ .map(service -> new MonitoredServiceWithMetadata(node, iface, service))))
+ .collect(Collectors.toList());
+
+ this.sendState(services);
+ }
+}
diff --git a/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/commands/InventorySnapshot.java b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/commands/InventorySnapshot.java
new file mode 100644
index 000000000000..3931c5aab52a
--- /dev/null
+++ b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/commands/InventorySnapshot.java
@@ -0,0 +1,43 @@
+/*
+ * 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.features.grpc.exporter.commands;
+
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.opennms.features.grpc.exporter.InventoryService;
+
+@Command(scope = "opennms", name = "grpc-exporter-send-inventory-snapshot", description = "Send an inventory snapshot")
+@Service
+public class InventorySnapshot implements Action {
+
+ @Reference
+ private InventoryService inventoryService;
+
+ @Override
+ public Object execute() {
+ this.inventoryService.sendSnapshot();
+ return null;
+ }
+}
diff --git a/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/commands/State.java b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/commands/State.java
new file mode 100644
index 000000000000..83b9dedd0660
--- /dev/null
+++ b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/commands/State.java
@@ -0,0 +1,43 @@
+/*
+ * 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.features.grpc.exporter.commands;
+
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.opennms.features.grpc.exporter.StateService;
+
+@Command(scope = "opennms", name = "grpc-exporter-send-state", description = "Send all current monitor service states")
+@Service
+public class State implements Action {
+
+ @Reference
+ private StateService stateService;
+
+ @Override
+ public Object execute() {
+ this.stateService.sendAllState();
+ return null;
+ }
+}
diff --git a/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/common/MonitoredServiceWithMetadata.java b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/common/MonitoredServiceWithMetadata.java
new file mode 100644
index 000000000000..4bce6f6e57df
--- /dev/null
+++ b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/common/MonitoredServiceWithMetadata.java
@@ -0,0 +1,56 @@
+/*
+ * 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.features.grpc.exporter.common;
+
+import org.opennms.integration.api.v1.model.IpInterface;
+import org.opennms.integration.api.v1.model.MonitoredService;
+import org.opennms.integration.api.v1.model.Node;
+
+import java.util.Objects;
+
+public class MonitoredServiceWithMetadata {
+
+ private final Node node;
+ private final IpInterface iface;
+ private final MonitoredService monitoredService;
+
+ public MonitoredServiceWithMetadata(final Node node,
+ final IpInterface iface,
+ final MonitoredService monitoredService) {
+ this.node = Objects.requireNonNull(node);
+ this.iface = Objects.requireNonNull(iface);
+ this.monitoredService = Objects.requireNonNull(monitoredService);
+ }
+
+ public Node getNode() {
+ return this.node;
+ }
+
+ public IpInterface getIface() {
+ return this.iface;
+ }
+
+ public MonitoredService getMonitoredService() {
+ return this.monitoredService;
+ }
+}
diff --git a/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/events/EventConstants.java b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/events/EventConstants.java
new file mode 100644
index 000000000000..d71205f702b2
--- /dev/null
+++ b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/events/EventConstants.java
@@ -0,0 +1,107 @@
+/*
+ * 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.features.grpc.exporter.events;
+
+// This has a copy of some of the events that deal with a service down/up.
+public class EventConstants {
+
+ /**
+ * The node gained service event UEI.
+ */
+ public static final String NODE_GAINED_SERVICE_EVENT_UEI = "uei.opennms.org/nodes/nodeGainedService";
+
+ /**
+ * The node regained service event UEI.
+ */
+ public static final String NODE_REGAINED_SERVICE_EVENT_UEI = "uei.opennms.org/nodes/nodeRegainedService";
+
+ /**
+ * The node lost service event UEI.
+ */
+ public static final String NODE_LOST_SERVICE_EVENT_UEI = "uei.opennms.org/nodes/nodeLostService";
+
+
+ /**
+ * The node down event UEI.
+ */
+ public static final String NODE_DOWN_EVENT_UEI = "uei.opennms.org/nodes/nodeDown";
+
+
+ /**
+ * The node up event UEI.
+ */
+ public static final String NODE_UP_EVENT_UEI = "uei.opennms.org/nodes/nodeUp";
+
+ /**
+ * The interface up event UEI.
+ */
+ public static final String INTERFACE_UP_EVENT_UEI = "uei.opennms.org/nodes/interfaceUp";
+
+
+ /**
+ * The interface down event UEI.
+ */
+ public static final String INTERFACE_DOWN_EVENT_UEI = "uei.opennms.org/nodes/interfaceDown";
+
+ /**
+ * The node gained interface event UEI.
+ */
+ public static final String NODE_GAINED_INTERFACE_EVENT_UEI = "uei.opennms.org/nodes/nodeGainedInterface";
+
+
+ /**
+ * The service deleted event UEI.
+ */
+ public static final String SERVICE_DELETED_EVENT_UEI = "uei.opennms.org/nodes/serviceDeleted";
+
+ /**
+ * The interface deleted event UEI.
+ */
+ public static final String INTERFACE_DELETED_EVENT_UEI = "uei.opennms.org/nodes/interfaceDeleted";
+
+ /**
+ * The node deleted event UEI.
+ */
+ public static final String NODE_DELETED_EVENT_UEI = "uei.opennms.org/nodes/nodeDeleted";
+
+ /**
+ * The node added event UEI.
+ */
+ public static final String NODE_ADDED_EVENT_UEI = "uei.opennms.org/nodes/nodeAdded";
+
+ /**
+ * The suspend polling service event UEI.
+ */
+ public static final String SUSPEND_POLLING_SERVICE_EVENT_UEI = "uei.opennms.org/internal/poller/suspendPollingService";
+
+ /**
+ * The resume polling service event UEI.
+ */
+ public static final String RESUME_POLLING_SERVICE_EVENT_UEI = "uei.opennms.org/internal/poller/resumePollingService";
+
+ public static final String SERVICE_RESPONSIVE_EVENT_UEI = "uei.opennms.org/nodes/serviceResponsive";
+ public static final String SERVICE_UNRESPONSIVE_EVENT_UEI = "uei.opennms.org/nodes/serviceUnresponsive";
+ public static final String SERVICE_UNMANAGED_EVENT_UEI = "uei.opennms.org/nodes/serviceUnmanaged";
+
+ public static final String INTERFACE_REPARENTED_EVENT_UEI = "uei.opennms.org/nodes/interfaceReparented";
+}
diff --git a/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/events/InventoryEventHandler.java b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/events/InventoryEventHandler.java
new file mode 100644
index 000000000000..7db7d4866934
--- /dev/null
+++ b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/events/InventoryEventHandler.java
@@ -0,0 +1,121 @@
+/*
+ * 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.features.grpc.exporter.events;
+
+import org.opennms.features.grpc.exporter.InventoryService;
+import org.opennms.features.grpc.exporter.common.MonitoredServiceWithMetadata;
+import org.opennms.integration.api.v1.dao.NodeDao;
+import org.opennms.integration.api.v1.events.EventListener;
+import org.opennms.integration.api.v1.events.EventSubscriptionService;
+import org.opennms.integration.api.v1.model.InMemoryEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Objects;
+
+public class InventoryEventHandler implements EventListener {
+ private static final Logger LOG = LoggerFactory.getLogger(InventoryEventHandler.class);
+
+ private final EventSubscriptionService eventSubscriptionService;
+
+ private final NodeDao nodeDao;
+
+ private final InventoryService inventoryService;
+
+ public InventoryEventHandler(final EventSubscriptionService eventSubscriptionService,
+ final NodeDao nodeDao,
+ final InventoryService inventoryService) {
+ this.eventSubscriptionService = Objects.requireNonNull(eventSubscriptionService);
+ this.nodeDao = Objects.requireNonNull(nodeDao);
+ this.inventoryService = Objects.requireNonNull(inventoryService);
+ }
+
+ public void start() {
+ this.eventSubscriptionService.addEventListener(this, List.of(
+ // Events related to a service
+ EventConstants.NODE_GAINED_SERVICE_EVENT_UEI,
+ EventConstants.SERVICE_DELETED_EVENT_UEI,
+
+ //Events related to an interface
+ EventConstants.INTERFACE_DELETED_EVENT_UEI,
+ EventConstants.INTERFACE_REPARENTED_EVENT_UEI,
+
+ //Events related to a node
+ EventConstants.NODE_DELETED_EVENT_UEI
+ ));
+ }
+
+ public void stop() {
+ this.eventSubscriptionService.removeEventListener(this);
+ }
+
+ @Override
+ public String getName() {
+ return InventoryEventHandler.class.getName();
+ }
+
+ @Override
+ public int getNumThreads() {
+ return 1;
+ }
+
+ @Override
+ public void onEvent(final InMemoryEvent event) {
+ LOG.debug("Got event: {}", event);
+
+ switch (event.getUei()) {
+ case EventConstants.NODE_GAINED_SERVICE_EVENT_UEI:
+ if (event.getNodeId() == null || event.getInterface() == null || event.getService() == null) {
+ return;
+ }
+
+ final var node = this.nodeDao.getNodeById(event.getNodeId());
+ if (node == null) {
+ return;
+ }
+
+ final var iface = node.getInterfaceByIp(event.getInterface()).orElse(null);
+ if (iface == null) {
+ return;
+ }
+
+ final var service = iface.getMonitoredService(event.getService()).orElse(null);
+ if (service == null) {
+ return;
+ }
+
+ LOG.debug("New service: {}/{}/{}", node.getId(), iface.getIpAddress().getHostAddress(), service.getName());
+
+ this.inventoryService.sendAddService(new MonitoredServiceWithMetadata(node, iface, service));
+ break;
+
+ case EventConstants.SERVICE_DELETED_EVENT_UEI:
+ // There is not much we can do here for now
+
+ default:
+ this.inventoryService.sendSnapshot();
+ break;
+ }
+ }
+}
diff --git a/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/events/StateEventHandler.java b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/events/StateEventHandler.java
new file mode 100644
index 000000000000..3f7af82a2278
--- /dev/null
+++ b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/events/StateEventHandler.java
@@ -0,0 +1,113 @@
+/*
+ * 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.features.grpc.exporter.events;
+
+import org.opennms.features.grpc.exporter.StateService;
+import org.opennms.features.grpc.exporter.common.MonitoredServiceWithMetadata;
+import org.opennms.integration.api.v1.dao.NodeDao;
+import org.opennms.integration.api.v1.events.EventListener;
+import org.opennms.integration.api.v1.events.EventSubscriptionService;
+import org.opennms.integration.api.v1.model.InMemoryEvent;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class StateEventHandler implements EventListener {
+ private static final Logger LOG = LoggerFactory.getLogger(StateEventHandler.class);
+
+ private final EventSubscriptionService eventSubscriptionService;
+
+ private final NodeDao nodeDao;
+
+ private final StateService stateService;
+
+ public StateEventHandler(final EventSubscriptionService eventSubscriptionService,
+ final NodeDao nodeDao,
+ final StateService stateService) {
+ this.eventSubscriptionService = Objects.requireNonNull(eventSubscriptionService);
+ this.nodeDao = Objects.requireNonNull(nodeDao);
+ this.stateService = Objects.requireNonNull(stateService);
+ }
+
+ public void start() {
+ this.eventSubscriptionService.addEventListener(this, List.of(
+ EventConstants.SERVICE_RESPONSIVE_EVENT_UEI,
+ EventConstants.SERVICE_UNRESPONSIVE_EVENT_UEI,
+ EventConstants.SERVICE_UNMANAGED_EVENT_UEI,
+
+ EventConstants.NODE_REGAINED_SERVICE_EVENT_UEI,
+ EventConstants.NODE_LOST_SERVICE_EVENT_UEI,
+
+ EventConstants.INTERFACE_UP_EVENT_UEI,
+ EventConstants.INTERFACE_DOWN_EVENT_UEI,
+
+ EventConstants.NODE_UP_EVENT_UEI,
+ EventConstants.NODE_DOWN_EVENT_UEI
+ ));
+ }
+
+ public void stop() {
+ this.eventSubscriptionService.removeEventListener(this);
+ }
+
+ @Override
+ public String getName() {
+ return StateEventHandler.class.getName();
+ }
+
+ @Override
+ public int getNumThreads() {
+ return 1;
+ }
+
+ @Override
+ public void onEvent(final InMemoryEvent event) {
+ LOG.debug("Got event: {}", event);
+
+ if (event.getNodeId() == null) {
+ return;
+ }
+
+ final var node = nodeDao.getNodeById(event.getNodeId());
+ if (node == null) {
+ return;
+ }
+
+ final var interfaces = event.getInterface() != null
+ ? node.getInterfaceByIp(event.getInterface()).stream()
+ : node.getIpInterfaces().stream();
+
+ final var services = interfaces
+ .flatMap(iface -> (event.getService() != null
+ ? iface.getMonitoredService(event.getService()).stream()
+ : iface.getMonitoredServices().stream()
+ ).map(service -> new MonitoredServiceWithMetadata(node, iface, service)))
+ .collect(Collectors.toList());
+
+ this.stateService.sendState(services);
+ }
+}
diff --git a/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/mapper/MonitoredServiceMapper.java b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/mapper/MonitoredServiceMapper.java
new file mode 100644
index 000000000000..4f1b929de6a9
--- /dev/null
+++ b/features/grpc/exporter/src/main/java/org/opennms/features/grpc/exporter/mapper/MonitoredServiceMapper.java
@@ -0,0 +1,97 @@
+/*
+ * 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.features.grpc.exporter.mapper;
+
+import org.mapstruct.CollectionMappingStrategy;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Named;
+import org.mapstruct.NullValueCheckStrategy;
+import org.mapstruct.factory.Mappers;
+import org.opennms.features.grpc.exporter.GrpcExporterClient;
+import org.opennms.features.grpc.exporter.common.MonitoredServiceWithMetadata;
+import org.opennms.integration.api.v1.runtime.RuntimeInfo;
+import org.opennms.plugin.grpc.proto.services.InventoryUpdateList;
+import org.opennms.plugin.grpc.proto.services.ServiceComponent;
+import org.opennms.plugin.grpc.proto.services.StateUpdate;
+import org.opennms.plugin.grpc.proto.services.StateUpdateList;
+
+import java.util.List;
+import java.util.Map;
+
+@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
+ nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
+public interface MonitoredServiceMapper {
+ MonitoredServiceMapper INSTANCE = Mappers.getMapper(MonitoredServiceMapper.class);
+
+ @Named("foreignService")
+ default String foreignService(final MonitoredServiceWithMetadata service) {
+ return String.format("%s/%s/%s/%s",
+ service.getNode().getForeignSource(),
+ service.getNode().getForeignId(),
+ service.getIface().getIpAddress().getHostAddress(),
+ service.getMonitoredService().getName());
+ }
+
+ @Named("displayName")
+ default String displayName(final MonitoredServiceWithMetadata service) {
+ return String.format("%s %s %s",
+ service.getNode().getLabel(),
+ service.getIface().getIpAddress().getHostAddress(),
+ service.getMonitoredService().getName());
+ }
+
+ @Named("attributes")
+ default Map attributes(final MonitoredServiceWithMetadata service) {
+ return Map.ofEntries(
+ Map.entry("nodeId", Integer.toString(service.getNode().getId())),
+ Map.entry("nodeLabel", service.getNode().getLabel()),
+ Map.entry("location", service.getNode().getLocation()),
+ Map.entry("nodeCriteria", String.format("%s:%s", service.getNode().getForeignSource(), service.getNode().getForeignId())),
+ Map.entry("ipAddress", service.getIface().getIpAddress().getHostAddress()),
+ Map.entry("serviceName", service.getMonitoredService().getName())
+ );
+ }
+
+ @Mapping(target = "foreignService", source = "service", qualifiedByName = "foreignService")
+ @Mapping(target = "name", source = "service", qualifiedByName = "displayName")
+ @Mapping(target = "healthy", source = "service.monitoredService.status")
+ @Mapping(target = "attributes", source = "service", qualifiedByName = "attributes")
+ @Mapping(target = "tags", source = "service.node.categories")
+ ServiceComponent toInventoryUpdate(final MonitoredServiceWithMetadata service);
+
+ @Mapping(target = "foreignType", constant = GrpcExporterClient.FOREIGN_TYPE)
+ @Mapping(target = "foreignSource", source = "runtimeInfo.systemId")
+ @Mapping(target = "services", source = "services")
+ @Mapping(target = "snapshot", source = "snapshot")
+ InventoryUpdateList toInventoryUpdates(final List services, final RuntimeInfo runtimeInfo, final boolean snapshot);
+
+ @Mapping(target = "foreignService", source = "service", qualifiedByName = "foreignService")
+ @Mapping(target = "healthy", source = "service.monitoredService.status")
+ StateUpdate toStateUpdate(final MonitoredServiceWithMetadata service);
+
+ @Mapping(target = "foreignType", constant = GrpcExporterClient.FOREIGN_TYPE)
+ @Mapping(target = "foreignSource", source = "runtimeInfo.systemId")
+ @Mapping(target = "updates", source = "updates")
+ StateUpdateList toStateUpdates(final List updates, final RuntimeInfo runtimeInfo);
+}
diff --git a/features/grpc/exporter/src/main/proto/monitored-services.proto b/features/grpc/exporter/src/main/proto/monitored-services.proto
new file mode 100644
index 000000000000..931f127a2c1f
--- /dev/null
+++ b/features/grpc/exporter/src/main/proto/monitored-services.proto
@@ -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.
+ */
+
+syntax = "proto3";
+
+import "google/protobuf/empty.proto";
+
+package org.opennms.plugin.grpc.proto.services;
+
+option java_multiple_files = true;
+option java_package = "org.opennms.plugin.grpc.proto.services";
+
+message ServiceComponent {
+ string foreign_service = 1;
+
+ string name = 2;
+
+ bool healthy = 3;
+
+ map attributes = 4;
+ repeated string tags = 5;
+}
+
+message InventoryUpdateList {
+ string foreign_type = 1;
+ string foreign_source = 2;
+
+ bool snapshot = 3;
+
+ repeated ServiceComponent services = 4;
+}
+
+message StateUpdate {
+ string foreign_service = 1;
+
+ bool healthy = 2;
+}
+
+message StateUpdateList {
+ string foreign_type = 1;
+ string foreign_source = 2;
+
+ repeated StateUpdate updates = 3;
+}
+
+service ServiceSync {
+ rpc InventoryUpdate(stream InventoryUpdateList) returns (stream google.protobuf.Empty) {}
+ rpc StateUpdate(stream StateUpdateList) returns (stream google.protobuf.Empty) {}
+}
diff --git a/features/grpc/exporter/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/features/grpc/exporter/src/main/resources/OSGI-INF/blueprint/blueprint.xml
new file mode 100644
index 000000000000..cc32c85d2858
--- /dev/null
+++ b/features/grpc/exporter/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/features/grpc/pom.xml b/features/grpc/pom.xml
new file mode 100644
index 000000000000..bc227b3fc16d
--- /dev/null
+++ b/features/grpc/pom.xml
@@ -0,0 +1,20 @@
+
+
+ 4.0.0
+
+ org.opennms
+ org.opennms.features
+ 34.0.0-SNAPSHOT
+
+
+ org.opennms.features
+ org.opennms.features.grpc
+ pom
+ OpenNMS :: Features :: Grpc
+
+
+ exporter
+
+
diff --git a/features/poller/monitors/core/src/main/java/org/opennms/netmgt/poller/monitors/PtpMonitor.java b/features/poller/monitors/core/src/main/java/org/opennms/netmgt/poller/monitors/PtpMonitor.java
index 795f07077b34..2e57764a0249 100644
--- a/features/poller/monitors/core/src/main/java/org/opennms/netmgt/poller/monitors/PtpMonitor.java
+++ b/features/poller/monitors/core/src/main/java/org/opennms/netmgt/poller/monitors/PtpMonitor.java
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 1999-2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 1999-2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/features/poller/monitors/core/src/test/java/org/opennms/netmgt/poller/monitors/PtpMonitorTest.java b/features/poller/monitors/core/src/test/java/org/opennms/netmgt/poller/monitors/PtpMonitorTest.java
index 29cbca4557aa..996dc7b54c97 100644
--- a/features/poller/monitors/core/src/test/java/org/opennms/netmgt/poller/monitors/PtpMonitorTest.java
+++ b/features/poller/monitors/core/src/test/java/org/opennms/netmgt/poller/monitors/PtpMonitorTest.java
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 1999-2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 1999-2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/features/pom.xml b/features/pom.xml
index 7d27831765e3..89ffd007e182 100644
--- a/features/pom.xml
+++ b/features/pom.xml
@@ -191,5 +191,7 @@
usageanalyticsinmemory-timeseries-plugin
+
+ grpc
diff --git a/features/springframework-security/src/test/java/org/opennms/web/springframework/security/NMS16450Test.java b/features/springframework-security/src/test/java/org/opennms/web/springframework/security/NMS16450Test.java
index 2bc1ca49c990..44f46aac7802 100644
--- a/features/springframework-security/src/test/java/org/opennms/web/springframework/security/NMS16450Test.java
+++ b/features/springframework-security/src/test/java/org/opennms/web/springframework/security/NMS16450Test.java
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 1999-2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 1999-2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/features/telemetry/protocols/openconfig/adapter/src/test/resources/openconfig-gnmi-telemetry.groovy b/features/telemetry/protocols/openconfig/adapter/src/test/resources/openconfig-gnmi-telemetry.groovy
index 38c73ec4866a..466e8221fe18 100644
--- a/features/telemetry/protocols/openconfig/adapter/src/test/resources/openconfig-gnmi-telemetry.groovy
+++ b/features/telemetry/protocols/openconfig/adapter/src/test/resources/openconfig-gnmi-telemetry.groovy
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 1999-2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 1999-2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/features/vaadin-snmp-events-and-metrics/src/test/java/org/opennms/features/vaadin/mibcompiler/NMS16444Test.java b/features/vaadin-snmp-events-and-metrics/src/test/java/org/opennms/features/vaadin/mibcompiler/NMS16444Test.java
index e9dcc6437241..426593357bab 100644
--- a/features/vaadin-snmp-events-and-metrics/src/test/java/org/opennms/features/vaadin/mibcompiler/NMS16444Test.java
+++ b/features/vaadin-snmp-events-and-metrics/src/test/java/org/opennms/features/vaadin/mibcompiler/NMS16444Test.java
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 1999-2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 1999-2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/opennms-assemblies/minion/src/main/filtered/debian/changelog b/opennms-assemblies/minion/src/main/filtered/debian/changelog
index 7d7cacd9289f..4c3bd073125a 100644
--- a/opennms-assemblies/minion/src/main/filtered/debian/changelog
+++ b/opennms-assemblies/minion/src/main/filtered/debian/changelog
@@ -1,3 +1,12 @@
+opennms-minion (33.1.2-1) stable; urgency=medium
+
+ * Release 33.1.2 contains a bug fix and a new feature.
+
+ For details on what has changed, see:
+ https://docs.opennms.com/horizon/33.1.2/index.html
+
+ -- OpenNMS Release Manager Tue, 07 Jan 2024 10:30:00 -0500
+
opennms-minion (33.1.1-1) stable; urgency=medium
* Release 33.1.1 contains bug fixes, security updates and new features.
diff --git a/opennms-assemblies/sentinel/src/main/filtered/debian/changelog b/opennms-assemblies/sentinel/src/main/filtered/debian/changelog
index abea928e995f..f5f6d3c7def9 100644
--- a/opennms-assemblies/sentinel/src/main/filtered/debian/changelog
+++ b/opennms-assemblies/sentinel/src/main/filtered/debian/changelog
@@ -1,3 +1,12 @@
+opennms-sentinel (33.1.2-1) stable; urgency=medium
+
+ * Release 33.1.2 contains a bug fix and a new feature.
+
+ For details on what has changed, see:
+ https://docs.opennms.com/horizon/33.1.2/index.html
+
+ -- OpenNMS Release Manager Tue, 07 Jan 2024 10:30:00 -0500
+
opennms-sentinel (33.1.1-1) stable; urgency=medium
* Release 33.1.1 contains bug fixes, security updates and new features.
diff --git a/opennms-base-assembly/src/main/filtered/etc/snmp-graph.properties.d/wsman-microsoft-windows-virtmem.properties b/opennms-base-assembly/src/main/filtered/etc/snmp-graph.properties.d/wsman-microsoft-windows-virtmem.properties
deleted file mode 100644
index 9f2c81baacfa..000000000000
--- a/opennms-base-assembly/src/main/filtered/etc/snmp-graph.properties.d/wsman-microsoft-windows-virtmem.properties
+++ /dev/null
@@ -1,32 +0,0 @@
-##############################################################################
-##
-## Please add report definition in a new line to make it easier
-## for script based sanity checks
-##
-##################################################
-
-reports=microsoft.windows.virtmem1
-
-report.microsoft.windows.virtmem1.name=Virtual Memory
-report.microsoft.windows.virtmem1.columns=freeVirtMem,totalVirtMem
-report.microsoft.windows.virtmem1.type=nodeSnmp
-report.microsoft.windows.virtmem1.suppress=microsoft.windows.virtmem
-report.microsoft.windows.virtmem1.command=--title="Virtual Memory Usage (WinRM)" \
- --vertical-label="Memory" \
- DEF:freekBytes={rrd1}:freeVirtMem:AVERAGE \
- DEF:totalkBytes={rrd2}:totalVirtMem:AVERAGE \
- CDEF:freeBytes=freekBytes,1024,* \
- CDEF:totalBytes=totalkBytes,1024,* \
- CDEF:usedBytes=totalBytes,freeBytes,- \
- AREA:usedBytes#ff0000:"Used" \
- GPRINT:usedBytes:AVERAGE:"Avg \\: %10.2lf %s" \
- GPRINT:usedBytes:MIN:"Min \\: %10.2lf %s" \
- GPRINT:usedBytes:MAX:"Max \\: %10.2lf %s\\n" \
- STACK:freeBytes#0cff00:"Free" \
- GPRINT:freeBytes:AVERAGE:"Avg \\: %10.2lf %s" \
- GPRINT:freeBytes:MIN:"Min \\: %10.2lf %s" \
- GPRINT:freeBytes:MAX:"Max \\: %10.2lf %s\\n" \
- LINE2:totalBytes#0000ff:"Total" \
- GPRINT:totalBytes:AVERAGE:"Avg \\: %10.2lf %s" \
- GPRINT:totalBytes:MIN:"Min \\: %10.2lf %s" \
- GPRINT:totalBytes:MAX:"Max \\: %10.2lf %s\\n"
diff --git a/opennms-base-assembly/src/main/filtered/etc/snmp-graph.properties.d/wsman-microsoft-windows.properties b/opennms-base-assembly/src/main/filtered/etc/snmp-graph.properties.d/wsman-microsoft-windows.properties
index afa80ac0f4b8..34e4238931cb 100644
--- a/opennms-base-assembly/src/main/filtered/etc/snmp-graph.properties.d/wsman-microsoft-windows.properties
+++ b/opennms-base-assembly/src/main/filtered/etc/snmp-graph.properties.d/wsman-microsoft-windows.properties
@@ -10,23 +10,22 @@ reports=microsoft.windows.virtmem
report.microsoft.windows.virtmem.name=Virtual Memory
report.microsoft.windows.virtmem.columns=freeVirtMem,totalVirtMem
report.microsoft.windows.virtmem.type=nodeSnmp
-report.microsoft.windows.virtmem.command=--title="Virtual Memory Usage" \
+report.microsoft.windows.virtmem.command=--title="Virtual Memory Usage (WinRM)" \
--vertical-label="Memory" \
- DEF:freeBytes={rrd1}:freeVirtMem:AVERAGE \
- DEF:totalBytes={rrd2}:totalVirtMem:AVERAGE \
- CDEF:freeBits=freeBytes,8,* \
- CDEF:totalBits=totalBytes,8,* \
- CDEF:usedBits=totalBits,freeBits,- \
- AREA:usedBits#ff0000:"Used" \
- GPRINT:usedBits:AVERAGE:"Avg \\: %10.2lf %s" \
- GPRINT:usedBits:MIN:"Min \\: %10.2lf %s" \
- GPRINT:usedBits:MAX:"Max \\: %10.2lf %s\\n" \
- STACK:freeBits#0cff00:"Free" \
- GPRINT:freeBits:AVERAGE:"Avg \\: %10.2lf %s" \
- GPRINT:freeBits:MIN:"Min \\: %10.2lf %s" \
- GPRINT:freeBits:MAX:"Max \\: %10.2lf %s\\n" \
- LINE2:totalBits#0000ff:"Total" \
- GPRINT:totalBits:AVERAGE:"Avg \\: %10.2lf %s" \
- GPRINT:totalBits:MIN:"Min \\: %10.2lf %s" \
- GPRINT:totalBits:MAX:"Max \\: %10.2lf %s\\n"
-
+ DEF:freekBytes={rrd1}:freeVirtMem:AVERAGE \
+ DEF:totalkBytes={rrd2}:totalVirtMem:AVERAGE \
+ CDEF:freeBytes=freekBytes,1024,* \
+ CDEF:totalBytes=totalkBytes,1024,* \
+ CDEF:usedBytes=totalBytes,freeBytes,- \
+ AREA:usedBytes#ff0000:"Used" \
+ GPRINT:usedBytes:AVERAGE:"Avg \\: %10.2lf %s" \
+ GPRINT:usedBytes:MIN:"Min \\: %10.2lf %s" \
+ GPRINT:usedBytes:MAX:"Max \\: %10.2lf %s\\n" \
+ STACK:freeBytes#0cff00:"Free" \
+ GPRINT:freeBytes:AVERAGE:"Avg \\: %10.2lf %s" \
+ GPRINT:freeBytes:MIN:"Min \\: %10.2lf %s" \
+ GPRINT:freeBytes:MAX:"Max \\: %10.2lf %s\\n" \
+ LINE2:totalBytes#0000ff:"Total" \
+ GPRINT:totalBytes:AVERAGE:"Avg \\: %10.2lf %s" \
+ GPRINT:totalBytes:MIN:"Min \\: %10.2lf %s" \
+ GPRINT:totalBytes:MAX:"Max \\: %10.2lf %s\\n"
diff --git a/opennms-container/common.mk b/opennms-container/common.mk
index 67c6c8a106c3..364c2260ddb5 100644
--- a/opennms-container/common.mk
+++ b/opennms-container/common.mk
@@ -12,7 +12,7 @@ endif
VERSION := $(shell ../../.circleci/scripts/pom2version.sh ../../pom.xml)
SHELL := /bin/bash -o nounset -o pipefail -o errexit
BUILD_DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
-BASE_IMAGE := opennms/deploy-base:ubi9-3.5.1.b280-jre-17
+BASE_IMAGE := opennms/deploy-base:ubi9-3.5.2.b283-jre-17
DOCKER_CLI_EXPERIMENTAL := enabled
DOCKER_REGISTRY := docker.io
DOCKER_ORG := opennms
diff --git a/opennms-container/core/Dockerfile b/opennms-container/core/Dockerfile
index 7a48ed8c1457..40eccc4c0351 100644
--- a/opennms-container/core/Dockerfile
+++ b/opennms-container/core/Dockerfile
@@ -23,7 +23,7 @@
##
# Use Java base image and setup required RPMs as cacheable image.
##
-ARG BASE_IMAGE="opennms/deploy-base:ubi9-3.5.1.b280-jre-17"
+ARG BASE_IMAGE="opennms/deploy-base:ubi9-3.5.2.b283-jre-17"
FROM ${BASE_IMAGE} as core-tarball
diff --git a/opennms-container/minion/Dockerfile b/opennms-container/minion/Dockerfile
index 45dda8628887..5d1c6f1a4274 100644
--- a/opennms-container/minion/Dockerfile
+++ b/opennms-container/minion/Dockerfile
@@ -26,7 +26,7 @@
# To avoid issues, we rearrange the directories in pre-stage to avoid injecting these
# as additional layers into the final image.
##
-ARG BASE_IMAGE="opennms/deploy-base:ubi9-3.5.1.b280-jre-17"
+ARG BASE_IMAGE="opennms/deploy-base:ubi9-3.5.2.b283-jre-17"
FROM ${BASE_IMAGE} as minion-base
diff --git a/opennms-container/sentinel/Dockerfile b/opennms-container/sentinel/Dockerfile
index e4f91f669e7d..797780ad8f9e 100644
--- a/opennms-container/sentinel/Dockerfile
+++ b/opennms-container/sentinel/Dockerfile
@@ -23,7 +23,7 @@
##
# Use Java base image and setup required RPMs as cacheable image.
##
-ARG BASE_IMAGE="opennms/deploy-base:ubi9-3.5.1.b280-jre-17"
+ARG BASE_IMAGE="opennms/deploy-base:ubi9-3.5.2.b283-jre-17"
FROM ${BASE_IMAGE} as sentinel-tarball
diff --git a/opennms-full-assembly/pom.xml b/opennms-full-assembly/pom.xml
index 655419206c1c..f0431c6cdb14 100644
--- a/opennms-full-assembly/pom.xml
+++ b/opennms-full-assembly/pom.xml
@@ -391,6 +391,7 @@
opennms-javamailopennms-kafka-produceropennms-kafka-consumer
+ opennms-grpc-exporteropennms-modelopennms-measurements-shellopennms-notifications-shell
diff --git a/opennms-provision/opennms-provision-persistence/src/test/java/org/opennms/netmgt/provision/persist/NMS16357Test.java b/opennms-provision/opennms-provision-persistence/src/test/java/org/opennms/netmgt/provision/persist/NMS16357Test.java
index d5331c7167d3..cd2a94d02bf0 100644
--- a/opennms-provision/opennms-provision-persistence/src/test/java/org/opennms/netmgt/provision/persist/NMS16357Test.java
+++ b/opennms-provision/opennms-provision-persistence/src/test/java/org/opennms/netmgt/provision/persist/NMS16357Test.java
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 1999-2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 1999-2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/opennms-services/src/test/java/org/opennms/netmgt/collectd/SnmpSetIT.java b/opennms-services/src/test/java/org/opennms/netmgt/collectd/SnmpSetIT.java
index 1b2ed1497b96..889da9044747 100644
--- a/opennms-services/src/test/java/org/opennms/netmgt/collectd/SnmpSetIT.java
+++ b/opennms-services/src/test/java/org/opennms/netmgt/collectd/SnmpSetIT.java
@@ -1,8 +1,8 @@
/*******************************************************************************
* This file is part of OpenNMS(R).
*
- * Copyright (C) 2024 The OpenNMS Group, Inc.
- * OpenNMS(R) is Copyright (C) 1999-2024 The OpenNMS Group, Inc.
+ * Copyright (C) 2025 The OpenNMS Group, Inc.
+ * OpenNMS(R) is Copyright (C) 1999-2025 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
diff --git a/opennms-webapp/src/main/webapp/includes/bootstrap-footer.jsp b/opennms-webapp/src/main/webapp/includes/bootstrap-footer.jsp
index b1db60e1ae99..9712377d00ad 100644
--- a/opennms-webapp/src/main/webapp/includes/bootstrap-footer.jsp
+++ b/opennms-webapp/src/main/webapp/includes/bootstrap-footer.jsp
@@ -58,7 +58,7 @@