Proxmox Integration
This commit is contained in:
7
.env
7
.env
@@ -28,6 +28,13 @@ QBIT_HOST=http://localhost:8080
|
|||||||
QBIT_USERNAME=your_username
|
QBIT_USERNAME=your_username
|
||||||
QBIT_PASSWORD=your_password
|
QBIT_PASSWORD=your_password
|
||||||
|
|
||||||
|
# Proxmox
|
||||||
|
ENABLE_PROXMOX=false
|
||||||
|
PROXMOX_HOST=https://your-proxmox-server:8006
|
||||||
|
PROXMOX_TOKEN_NAME=root@pam!yourtokenname
|
||||||
|
PROXMOX_TOKEN_VALUE=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
PROXMOX_VERIFY_SSL=false
|
||||||
|
|
||||||
# MySQL
|
# MySQL
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
DB_USER=root
|
DB_USER=root
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# 1.0.7
|
# 1.0.7
|
||||||
|
|
||||||
- Fixed JFA-GO API keys expiring. The bot now schedules a key refresh
|
- Fixed JFA-GO API keys expiring. The bot now schedules a key refresh
|
||||||
|
- Added Proxmox support for checking storage pool size
|
||||||
|
|
||||||
# 1.0.6
|
# 1.0.6
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ Fill out values in the .env and you're good to go!
|
|||||||
***💾 qBittorrent Commands***
|
***💾 qBittorrent Commands***
|
||||||
- `!qbview` - View current qBittorrent downloads
|
- `!qbview` - View current qBittorrent downloads
|
||||||
|
|
||||||
|
***🗳️ Proxmox Commands***
|
||||||
|
- `!storage` - Show available storage pools and free space
|
||||||
|
|
||||||
***🔑 JFA Commands***
|
***🔑 JFA Commands***
|
||||||
|
|
||||||
- `!createinvite` - Create a new JFA invite link
|
- `!createinvite` - Create a new JFA invite link
|
||||||
|
|||||||
117
app.py
117
app.py
@@ -8,6 +8,7 @@ from dotenv import load_dotenv
|
|||||||
import pytz
|
import pytz
|
||||||
import random
|
import random
|
||||||
import qbittorrentapi
|
import qbittorrentapi
|
||||||
|
from proxmoxer import ProxmoxAPI
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# ENV + VALIDATION
|
# ENV + VALIDATION
|
||||||
@@ -49,6 +50,12 @@ QBIT_HOST = os.getenv("QBIT_HOST")
|
|||||||
QBIT_USERNAME = os.getenv("QBIT_USERNAME")
|
QBIT_USERNAME = os.getenv("QBIT_USERNAME")
|
||||||
QBIT_PASSWORD = os.getenv("QBIT_PASSWORD")
|
QBIT_PASSWORD = os.getenv("QBIT_PASSWORD")
|
||||||
|
|
||||||
|
ENABLE_PROXMOX = os.getenv("ENABLE_PROXMOX", "False").lower() == "true"
|
||||||
|
PROXMOX_HOST = os.getenv("PROXMOX_HOST")
|
||||||
|
PROXMOX_TOKEN_NAME = os.getenv("PROXMOX_TOKEN_NAME")
|
||||||
|
PROXMOX_TOKEN_VALUE = os.getenv("PROXMOX_TOKEN_VALUE")
|
||||||
|
PROXMOX_VERIFY_SSL = os.getenv("PROXMOX_VERIFY_SSL", "False").lower() == "true"
|
||||||
|
|
||||||
DB_HOST = get_env_var("DB_HOST")
|
DB_HOST = get_env_var("DB_HOST")
|
||||||
DB_USER = get_env_var("DB_USER")
|
DB_USER = get_env_var("DB_USER")
|
||||||
DB_PASSWORD = get_env_var("DB_PASSWORD")
|
DB_PASSWORD = get_env_var("DB_PASSWORD")
|
||||||
@@ -358,6 +365,42 @@ def progress_bar(progress: float, length: int = 20) -> str:
|
|||||||
bar = '█' * filled_length + '░' * (length - filled_length)
|
bar = '█' * filled_length + '░' * (length - filled_length)
|
||||||
return f"[{bar}] {progress*100:.2f}%"
|
return f"[{bar}] {progress*100:.2f}%"
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# PROXMOX HELPERS
|
||||||
|
# =====================
|
||||||
|
|
||||||
|
def get_proxmox_client():
|
||||||
|
"""Create and return a Proxmox client using API token auth."""
|
||||||
|
if not (PROXMOX_HOST and PROXMOX_TOKEN_NAME and PROXMOX_TOKEN_VALUE):
|
||||||
|
raise ValueError("Proxmox API credentials are not fully configured in .env")
|
||||||
|
|
||||||
|
# Parse host + port safely
|
||||||
|
base_host = PROXMOX_HOST.replace("https://", "").replace("http://", "")
|
||||||
|
if ":" in base_host:
|
||||||
|
host, port = base_host.split(":")
|
||||||
|
else:
|
||||||
|
host, port = base_host, 8006 # default Proxmox port
|
||||||
|
|
||||||
|
# Split token into user + token name
|
||||||
|
try:
|
||||||
|
user, token_name = PROXMOX_TOKEN_NAME.split("!")
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
"❌ PROXMOX_TOKEN_NAME must be in the format 'user@realm!tokenid'"
|
||||||
|
)
|
||||||
|
|
||||||
|
log_event(f"[Proxmox] Connecting to {host}:{port} as {user} with token '{token_name}'")
|
||||||
|
|
||||||
|
return ProxmoxAPI(
|
||||||
|
host,
|
||||||
|
port=int(port),
|
||||||
|
user=user,
|
||||||
|
token_name=token_name,
|
||||||
|
token_value=PROXMOX_TOKEN_VALUE,
|
||||||
|
verify_ssl=PROXMOX_VERIFY_SSL
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# JFA-GO HELPERS
|
# JFA-GO HELPERS
|
||||||
# =====================
|
# =====================
|
||||||
@@ -1343,6 +1386,71 @@ async def qbview(ctx):
|
|||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command()
|
||||||
|
async def storage(ctx):
|
||||||
|
"""Check Proxmox storage pools and ZFS pools."""
|
||||||
|
if not ENABLE_PROXMOX:
|
||||||
|
await ctx.send("⚠️ Proxmox integration is disabled in the configuration.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not has_admin_role(ctx.author):
|
||||||
|
await ctx.send("❌ You don’t have permission to use this command.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
proxmox = get_proxmox_client()
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="📦 Proxmox Storage",
|
||||||
|
description="Storage pool usage and ZFS pools",
|
||||||
|
color=discord.Color.green()
|
||||||
|
)
|
||||||
|
|
||||||
|
for node in proxmox.nodes.get():
|
||||||
|
node_name = node["node"]
|
||||||
|
|
||||||
|
# ---- ZFS ----
|
||||||
|
try:
|
||||||
|
zfs_pools = proxmox.nodes(node_name).disks.zfs.get()
|
||||||
|
if zfs_pools:
|
||||||
|
zfs_info = [
|
||||||
|
f"**{p['name']}**: {p['alloc']/1024**3:.2f} GiB / "
|
||||||
|
f"{p['size']/1024**3:.2f} GiB ({(p['alloc']/p['size']*100):.1f}%)"
|
||||||
|
for p in zfs_pools
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
zfs_info = ["No ZFS pools found"]
|
||||||
|
except Exception as e:
|
||||||
|
zfs_info = [f"⚠️ Failed to fetch ZFS pools ({e})"]
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name=f"🖥️ {node_name} - ZFS Pools",
|
||||||
|
value="\n".join(zfs_info),
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- Normal storage (skip ZFS) ----
|
||||||
|
try:
|
||||||
|
storage_info = proxmox.nodes(node_name).storage.get()
|
||||||
|
normal_lines = [
|
||||||
|
f"**{s['storage']}**: {s['used']/1024**3:.2f} GiB / "
|
||||||
|
f"{s['total']/1024**3:.2f} GiB ({(s['used']/s['total']*100):.1f}%)"
|
||||||
|
for s in storage_info
|
||||||
|
if s.get("type", "").lower() not in ("zfspool", "zfs")
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
normal_lines = [f"⚠️ Failed to fetch normal storage ({e})"]
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name=f"🗳️ {node_name} - Normal Storage",
|
||||||
|
value="\n".join(normal_lines) or "No non‑ZFS storage found",
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.send(f"⚠️ Unexpected error: {e}")
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def link(ctx, jellyfin_username: str = None, user: discord.User = None, js_id: str = None):
|
async def link(ctx, jellyfin_username: str = None, user: discord.User = None, js_id: str = None):
|
||||||
log_event(f"link invoked by {ctx.author}")
|
log_event(f"link invoked by {ctx.author}")
|
||||||
@@ -1523,6 +1631,13 @@ async def help_command(ctx):
|
|||||||
]
|
]
|
||||||
embed.add_field(name="💾 qBittorrent Commands", value="\n".join(qb_cmds), inline=False)
|
embed.add_field(name="💾 qBittorrent Commands", value="\n".join(qb_cmds), inline=False)
|
||||||
|
|
||||||
|
# --- Proxmox Commands ---
|
||||||
|
if ENABLE_PROXMOX:
|
||||||
|
qb_cmds = [
|
||||||
|
f"`{PREFIX}storage` - Show available storage pools and free space",
|
||||||
|
]
|
||||||
|
embed.add_field(name="🗳️ Proxmox Commands", value="\n".join(qb_cmds), inline=False)
|
||||||
|
|
||||||
# --- JFA Commands ---
|
# --- JFA Commands ---
|
||||||
if ENABLE_JFA:
|
if ENABLE_JFA:
|
||||||
jfa_cmds = [
|
jfa_cmds = [
|
||||||
@@ -1686,7 +1801,7 @@ async def cleanup_task():
|
|||||||
# =====================
|
# =====================
|
||||||
if ENABLE_JFA:
|
if ENABLE_JFA:
|
||||||
|
|
||||||
@tasks.loop(hours=18)
|
@tasks.loop(hours=1)
|
||||||
async def refresh_jfa_loop():
|
async def refresh_jfa_loop():
|
||||||
success = refresh_jfa_token()
|
success = refresh_jfa_token()
|
||||||
if success:
|
if success:
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ python-dotenv==1.0.1
|
|||||||
pytz==2025.2
|
pytz==2025.2
|
||||||
apscheduler==3.11.0
|
apscheduler==3.11.0
|
||||||
qbittorrent-api==2025.7.0
|
qbittorrent-api==2025.7.0
|
||||||
|
proxmoxer==2.2.0
|
||||||
Reference in New Issue
Block a user