diff --git a/src/pve_exporter/collector.py b/src/pve_exporter/collector.py index 57bf4bc..476573b 100644 --- a/src/pve_exporter/collector.py +++ b/src/pve_exporter/collector.py @@ -1,299 +1,299 @@ """ Prometheus collecters for Proxmox VE cluster. """ # pylint: disable=too-few-public-methods import itertools from proxmoxer import ProxmoxAPI from prometheus_client import CollectorRegistry, generate_latest from prometheus_client.core import GaugeMetricFamily -class StatusCollector(object): +class StatusCollector: """ Collects Proxmox VE Node/VM/CT-Status # HELP pve_up Node/VM/CT-Status is online/running # TYPE pve_up gauge pve_up{id="node/proxmox-host"} 1.0 pve_up{id="cluster/pvec"} 1.0 pve_up{id="lxc/101"} 1.0 pve_up{id="qemu/102"} 1.0 """ def __init__(self, pve): self._pve = pve def collect(self): # pylint: disable=missing-docstring status_metrics = GaugeMetricFamily( 'pve_up', 'Node/VM/CT-Status is online/running', labels=['id']) for entry in self._pve.cluster.status.get(): if entry['type'] == 'node': label_values = [entry['id']] status_metrics.add_metric(label_values, entry['online']) elif entry['type'] == 'cluster': label_values = ['cluster/{:s}'.format(entry['name'])] status_metrics.add_metric(label_values, entry['quorate']) else: raise ValueError('Got unexpected status entry type {:s}'.format(entry['type'])) for resource in self._pve.cluster.resources.get(type='vm'): label_values = [resource['id']] status_metrics.add_metric(label_values, resource['status'] == 'running') yield status_metrics -class VersionCollector(object): +class VersionCollector: """ Collects Proxmox VE build information. E.g.: # HELP pve_version_info Proxmox VE version info # TYPE pve_version_info gauge pve_version_info{release="15",repoid="7599e35a",version="4.4"} 1.0 """ LABEL_WHITELIST = ['release', 'repoid', 'version'] def __init__(self, pve): self._pve = pve def collect(self): # pylint: disable=missing-docstring version_items = self._pve.version.get().items() version = {key: value for key, value in version_items if key in self.LABEL_WHITELIST} labels, label_values = zip(*version.items()) metric = GaugeMetricFamily( 'pve_version_info', 'Proxmox VE version info', labels=labels ) metric.add_metric(label_values, 1) yield metric -class ClusterNodeCollector(object): +class ClusterNodeCollector: """ Collects Proxmox VE cluster node information. E.g.: # HELP pve_node_info Node info # TYPE pve_node_info gauge pve_node_info{id="node/proxmox-host", level="c", name="proxmox-host", nodeid="0"} 1.0 """ def __init__(self, pve): self._pve = pve def collect(self): # pylint: disable=missing-docstring nodes = [entry for entry in self._pve.cluster.status.get() if entry['type'] == 'node'] labels = ['id', 'level', 'name', 'nodeid'] if nodes: info_metrics = GaugeMetricFamily( 'pve_node_info', 'Node info', labels=labels) for node in nodes: label_values = [str(node[key]) for key in labels] info_metrics.add_metric(label_values, 1) yield info_metrics -class ClusterInfoCollector(object): +class ClusterInfoCollector: """ Collects Proxmox VE cluster information. E.g.: # HELP pve_cluster_info Cluster info # TYPE pve_cluster_info gauge pve_cluster_info{id="cluster/pvec",nodes="2",quorate="1",version="2"} 1.0 """ def __init__(self, pve): self._pve = pve def collect(self): # pylint: disable=missing-docstring clusters = [entry for entry in self._pve.cluster.status.get() if entry['type'] == 'cluster'] if clusters: # Remove superflous keys. for cluster in clusters: del cluster['type'] # Add cluster-prefix to id. for cluster in clusters: cluster['id'] = 'cluster/{:s}'.format(cluster['name']) del cluster['name'] # Yield remaining data. labels = clusters[0].keys() info_metrics = GaugeMetricFamily( 'pve_cluster_info', 'Cluster info', labels=labels) for cluster in clusters: label_values = [str(cluster[key]) for key in labels] info_metrics.add_metric(label_values, 1) yield info_metrics -class ClusterResourcesCollector(object): +class ClusterResourcesCollector: """ Collects Proxmox VE cluster resources information, i.e. memory, storage, cpu usage for cluster nodes and guests. """ def __init__(self, pve): self._pve = pve def collect(self): # pylint: disable=missing-docstring metrics = { 'maxdisk': GaugeMetricFamily( 'pve_disk_size_bytes', 'Size of storage device', labels=['id']), 'disk': GaugeMetricFamily( 'pve_disk_usage_bytes', 'Disk usage in bytes', labels=['id']), 'maxmem': GaugeMetricFamily( 'pve_memory_size_bytes', 'Size of memory', labels=['id']), 'mem': GaugeMetricFamily( 'pve_memory_usage_bytes', 'Memory usage in bytes', labels=['id']), 'netout': GaugeMetricFamily( 'pve_network_transmit_bytes', 'Number of bytes transmitted over the network', labels=['id']), 'netin': GaugeMetricFamily( 'pve_network_receive_bytes', 'Number of bytes received over the network', labels=['id']), 'diskwrite': GaugeMetricFamily( 'pve_disk_write_bytes', 'Number of bytes written to storage', labels=['id']), 'diskread': GaugeMetricFamily( 'pve_disk_read_bytes', 'Number of bytes read from storage', labels=['id']), 'cpu': GaugeMetricFamily( 'pve_cpu_usage_ratio', 'CPU usage (value between 0.0 and pve_cpu_usage_limit)', labels=['id']), 'maxcpu': GaugeMetricFamily( 'pve_cpu_usage_limit', 'Maximum allowed CPU usage', labels=['id']), 'uptime': GaugeMetricFamily( 'pve_uptime_seconds', 'Number of seconds since the last boot', labels=['id']), 'shared': GaugeMetricFamily( 'pve_storage_shared', 'Whether or not the storage is shared among cluster nodes', labels=['id']), } info_metrics = { 'guest': GaugeMetricFamily( 'pve_guest_info', 'VM/CT info', labels=['id', 'node', 'name', 'type']), 'storage': GaugeMetricFamily( 'pve_storage_info', 'Storage info', labels=['id', 'node', 'storage']), } info_lookup = { 'lxc': { 'labels': ['id', 'node', 'name', 'type'], 'gauge': info_metrics['guest'], }, 'qemu': { 'labels': ['id', 'node', 'name', 'type'], 'gauge': info_metrics['guest'], }, 'storage': { 'labels': ['id', 'node', 'storage'], 'gauge': info_metrics['storage'], }, } for resource in self._pve.cluster.resources.get(): restype = resource['type'] if restype in info_lookup: label_values = [resource.get(key, '') for key in info_lookup[restype]['labels']] info_lookup[restype]['gauge'].add_metric(label_values, 1) label_values = [resource['id']] for key, metric_value in resource.items(): if key in metrics: metrics[key].add_metric(label_values, metric_value) return itertools.chain(metrics.values(), info_metrics.values()) -class ClusterNodeConfigCollector(object): +class ClusterNodeConfigCollector: """ Collects Proxmox VE VM information directly from config, i.e. boot, name, onboot, etc. For manual test: "pvesh get /nodes////config" # HELP pve_onboot_status Proxmox vm config onboot value # TYPE pve_onboot_status gauge pve_onboot_status{id="qemu/113",node="XXXX",type="qemu"} 1.0 """ def __init__(self, pve): self._pve = pve def collect(self): # pylint: disable=missing-docstring metrics = { 'onboot': GaugeMetricFamily( 'pve_onboot_status', 'Proxmox vm config onboot value', labels=['id', 'node', 'type']), } for node in self._pve.nodes.get(): if node["status"] == "online": # Qemu vmtype = 'qemu' for vmdata in self._pve.nodes(node['node']).qemu.get(): config = self._pve.nodes(node['node']).qemu(vmdata['vmid']).config.get().items() for key, metric_value in config: label_values = ["%s/%s" % (vmtype, vmdata['vmid']), node['node'], vmtype] if key in metrics: metrics[key].add_metric(label_values, metric_value) # LXC vmtype = 'lxc' for vmdata in self._pve.nodes(node['node']).lxc.get(): config = self._pve.nodes(node['node']).lxc(vmdata['vmid']).config.get().items() for key, metric_value in config: label_values = ["%s/%s" % (vmtype, vmdata['vmid']), node['node'], vmtype] if key in metrics: metrics[key].add_metric(label_values, metric_value) return metrics.values() def collect_pve(config, host): """Scrape a host and return prometheus text format for it""" pve = ProxmoxAPI(host, **config) registry = CollectorRegistry() registry.register(StatusCollector(pve)) registry.register(ClusterResourcesCollector(pve)) registry.register(ClusterNodeCollector(pve)) registry.register(ClusterInfoCollector(pve)) registry.register(ClusterNodeConfigCollector(pve)) registry.register(VersionCollector(pve)) return generate_latest(registry) diff --git a/src/pve_exporter/config.py b/src/pve_exporter/config.py index 2350cf1..0d9e666 100644 --- a/src/pve_exporter/config.py +++ b/src/pve_exporter/config.py @@ -1,100 +1,100 @@ """ Config module for Proxmox VE prometheus collector. """ try: from collections.abc import Mapping except ImportError: from collections import Mapping def config_from_yaml(yaml): """ Given a dictionary parsed from a yaml file return a config object. """ if not isinstance(yaml, Mapping): return ConfigInvalid( "Not a dictionary. Check the syntax of the YAML config file." ) modules = { key: config_module_from_yaml(value) for key, value in yaml.items() } invalid = [ " - {0}: {1}".format(key, module) for key, module in modules.items() if not module.valid ] if invalid: return ConfigInvalid("\n".join( ["Invalid module config entries in config file"] + invalid )) if not modules: return ConfigInvalid("Empty dictionary. No modules specified.") return ConfigMapping(modules) def config_module_from_yaml(yaml): """ Given a dictionary parsed from a yaml file return a module config object. """ if not isinstance(yaml, Mapping): return ConfigInvalid( "Not a dictionary. Check the syntax of the YAML config file." ) if not yaml: return ConfigInvalid( "Empty dictionary. No pve API parameters specified." ) return ConfigMapping(yaml) class ConfigMapping(Mapping): """ Valid config object. """ valid = True def __init__(self, mapping): - super(ConfigMapping, self).__init__() + super().__init__() self._mapping = mapping def __str__(self): num = len(self._mapping) keys = ", ".join(self._mapping.keys()) return "Valid config: with {0} keys: {1}".format(num, keys) def __getitem__(self, key): return self._mapping[key] def __iter__(self): return iter(self._mapping) def __len__(self): return len(self._mapping) -class ConfigInvalid(object): +class ConfigInvalid: """ Invalid config object. """ # pylint: disable=too-few-public-methods valid = False def __init__(self, error="Unspecified reason."): self._error = error def __str__(self): return "Invalid config: {0}".format(self._error) diff --git a/src/pve_exporter/http.py b/src/pve_exporter/http.py index 4cd7937..95bfb9c 100644 --- a/src/pve_exporter/http.py +++ b/src/pve_exporter/http.py @@ -1,138 +1,138 @@ """ HTTP API for Proxmox VE prometheus collector. """ import logging import time from prometheus_client import CONTENT_TYPE_LATEST, Summary, Counter, generate_latest from werkzeug.routing import Map, Rule from werkzeug.serving import run_simple from werkzeug.wrappers import Request, Response from werkzeug.exceptions import InternalServerError from .collector import collect_pve -class PveExporterApplication(object): +class PveExporterApplication: """ Proxmox VE prometheus collector HTTP handler. """ # pylint: disable=no-self-use def __init__(self, config, duration, errors): self._config = config self._duration = duration self._errors = errors self._url_map = Map([ Rule('/', endpoint='index'), Rule('/metrics', endpoint='metrics'), Rule('/pve', endpoint='pve'), ]) self._args = { 'pve': ['module', 'target'] } self._views = { 'index': self.on_index, 'metrics': self.on_metrics, 'pve': self.on_pve, } self._log = logging.getLogger(__name__) def on_pve(self, module='default', target='localhost'): """ Request handler for /pve route """ if module in self._config: start = time.time() output = collect_pve(self._config[module], target) response = Response(output) response.headers['content-type'] = CONTENT_TYPE_LATEST self._duration.labels(module).observe(time.time() - start) else: response = Response("Module '{0}' not found in config".format(module)) response.status_code = 400 return response def on_metrics(self): """ Request handler for /metrics route """ response = Response(generate_latest()) response.headers['content-type'] = CONTENT_TYPE_LATEST return response def on_index(self): """ Request handler for index route (/). """ response = Response( """ Proxmox VE Exporter

Proxmox VE Exporter

Visit /pve?target=1.2.3.4 to use.

""" ) response.headers['content-type'] = 'text/html' return response def view(self, endpoint, values, args): """ Werkzeug views mapping method. """ params = dict(values) if endpoint in self._args: params.update({key: args[key] for key in self._args[endpoint] if key in args}) try: return self._views[endpoint](**params) except Exception as error: # pylint: disable=broad-except self._log.exception("Exception thrown while rendering view") self._errors.labels(args.get('module', 'default')).inc() - raise InternalServerError(error) + raise InternalServerError from error @Request.application def __call__(self, request): urls = self._url_map.bind_to_environ(request.environ) view_func = lambda endpoint, values: self.view(endpoint, values, request.args) return urls.dispatch(view_func, catch_http_exceptions=True) def start_http_server(config, port, address=''): """ Start a HTTP API server for Proxmox VE prometheus collector. """ duration = Summary( 'pve_collection_duration_seconds', 'Duration of collections by the PVE exporter', ['module'], ) errors = Counter( 'pve_request_errors_total', 'Errors in requests to PVE exporter', ['module'], ) # Initialize metrics. for module in config.keys(): # pylint: disable=no-member errors.labels(module) # pylint: disable=no-member duration.labels(module) app = PveExporterApplication(config, duration, errors) run_simple(address, port, app, threaded=True)