2021-06-09 18:44:10 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
"""Prometheus Discovery."""
|
|
|
|
|
2022-02-27 11:31:46 +00:00
|
|
|
import ipaddress
|
2021-06-09 18:44:10 +00:00
|
|
|
import json
|
|
|
|
import re
|
|
|
|
from collections import defaultdict
|
|
|
|
|
2023-02-12 14:11:15 +00:00
|
|
|
from prometheus_client import Gauge, Summary
|
2021-06-09 20:57:48 +00:00
|
|
|
|
2022-04-04 20:43:00 +00:00
|
|
|
from prometheuspvesd.client import ProxmoxClient
|
2021-06-09 18:44:10 +00:00
|
|
|
from prometheuspvesd.config import SingleConfig
|
2021-06-09 20:57:48 +00:00
|
|
|
from prometheuspvesd.exception import APIError
|
2021-06-12 14:59:46 +00:00
|
|
|
from prometheuspvesd.logger import SingleLog
|
2023-02-12 14:11:15 +00:00
|
|
|
from prometheuspvesd.model import Host, HostList
|
2021-06-09 18:44:10 +00:00
|
|
|
|
2021-06-15 20:48:11 +00:00
|
|
|
PROPAGATION_TIME = Summary(
|
|
|
|
"pve_sd_propagate_seconds", "Time spent propagating the inventory from PVE"
|
|
|
|
)
|
|
|
|
HOST_GAUGE = Gauge("pve_sd_hosts", "Number of hosts discovered by PVE SD")
|
|
|
|
|
2021-06-09 18:44:10 +00:00
|
|
|
|
2023-06-28 07:54:46 +00:00
|
|
|
class Discovery:
|
2021-06-09 18:44:10 +00:00
|
|
|
"""Prometheus PVE Service Discovery."""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.config = SingleConfig()
|
|
|
|
self.log = SingleLog()
|
|
|
|
self.logger = SingleLog().logger
|
2022-04-04 20:43:00 +00:00
|
|
|
self.client = ProxmoxClient()
|
2021-06-09 18:44:10 +00:00
|
|
|
self.host_list = HostList()
|
|
|
|
|
|
|
|
def _get_names(self, pve_list, pve_type):
|
|
|
|
names = []
|
|
|
|
|
|
|
|
if pve_type == "node":
|
|
|
|
names = [node["node"] for node in pve_list]
|
|
|
|
elif pve_type == "pool":
|
|
|
|
names = [pool["poolid"] for pool in pve_list]
|
|
|
|
|
2023-03-20 09:34:12 +00:00
|
|
|
return names
|
2021-06-09 18:44:10 +00:00
|
|
|
|
|
|
|
def _get_variables(self, pve_list, pve_type):
|
|
|
|
variables = {}
|
|
|
|
|
|
|
|
if pve_type in ["qemu", "container"]:
|
|
|
|
for vm in pve_list:
|
|
|
|
nested = {}
|
|
|
|
for key, value in vm.items():
|
|
|
|
nested["proxmox_" + key] = value
|
|
|
|
variables[vm["name"]] = nested
|
|
|
|
|
|
|
|
return variables
|
|
|
|
|
2022-02-27 11:31:46 +00:00
|
|
|
def _get_ip_addresses(self, pve_type, pve_node, vmid):
|
|
|
|
ipv4_address = False
|
|
|
|
ipv6_address = False
|
2021-06-09 18:44:10 +00:00
|
|
|
networks = False
|
|
|
|
if pve_type == "qemu":
|
|
|
|
# If qemu agent is enabled, try to gather the IP address
|
|
|
|
try:
|
2022-04-04 20:43:00 +00:00
|
|
|
if self.client.get_agent_info(pve_node, pve_type, vmid) is not None:
|
|
|
|
networks = self.client.get_network_interfaces(pve_node, vmid)
|
2023-02-20 09:49:59 +00:00
|
|
|
except Exception: # noqa
|
|
|
|
pass
|
2021-06-09 18:44:10 +00:00
|
|
|
|
2023-08-24 20:27:14 +00:00
|
|
|
if isinstance(networks, list):
|
2022-02-27 15:04:01 +00:00
|
|
|
for network in networks:
|
2022-04-06 08:10:29 +00:00
|
|
|
for ip_address in network.get("ip-addresses", []):
|
2022-02-27 15:04:01 +00:00
|
|
|
if ip_address["ip-address-type"] == "ipv4" and not ipv4_address:
|
|
|
|
ipv4_address = self._validate_ip(ip_address["ip-address"])
|
|
|
|
elif ip_address["ip-address-type"] == "ipv6" and not ipv6_address:
|
|
|
|
ipv6_address = self._validate_ip(ip_address["ip-address"])
|
2021-06-09 18:44:10 +00:00
|
|
|
|
2022-04-04 20:43:00 +00:00
|
|
|
config = self.client.get_instance_config(pve_node, pve_type, vmid)
|
|
|
|
if config and not ipv4_address:
|
2021-06-09 18:44:10 +00:00
|
|
|
try:
|
2023-02-12 14:11:15 +00:00
|
|
|
if "ipconfig0" in config:
|
2022-02-27 11:31:46 +00:00
|
|
|
sources = [config["net0"], config["ipconfig0"]]
|
|
|
|
else:
|
|
|
|
sources = [config["net0"]]
|
2021-06-09 18:44:10 +00:00
|
|
|
|
|
|
|
for s in sources:
|
2022-02-27 11:31:46 +00:00
|
|
|
find = re.search(r"ip=(\d*\.\d*\.\d*\.\d*)", str(s))
|
2021-06-09 18:44:10 +00:00
|
|
|
if find and find.group(1):
|
2022-02-27 11:31:46 +00:00
|
|
|
ipv4_address = find.group(1)
|
2021-06-09 18:44:10 +00:00
|
|
|
break
|
2023-02-20 09:49:59 +00:00
|
|
|
except Exception: # noqa
|
|
|
|
pass
|
2021-06-09 18:44:10 +00:00
|
|
|
|
2022-04-04 20:43:00 +00:00
|
|
|
if config and not ipv6_address:
|
2022-02-27 11:31:46 +00:00
|
|
|
try:
|
2023-02-12 14:11:15 +00:00
|
|
|
if "ipconfig0" in config:
|
2022-02-27 11:31:46 +00:00
|
|
|
sources = [config["net0"], config["ipconfig0"]]
|
|
|
|
else:
|
|
|
|
sources = [config["net0"]]
|
|
|
|
|
|
|
|
for s in sources:
|
2022-04-04 20:43:00 +00:00
|
|
|
find = re.search(
|
|
|
|
r"ip=(([a-fA-F0-9]{0,4}:{0,2}){0,7}:[0-9a-fA-F]{1,4})", str(s)
|
|
|
|
)
|
2022-02-27 11:31:46 +00:00
|
|
|
if find and find.group(1):
|
|
|
|
ipv6_address = find.group(1)
|
|
|
|
break
|
2023-02-20 09:49:59 +00:00
|
|
|
except Exception: # noqa
|
|
|
|
pass
|
2022-02-27 11:31:46 +00:00
|
|
|
|
|
|
|
return ipv4_address, ipv6_address
|
2021-06-09 18:44:10 +00:00
|
|
|
|
2022-03-30 18:48:39 +00:00
|
|
|
def _filter(self, pve_list):
|
2021-06-09 18:44:10 +00:00
|
|
|
filtered = []
|
|
|
|
for item in pve_list:
|
|
|
|
obj = defaultdict(dict, item)
|
2022-03-30 18:48:39 +00:00
|
|
|
if (
|
|
|
|
len(self.config.config["include_vmid"]) > 0
|
|
|
|
and str(obj["vmid"]) not in self.config.config["include_vmid"]
|
|
|
|
):
|
|
|
|
continue
|
|
|
|
|
2023-11-10 13:50:58 +00:00
|
|
|
if len(self.config.config["include_tags"]) > 0 and (
|
|
|
|
bool(obj["tags"]) is False # continue if tags is not set
|
|
|
|
or set(obj["tags"].split(",")).isdisjoint(self.config.config["include_tags"])
|
2022-03-30 18:48:39 +00:00
|
|
|
):
|
|
|
|
continue
|
|
|
|
|
2021-06-09 18:44:10 +00:00
|
|
|
if obj["template"] == 1:
|
|
|
|
continue
|
|
|
|
|
2021-06-13 15:34:30 +00:00
|
|
|
if obj["status"] in map(str, self.config.config["exclude_state"]):
|
2021-06-09 18:44:10 +00:00
|
|
|
continue
|
|
|
|
|
2022-02-26 17:05:18 +00:00
|
|
|
if str(obj["vmid"]) in self.config.config["exclude_vmid"]:
|
2021-06-09 18:44:10 +00:00
|
|
|
continue
|
|
|
|
|
2024-01-24 09:52:35 +00:00
|
|
|
if isinstance(obj["tags"], str) and not set(obj["tags"].split(";")).isdisjoint(
|
2023-11-10 13:50:58 +00:00
|
|
|
self.config.config["exclude_tags"]
|
2022-03-03 08:08:27 +00:00
|
|
|
):
|
|
|
|
continue
|
|
|
|
|
2021-06-09 18:44:10 +00:00
|
|
|
filtered.append(item.copy())
|
|
|
|
return filtered
|
|
|
|
|
2022-02-27 16:56:08 +00:00
|
|
|
def _validate_ip(self, address: object) -> object:
|
|
|
|
try:
|
|
|
|
if (
|
|
|
|
not ipaddress.ip_address(address).is_loopback
|
|
|
|
and not ipaddress.ip_address(address).is_link_local
|
|
|
|
):
|
|
|
|
return address
|
|
|
|
except ValueError:
|
|
|
|
return False
|
|
|
|
|
2021-06-15 20:48:11 +00:00
|
|
|
@PROPAGATION_TIME.time()
|
2021-06-09 18:44:10 +00:00
|
|
|
def propagate(self):
|
|
|
|
self.host_list.clear()
|
2023-05-14 17:55:03 +00:00
|
|
|
nodelist = self._get_names(self.client.get_nodes(), "node")
|
|
|
|
self.logger.info(f"Discovered nodes: {','.join(nodelist)}")
|
|
|
|
for node in nodelist:
|
2021-06-09 18:44:10 +00:00
|
|
|
try:
|
2022-04-04 20:43:00 +00:00
|
|
|
qemu_list = self._filter(self.client.get_all_vms(node))
|
|
|
|
container_list = self._filter(self.client.get_all_containers(node))
|
2021-06-09 18:44:10 +00:00
|
|
|
except Exception as e: # noqa
|
2023-02-12 14:11:15 +00:00
|
|
|
raise APIError(str(e)) from e
|
2021-06-09 18:44:10 +00:00
|
|
|
|
|
|
|
# Merge QEMU and Containers lists from this node
|
|
|
|
instances = self._get_variables(qemu_list, "qemu").copy()
|
|
|
|
instances.update(self._get_variables(container_list, "container"))
|
|
|
|
|
2021-06-15 20:48:11 +00:00
|
|
|
HOST_GAUGE.set(len(instances))
|
2023-05-14 17:55:03 +00:00
|
|
|
self.logger.info(f"{node}: Found {len(instances)} targets")
|
2021-06-09 18:44:10 +00:00
|
|
|
for host in instances:
|
|
|
|
host_meta = instances[host]
|
|
|
|
vmid = host_meta["proxmox_vmid"]
|
|
|
|
|
|
|
|
try:
|
|
|
|
pve_type = host_meta["proxmox_type"]
|
|
|
|
except KeyError:
|
|
|
|
pve_type = "qemu"
|
|
|
|
|
2022-04-04 20:43:00 +00:00
|
|
|
config = self.client.get_instance_config(node, pve_type, vmid)
|
2021-06-09 18:44:10 +00:00
|
|
|
|
|
|
|
try:
|
2023-11-10 13:50:58 +00:00
|
|
|
description = config["description"]
|
2021-06-09 18:44:10 +00:00
|
|
|
except KeyError:
|
|
|
|
description = None
|
|
|
|
except Exception as e: # noqa
|
2023-02-12 14:11:15 +00:00
|
|
|
raise APIError(str(e)) from e
|
2021-06-09 18:44:10 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
metadata = json.loads(description)
|
|
|
|
except TypeError:
|
|
|
|
metadata = {}
|
|
|
|
except ValueError:
|
|
|
|
metadata = {"notes": description}
|
|
|
|
|
2022-02-27 15:04:01 +00:00
|
|
|
ipv4_address, ipv6_address = self._get_ip_addresses(pve_type, node, vmid)
|
2021-06-09 18:44:10 +00:00
|
|
|
|
2022-02-27 11:31:46 +00:00
|
|
|
prom_host = Host(vmid, host, ipv4_address, ipv6_address, pve_type)
|
2021-06-09 18:44:10 +00:00
|
|
|
|
|
|
|
config_flags = [("cpu", "sockets"), ("cores", "cores"), ("memory", "memory")]
|
2022-03-11 09:41:22 +00:00
|
|
|
meta_flags = [("status", "proxmox_status"), ("tags", "proxmox_tags")]
|
2021-06-09 18:44:10 +00:00
|
|
|
|
|
|
|
for key, flag in config_flags:
|
|
|
|
if flag in config:
|
|
|
|
prom_host.add_label(key, config[flag])
|
|
|
|
|
|
|
|
for key, flag in meta_flags:
|
|
|
|
if flag in host_meta:
|
|
|
|
prom_host.add_label(key, host_meta[flag])
|
|
|
|
|
|
|
|
if "groups" in metadata:
|
|
|
|
prom_host.add_label("groups", ",".join(metadata["groups"]))
|
|
|
|
|
|
|
|
self.host_list.add_host(prom_host)
|
2023-02-12 14:11:15 +00:00
|
|
|
self.logger.debug(f"Discovered {prom_host}")
|
2021-06-09 18:44:10 +00:00
|
|
|
|
|
|
|
return self.host_list
|