Compare commits

..

6 Commits

Author SHA1 Message Date
illustris
bd955e8067 Bump version to 1.3.3 2025-05-11 05:28:45 +05:30
illustris
b1e9b1e0b5 Add host binding option 2025-05-11 05:27:37 +05:30
illustris
4ac1ba1f24 bump inputs 2025-05-11 05:22:46 +05:30
illustris
b57db23a35 Bump version to 1.3.2 2025-04-14 08:33:25 +05:30
illustris
ebcc08cc8f Use zpool command to measure ZFS pool sizes accurately
Replace statvfs with zpool list command for ZFS storage pools to get accurate
size and free space metrics. This resolves the 'mountpoint' key error for ZFS
pools and provides more accurate capacity information.
2025-04-14 08:30:44 +05:30
illustris
1d06e1c180 convert bool labels in storage info to strings 2025-04-14 08:19:05 +05:30
5 changed files with 63 additions and 18 deletions

12
flake.lock generated
View File

@@ -3,11 +3,11 @@
"debBundler": { "debBundler": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1725149456, "lastModified": 1746317543,
"narHash": "sha256-rRrSD7itoPm+VIT4bIzSupQ7jw+H4eOjxRiRA89Kxb4=", "narHash": "sha256-1Xph5g1Lazzkc9XuY1nOkG5Fn7+lmSdldAC91boDawY=",
"owner": "illustris", "owner": "illustris",
"repo": "flake", "repo": "flake",
"rev": "257a6c986cb9a67c4d6d0e0363507cab7f958b63", "rev": "e86bd104d76d22b2ba36fede405e7bff290ef489",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -18,11 +18,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1725103162, "lastModified": 1746663147,
"narHash": "sha256-Ym04C5+qovuQDYL/rKWSR+WESseQBbNAe5DsXNx5trY=", "narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "12228ff1752d7b7624a54e9c1af4b222b3c1073b", "rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -14,7 +14,7 @@ rec {
packages.x86_64-linux = with nixpkgs.legacyPackages.x86_64-linux; rec { packages.x86_64-linux = with nixpkgs.legacyPackages.x86_64-linux; rec {
pvemon = python3Packages.buildPythonApplication { pvemon = python3Packages.buildPythonApplication {
pname = "pvemon"; pname = "pvemon";
version = "1.3.1"; version = "1.3.3";
src = ./src; src = ./src;
propagatedBuildInputs = with python3Packages; [ propagatedBuildInputs = with python3Packages; [
pexpect pexpect

View File

@@ -34,6 +34,7 @@ pool_cache = {
DEFAULT_PORT = 9116 DEFAULT_PORT = 9116
DEFAULT_INTERVAL = 10 DEFAULT_INTERVAL = 10
DEFAULT_PREFIX = "pve" DEFAULT_PREFIX = "pve"
DEFAULT_HOST = "0.0.0.0"
gauge_settings = [ gauge_settings = [
('kvm_cpu', 'CPU time for VM', ['id', 'mode']), ('kvm_cpu', 'CPU time for VM', ['id', 'mode']),
@@ -353,6 +354,7 @@ class PVECollector(object):
def main(): def main():
parser = argparse.ArgumentParser(description='PVE metrics exporter for Prometheus') parser = argparse.ArgumentParser(description='PVE metrics exporter for Prometheus')
parser.add_argument('--port', type=int, default=DEFAULT_PORT, help='Port for the exporter to listen on') parser.add_argument('--port', type=int, default=DEFAULT_PORT, help='Port for the exporter to listen on')
parser.add_argument('--host', type=str, default=DEFAULT_HOST, help='Host address to bind the exporter to')
parser.add_argument('--interval', type=int, default=DEFAULT_INTERVAL, help='THIS OPTION DOES NOTHING') parser.add_argument('--interval', type=int, default=DEFAULT_INTERVAL, help='THIS OPTION DOES NOTHING')
parser.add_argument('--collect-running-vms', type=str, default='true', help='Enable or disable collecting running VMs metric (true/false)') parser.add_argument('--collect-running-vms', type=str, default='true', help='Enable or disable collecting running VMs metric (true/false)')
parser.add_argument('--collect-storage', type=str, default='true', help='Enable or disable collecting storage info (true/false)') parser.add_argument('--collect-storage', type=str, default='true', help='Enable or disable collecting storage info (true/false)')
@@ -388,7 +390,7 @@ def main():
return return
else: else:
REGISTRY.register(PVECollector()) REGISTRY.register(PVECollector())
start_http_server(cli_args.port) start_http_server(cli_args.port, addr=cli_args.host)
while True: while True:
time.sleep(100) time.sleep(100)

View File

@@ -87,15 +87,53 @@ def parse_storage_cfg(file_path='/etc/pve/storage.cfg'):
def get_storage_size(storage): def get_storage_size(storage):
try: try:
if storage["type"] in ["dir", "nfs", "cephfs", "zfspool"]: if storage["type"] == "zfspool":
if storage["type"] == "zfspool": if "pool" not in storage:
path = storage["mountpoint"] logging.debug(f"ZFS pool {storage['name']} has no pool name configured")
else: return None
path = storage["path"]
# Get filesystem statistics # Extract the pool name (could be in format like rpool/data)
pool_name = storage["pool"].split("/")[0]
# Use zpool command to get accurate size information
import subprocess
try:
result = subprocess.run(
["zpool", "list", pool_name, "-p"],
capture_output=True,
text=True,
check=True
)
# Parse the output
lines = result.stdout.strip().split("\n")
if len(lines) < 2:
logging.warn(f"Unexpected zpool list output format for {pool_name}")
return None
# Extract values from the second line (the data line)
values = lines[1].split()
if len(values) < 4:
logging.warn(f"Insufficient data in zpool list output for {pool_name}")
return None
# Values are: NAME SIZE ALLOC FREE ...
# We need the SIZE and FREE values (index 1 and 3)
total_size = int(values[1])
free_space = int(values[3])
return {
"total": total_size,
"free": free_space
}
except (subprocess.SubprocessError, ValueError, IndexError) as e:
logging.warn(f"Error running zpool list for {pool_name}: {e}")
return None
elif storage["type"] in ["dir", "nfs", "cephfs"]:
# For non-ZFS storage, use statvfs
path = storage["path"]
stats = os.statvfs(path) stats = os.statvfs(path)
# Calculate total size and free space in bytes
# TODO: find an alternative way to calculate total_size for ZFS
total_size = stats.f_frsize * stats.f_blocks total_size = stats.f_frsize * stats.f_blocks
free_space = stats.f_frsize * stats.f_bavail free_space = stats.f_frsize * stats.f_bavail
return { return {
@@ -126,7 +164,12 @@ def collect_storage_metrics():
storage_pools = parse_storage_cfg() storage_pools = parse_storage_cfg()
for storage in storage_pools: for storage in storage_pools:
info_dict["node_storage"].add_metric([], storage) # Convert any non-string values to strings for InfoMetricFamily
storage_info = {}
for key, value in storage.items():
storage_info[key] = str(value) if not isinstance(value, str) else value
info_dict["node_storage"].add_metric([], storage_info)
size = get_storage_size(storage) size = get_storage_size(storage)
if size != None: if size != None:
gauge_dict["node_storage_size"].add_metric([storage["name"], storage["type"]], size["total"]) gauge_dict["node_storage_size"].add_metric([storage["name"], storage["type"]], size["total"])

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name='pvemon', name='pvemon',
version = "1.3.1", version = "1.3.3",
packages=find_packages(), packages=find_packages(),
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [