Skip to content

Commit

Permalink
refactor: add ProxmoxClient to handle pve api interactions (#196)
Browse files Browse the repository at this point in the history
  • Loading branch information
peschmae authored Apr 4, 2022
1 parent dfb18c1 commit 9179fa2
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 91 deletions.
90 changes: 90 additions & 0 deletions prometheuspvesd/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Proxmox Client."""
import requests
from prometheus_client import Counter

from prometheuspvesd.config import SingleConfig
from prometheuspvesd.exception import APIError
from prometheuspvesd.logger import SingleLog
from prometheuspvesd.model import HostList
from prometheuspvesd.utils import to_bool

try:
from proxmoxer import ProxmoxAPI
HAS_PROXMOXER = True
except ImportError:
HAS_PROXMOXER = False

PVE_REQUEST_COUNT_TOTAL = Counter("pve_sd_requests_total", "Total count of requests to PVE API")
PVE_REQUEST_COUNT_ERROR_TOTAL = Counter(
"pve_sd_requests_error_total", "Total count of failed requests to PVE API"
)


class ProxmoxClient:
"""Proxmox API Client."""

def __init__(self):
if not HAS_PROXMOXER:
self.log.sysexit_with_message(
"The Proxmox VE Prometheus SD requires proxmoxer: "
"https://pypi.org/project/proxmoxer/"
)

self.config = SingleConfig()
self.log = SingleLog()
self.logger = SingleLog().logger
self.client = self._auth()
self.logger.debug("Successfully authenticated")
self.host_list = HostList()

def _auth(self):
try:
self.logger.debug(
"Trying to authenticate against {} as user {}".format(
self.config.config["pve"]["server"], self.config.config["pve"]["user"]
)
)
return ProxmoxAPI(
self.config.config["pve"]["server"],
user=self.config.config["pve"]["user"],
password=self.config.config["pve"]["password"],
verify_ssl=to_bool(self.config.config["pve"]["verify_ssl"]),
timeout=self.config.config["pve"]["auth_timeout"]
)
except requests.RequestException as e:
PVE_REQUEST_COUNT_ERROR_TOTAL.inc()
raise APIError(str(e))

def _do_request(self, *args):
PVE_REQUEST_COUNT_TOTAL.inc()
try:
# create a new tuple containing nodes and unpack it again for client.get
return self.client.get(*("nodes", *args))
except requests.RequestException as e:
PVE_REQUEST_COUNT_ERROR_TOTAL.inc()
raise APIError(str(e))

def get_nodes(self):
self.logger.debug("fetching all nodes")
return self._do_request()

def get_all_vms(self, pve_node):
self.logger.debug("fetching all vms on node {}".format(pve_node))
return self._do_request(pve_node, "qemu")

def get_all_containers(self, pve_node):
self.logger.debug("fetching all containers on node {}".format(pve_node))
return self._do_request(pve_node, "lxc")

def get_instance_config(self, pve_node, pve_type, vmid):
self.logger.debug("fetching instance config for {} on {}".format(vmid, pve_node))
return self._do_request(pve_node, pve_type, vmid, "config")

def get_agent_info(self, pve_node, pve_type, vmid):
self.logger.debug("fetching agent info for {} on {}".format(vmid, pve_node))
return self._do_request(pve_node, pve_type, vmid, "agent", "info")["result"]

def get_network_interfaces(self, pve_node, vmid):
self.logger.debug("fetching network interfaces for {} on {}".format(vmid, pve_node))
return self._do_request(pve_node, "qemu", vmid, "agent",
"network-get-interfaces")["result"]
69 changes: 14 additions & 55 deletions prometheuspvesd/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,63 +6,32 @@
import re
from collections import defaultdict

import requests
from prometheus_client import Counter
from prometheus_client import Gauge
from prometheus_client import Summary

from prometheuspvesd.client import ProxmoxClient
from prometheuspvesd.config import SingleConfig
from prometheuspvesd.exception import APIError
from prometheuspvesd.logger import SingleLog
from prometheuspvesd.model import Host
from prometheuspvesd.model import HostList
from prometheuspvesd.utils import to_bool

try:
from proxmoxer import ProxmoxAPI
HAS_PROXMOXER = True
except ImportError:
HAS_PROXMOXER = False

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")
PVE_REQUEST_COUNT_TOTAL = Counter("pve_sd_requests_total", "Total count of requests to PVE API")
PVE_REQUEST_COUNT_ERROR_TOTAL = Counter(
"pve_sd_requests_error_total", "Total count of failed requests to PVE API"
)


class Discovery():
"""Prometheus PVE Service Discovery."""

def __init__(self):
if not HAS_PROXMOXER:
self.log.sysexit_with_message(
"The Proxmox VE Prometheus SD requires proxmoxer: "
"https://pypi.org/project/proxmoxer/"
)

self.config = SingleConfig()
self.log = SingleLog()
self.logger = SingleLog().logger
self.client = self._auth()
self.client = ProxmoxClient()
self.host_list = HostList()

def _auth(self):
try:
return ProxmoxAPI(
self.config.config["pve"]["server"],
user=self.config.config["pve"]["user"],
password=self.config.config["pve"]["password"],
verify_ssl=to_bool(self.config.config["pve"]["verify_ssl"]),
timeout=self.config.config["pve"]["auth_timeout"]
)
except requests.RequestException as e:
PVE_REQUEST_COUNT_ERROR_TOTAL.inc()
raise APIError(str(e))

def _get_names(self, pve_list, pve_type):
names = []

Expand Down Expand Up @@ -92,11 +61,8 @@ def _get_ip_addresses(self, pve_type, pve_node, vmid):
if pve_type == "qemu":
# If qemu agent is enabled, try to gather the IP address
try:
PVE_REQUEST_COUNT_TOTAL.inc()
if self.client.get("nodes", pve_node, pve_type, vmid, "agent", "info") is not None:
networks = self.client.get(
"nodes", pve_node, "qemu", vmid, "agent", "network-get-interfaces"
)["result"]
if self.client.get_agent_info(pve_node, pve_type, vmid) is not None:
networks = self.client.get_network_interfaces(pve_node, vmid)
except Exception: # noqa # nosec
pass

Expand All @@ -108,10 +74,9 @@ def _get_ip_addresses(self, pve_type, pve_node, vmid):
elif ip_address["ip-address-type"] == "ipv6" and not ipv6_address:
ipv6_address = self._validate_ip(ip_address["ip-address"])

if not ipv4_address:
config = self.client.get_instance_config(pve_node, pve_type, vmid)
if config and not ipv4_address:
try:
PVE_REQUEST_COUNT_TOTAL.inc()
config = self.client.get("nodes", pve_node, pve_type, vmid, "config")
if "ipconfig0" in config.keys():
sources = [config["net0"], config["ipconfig0"]]
else:
Expand All @@ -125,17 +90,17 @@ def _get_ip_addresses(self, pve_type, pve_node, vmid):
except Exception: # noqa # nosec
pass

if not ipv6_address:
if config and not ipv6_address:
try:
PVE_REQUEST_COUNT_TOTAL.inc()
config = self.client.get("nodes", pve_node, pve_type, vmid, "config")
if "ipconfig0" in config.keys():
sources = [config["net0"], config["ipconfig0"]]
else:
sources = [config["net0"]]

for s in sources:
find = re.search(r"ip=(\d*:\d*:\d*:\d*:\d*:\d*)", str(s))
find = re.search(
r"ip=(([a-fA-F0-9]{0,4}:{0,2}){0,7}:[0-9a-fA-F]{1,4})", str(s)
)
if find and find.group(1):
ipv6_address = find.group(1)
break
Expand Down Expand Up @@ -194,15 +159,11 @@ def _validate_ip(self, address: object) -> object:
def propagate(self):
self.host_list.clear()

PVE_REQUEST_COUNT_TOTAL.inc()
for node in self._get_names(self.client.get("nodes"), "node"):
for node in self._get_names(self.client.get_nodes(), "node"):
try:
PVE_REQUEST_COUNT_TOTAL.inc()
qemu_list = self._filter(self.client.get("nodes", node, "qemu"))
PVE_REQUEST_COUNT_TOTAL.inc()
container_list = self._filter(self.client.get("nodes", node, "lxc"))
qemu_list = self._filter(self.client.get_all_vms(node))
container_list = self._filter(self.client.get_all_containers(node))
except Exception as e: # noqa
PVE_REQUEST_COUNT_ERROR_TOTAL.inc()
raise APIError(str(e))

# Merge QEMU and Containers lists from this node
Expand All @@ -220,15 +181,13 @@ def propagate(self):
except KeyError:
pve_type = "qemu"

PVE_REQUEST_COUNT_TOTAL.inc()
config = self.client.get("nodes", node, pve_type, vmid, "config")
config = self.client.get_instance_config(node, pve_type, vmid)

try:
description = (config["description"])
except KeyError:
description = None
except Exception as e: # noqa
PVE_REQUEST_COUNT_ERROR_TOTAL.inc()
raise APIError(str(e))

try:
Expand Down
14 changes: 14 additions & 0 deletions prometheuspvesd/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ class HostList:
def __init__(self):
self.hosts = []

def __eq__(self, other):
if not isinstance(other, HostList):
return False

if len(other.hosts) != len(self.hosts):
return False

for host in self.hosts:
if other.host_exists(host):
continue
return False

return True

def clear(self):
self.hosts = []

Expand Down
79 changes: 65 additions & 14 deletions prometheuspvesd/test/fixtures/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,24 @@ def defaults():
}


@pytest.fixture
def nodes():
return [{
"level": "",
"id": "node/example-node",
"disk": 4783488,
"cpu": 0.0935113631167406,
"maxcpu": 24,
"maxmem": 142073272990,
"mem": 135884478304,
"node": "example-node",
"type": "node",
"status": "online",
"maxdisk": 504209920,
"uptime": 200
}]


@pytest.fixture
def qemus():
return [
Expand Down Expand Up @@ -222,12 +240,35 @@ def qemus():
]


@pytest.fixture
def instance_config():
return {
"name": "102.example.com",
"description": '{"groups": "test-group"}',
"net0": "virtio=D8-85-75-47-2E-8D,bridge=vmbr122,ip=192.0.2.25,ip=2001:db8::666:77:8888",
"cpu": 2,
"cores": 2
}


@pytest.fixture
def agent_info():
return {
"supported_commands": [{
"name": "guest-network-get-interfaces",
"enabled": True,
"success-response": True
}],
"version": "5.2.0"
}


@pytest.fixture
def addresses():
return {
"ipv4_valid": [
"192.168.0.1",
"10.0.0.1",
"192.0.2.1",
"198.51.100.1",
],
"ipv4_invalid": [
"127.0.0.1",
Expand Down Expand Up @@ -282,17 +323,17 @@ def networks():
"hardware-address": "92:0b:bd:c1:f8:39",
"ip-addresses": [
{
"ip-address": "10.168.0.1",
"ip-address": "192.0.2.1",
"ip-address-type": "ipv4",
"prefix": 32
},
{
"ip-address": "10.168.0.2",
"ip-address": "192.0.2.4",
"ip-address-type": "ipv4",
"prefix": 32
},
{
"ip-address": "2001:cdba:3333:4444:5555:6666:7777:8888",
"ip-address": "2001:db8:3333:4444:5555:6666:7777:8888",
"ip-address-type": "ipv6",
"prefix": 64
},
Expand All @@ -315,30 +356,40 @@ def networks():
@pytest.fixture
def inventory():
hostlist = HostList()
hostlist.add_host(Host("101", "host1", "129.168.0.1", False, "qemu"))
hostlist.add_host(Host("202", "host2", "129.168.0.2", False, "qemu"))
hostlist.add_host(Host("100", "100.example.com", "192.0.2.1", False, "qemu"))
hostlist.add_host(Host("101", "101.example.com", "192.0.2.2", False, "qemu"))
hostlist.add_host(Host("102", "102.example.com", "192.0.2.3", False, "qemu"))

return hostlist


@pytest.fixture
def labels():
return [{
"targets": ["host1"],
"targets": ["100.example.com"],
"labels": {
"__meta_pve_ipv4": "192.0.2.1",
"__meta_pve_ipv6": "False",
"__meta_pve_name": "100.example.com",
"__meta_pve_type": "qemu",
"__meta_pve_vmid": "100"
}
}, {
"targets": ["101.example.com"],
"labels": {
"__meta_pve_ipv4": "129.168.0.1",
"__meta_pve_ipv4": "192.0.2.2",
"__meta_pve_ipv6": "False",
"__meta_pve_name": "host1",
"__meta_pve_name": "101.example.com",
"__meta_pve_type": "qemu",
"__meta_pve_vmid": "101"
}
}, {
"targets": ["host2"],
"targets": ["102.example.com"],
"labels": {
"__meta_pve_ipv4": "129.168.0.2",
"__meta_pve_ipv4": "192.0.2.3",
"__meta_pve_ipv6": "False",
"__meta_pve_name": "host2",
"__meta_pve_name": "102.example.com",
"__meta_pve_type": "qemu",
"__meta_pve_vmid": "202"
"__meta_pve_vmid": "102"
}
}]
Loading

0 comments on commit 9179fa2

Please sign in to comment.