Add a Prometheus metric to track host certificate expiry (#12613)

This commit is contained in:
Nicolas Vazquez 2026-02-11 09:46:49 -03:00 committed by GitHub
parent b45726f7b1
commit 4de8c2b6f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 151 additions and 0 deletions

View File

@ -17,6 +17,7 @@
package org.apache.cloudstack.metrics;
import java.math.BigDecimal;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@ -26,6 +27,7 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import org.apache.cloudstack.ca.CAManager;
import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope;
import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
import org.apache.commons.lang3.StringUtils;
@ -133,6 +135,8 @@ public class PrometheusExporterImpl extends ManagerBase implements PrometheusExp
private ResourceCountDao _resourceCountDao;
@Inject
private HostTagsDao _hostTagsDao;
@Inject
private CAManager caManager;
public PrometheusExporterImpl() {
super();
@ -216,6 +220,9 @@ public class PrometheusExporterImpl extends ManagerBase implements PrometheusExp
}
metricsList.add(new ItemHostVM(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), vmDao.listByHostId(host.getId()).size()));
addSSLCertificateExpirationMetrics(metricsList, zoneName, zoneUuid, host);
final CapacityVO coreCapacity = capacityDao.findByHostIdType(host.getId(), Capacity.CAPACITY_TYPE_CPU_CORE);
if (coreCapacity == null && !host.isInMaintenanceStates()){
@ -253,6 +260,18 @@ public class PrometheusExporterImpl extends ManagerBase implements PrometheusExp
addHostTagsMetrics(metricsList, dcId, zoneName, zoneUuid, totalHosts, upHosts, downHosts, total, up, down);
}
private void addSSLCertificateExpirationMetrics(List<Item> metricsList, String zoneName, String zoneUuid, HostVO host) {
if (caManager == null || caManager.getActiveCertificatesMap() == null) {
return;
}
X509Certificate cert = caManager.getActiveCertificatesMap().getOrDefault(host.getPrivateIpAddress(), null);
if (cert == null) {
return;
}
long certExpiryEpoch = cert.getNotAfter().getTime() / 1000; // Convert to epoch seconds
metricsList.add(new ItemHostCertExpiry(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), certExpiryEpoch));
}
private String markTagMaps(HostVO host, Map<String, Integer> totalHosts, Map<String, Integer> upHosts, Map<String, Integer> downHosts) {
List<HostTagVO> hostTagVOS = _hostTagsDao.getHostTags(host.getId());
List<String> hostTags = new ArrayList<>();
@ -1049,4 +1068,28 @@ public class PrometheusExporterImpl extends ManagerBase implements PrometheusExp
return String.format("%s{zone=\"%s\",cpu=\"%d\",memory=\"%d\"} %d", name, zoneName, cpu, memory, total);
}
}
class ItemHostCertExpiry extends Item {
String zoneName;
String zoneUuid;
String hostName;
String hostUuid;
String hostIp;
long expiryTimestamp;
public ItemHostCertExpiry(final String zoneName, final String zoneUuid, final String hostName, final String hostUuid, final String hostIp, final long expiry) {
super("cloudstack_host_cert_expiry_timestamp");
this.zoneName = zoneName;
this.zoneUuid = zoneUuid;
this.hostName = hostName;
this.hostUuid = hostUuid;
this.hostIp = hostIp;
this.expiryTimestamp = expiry;
}
@Override
public String toMetricsString() {
return String.format("%s{zone=\"%s\",hostname=\"%s\",ip=\"%s\"} %d", name, zoneName, hostName, hostIp, expiryTimestamp);
}
}
}

View File

@ -0,0 +1,108 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// 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.apache.cloudstack.metrics;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class PrometheusExporterImplTest {
private static final String TEST_ZONE_NAME = "zone1";
private static final String TEST_ZONE_UUID = "zone-uuid-1";
private static final String TEST_HOST_NAME = "host1";
private static final String TEST_HOST_UUID = "host-uuid-1";
private static final String TEST_HOST_IP = "192.168.1.10";
private static final long CERT_EXPIRY_TIME = 1735689600000L; // 2025-01-01 00:00:00 UTC
private static final long CERT_EXPIRY_EPOCH = CERT_EXPIRY_TIME / 1000;
@Test
public void testItemHostCertExpiryFormat() {
PrometheusExporterImpl exporter = new PrometheusExporterImpl();
PrometheusExporterImpl.ItemHostCertExpiry item = exporter.new ItemHostCertExpiry(
TEST_ZONE_NAME,
TEST_ZONE_UUID,
TEST_HOST_NAME,
TEST_HOST_UUID,
TEST_HOST_IP,
CERT_EXPIRY_EPOCH
);
String metricsString = item.toMetricsString();
String expected = String.format(
"cloudstack_host_cert_expiry_timestamp{zone=\"%s\",hostname=\"%s\",ip=\"%s\"} %d",
TEST_ZONE_NAME,
TEST_HOST_NAME,
TEST_HOST_IP,
CERT_EXPIRY_EPOCH
);
assertEquals("Certificate expiry metric format should match expected format", expected, metricsString);
}
@Test
public void testItemHostCertExpiryContainsCorrectMetricName() {
PrometheusExporterImpl exporter = new PrometheusExporterImpl();
PrometheusExporterImpl.ItemHostCertExpiry item = exporter.new ItemHostCertExpiry(
TEST_ZONE_NAME,
TEST_ZONE_UUID,
TEST_HOST_NAME,
TEST_HOST_UUID,
TEST_HOST_IP,
CERT_EXPIRY_EPOCH
);
String metricsString = item.toMetricsString();
assertTrue("Metric should contain correct metric name",
metricsString.contains("cloudstack_host_cert_expiry_timestamp"));
}
@Test
public void testItemHostCertExpiryContainsAllLabels() {
PrometheusExporterImpl exporter = new PrometheusExporterImpl();
PrometheusExporterImpl.ItemHostCertExpiry item = exporter.new ItemHostCertExpiry(
TEST_ZONE_NAME,
TEST_ZONE_UUID,
TEST_HOST_NAME,
TEST_HOST_UUID,
TEST_HOST_IP,
CERT_EXPIRY_EPOCH
);
String metricsString = item.toMetricsString();
assertTrue("Metric should contain zone label", metricsString.contains("zone=\"" + TEST_ZONE_NAME + "\""));
assertTrue("Metric should contain hostname label", metricsString.contains("hostname=\"" + TEST_HOST_NAME + "\""));
assertTrue("Metric should contain ip label", metricsString.contains("ip=\"" + TEST_HOST_IP + "\""));
}
@Test
public void testItemHostCertExpiryContainsTimestampValue() {
PrometheusExporterImpl exporter = new PrometheusExporterImpl();
PrometheusExporterImpl.ItemHostCertExpiry item = exporter.new ItemHostCertExpiry(
TEST_ZONE_NAME,
TEST_ZONE_UUID,
TEST_HOST_NAME,
TEST_HOST_UUID,
TEST_HOST_IP,
CERT_EXPIRY_EPOCH
);
String metricsString = item.toMetricsString();
assertTrue("Metric should contain correct timestamp value",
metricsString.endsWith(" " + CERT_EXPIRY_EPOCH));
}
}