diff --git a/src/pve_exporter/cli.py b/src/pve_exporter/cli.py index 8d7f60a..8c3e4fb 100755 --- a/src/pve_exporter/cli.py +++ b/src/pve_exporter/cli.py @@ -49,6 +49,9 @@ def main(): nodeflags.add_argument('--collector.replication', dest='collector_replication', action=BooleanOptionalAction, default=True, help='Exposes PVE replication info') + nodeflags.add_argument('--collector.subscription', dest='collector_subscription', + action=BooleanOptionalAction, default=True, + help='Exposes PVE subscription info') parser.add_argument('--config.file', type=pathlib.Path, dest="config_file", default='/etc/prometheus/pve.yml', @@ -70,6 +73,7 @@ def main(): collectors = CollectorsOptions( status=params.collector_status, version=params.collector_version, + subscription=params.collector_subscription, node=params.collector_node, cluster=params.collector_cluster, resources=params.collector_resources, diff --git a/src/pve_exporter/collector/__init__.py b/src/pve_exporter/collector/__init__.py index cf73b63..e5b5d80 100644 --- a/src/pve_exporter/collector/__init__.py +++ b/src/pve_exporter/collector/__init__.py @@ -16,12 +16,14 @@ ) from pve_exporter.collector.node import ( NodeConfigCollector, - NodeReplicationCollector + NodeReplicationCollector, + SubscriptionCollector ) CollectorsOptions = collections.namedtuple('CollectorsOptions', [ 'status', 'version', + 'subscription', 'node', 'cluster', 'resources', @@ -46,6 +48,8 @@ def collect_pve(config, host, cluster, node, options: CollectorsOptions): registry.register(ClusterInfoCollector(pve)) if cluster and options.version: registry.register(VersionCollector(pve)) + if cluster and options.subscription: + registry.register(SubscriptionCollector(pve)) if node and options.config: registry.register(NodeConfigCollector(pve)) if node and options.replication: diff --git a/src/pve_exporter/collector/node.py b/src/pve_exporter/collector/node.py index 0f2c817..109e8d0 100644 --- a/src/pve_exporter/collector/node.py +++ b/src/pve_exporter/collector/node.py @@ -5,6 +5,7 @@ import logging import itertools +from datetime import datetime from prometheus_client.core import GaugeMetricFamily @@ -126,3 +127,66 @@ def collect(self): # pylint: disable=missing-docstring metrics[key].add_metric(label_values, metric_value) return itertools.chain(metrics.values(), info_metrics.values()) + +class SubscriptionCollector: + """ + Collects Proxmox VE subscription information (node, subscription level, status, next due date). + """ + + def __init__(self, pve): + self._pve = pve + + def collect(self): # pylint: disable=missing-docstring + info_metric = GaugeMetricFamily( + "pve_subscription_info", + "Proxmox VE subscription info (1 if present)", + labels=["id", "node", "level", "status"], + ) + + possible_statuses = ["new", "notfound", "active", "invalid", "expired", "suspended"] + status_metric = GaugeMetricFamily( + "pve_subscription_status", + "Proxmox VE subscription status (1 if matches status)", + labels=["id", "node", "status"], + ) + + next_due_metric = GaugeMetricFamily( + "pve_subscription_next_due_timestamp_seconds", + "Subscription next due date as Unix timestamp", + labels=["id", "node", "level"], + ) + + node = None + for entry in self._pve.cluster.status.get(): + if entry['type'] == 'node' and entry['local']: + node = entry['name'] + break + + subscription = self._pve.nodes(node).subscription.get() + + level = subscription.get("level", "unknown") + status = subscription.get("status", "unknown") + + info_metric.add_metric( + [f"node/{node}", node, level, status], + 1, + ) + + for possible_status in possible_statuses: + value = 1 if status == possible_status else 0 + status_metric.add_metric( + [f"node/{node}", node, possible_status], + value, + ) + + next_due_date = subscription.get("nextduedate") + if next_due_date: + timestamp = datetime.strptime(next_due_date, "%Y-%m-%d").timestamp() + next_due_metric.add_metric( + [f"node/{node}", node, level], + timestamp, + ) + + yield info_metric + yield status_metric + yield next_due_metric