feat: add home-assistant-dashboard-conventions skill + update multiple skills

- New: smart-home/home-assistant-dashboard-conventions (Mushroom cards, view tabs, no Bubble Cards)
- Updated: rke2, ceph, galera, proxmox, brainstorming, compound-learning, 1password-cli, smart-home-automation skills
- New references: ceph-cluster-administration, docker-volume-forensics, ceph-crush-weight, ceph-ec-mixed-size
This commit is contained in:
Debian
2026-07-14 18:35:16 +00:00
parent e9cc106625
commit 01bd921ced
37 changed files with 6133 additions and 126 deletions
@@ -0,0 +1,176 @@
#!/bin/bash
# PVE RAM-Based Rebalancer
# Runs via cron. Checks RAM overcommit on all PVE nodes.
# Migrates HA-managed VMs from overcommitted nodes to nodes with headroom.
# Respects HA node-affinity rules (strict rules constrain target nodes).
# Only outputs when it performs a migration (silent otherwise).
#
# Overcommit = sum(running VM maxmem on node) / node physical RAM
# A node is "overcommitted" when this ratio exceeds THRESHOLD_PCT.
THRESHOLD_PCT=100
MIN_FREE_GB=4
MAX_MIGRATIONS=2
SSH_KEY=~/.ssh/id_ed25519_proxmox
PVE_API_NODE="10.0.20.10"
TMP_DIR=$(mktemp -d)
trap "rm -rf $TMP_DIR" EXIT
ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@$PVE_API_NODE \
'pvesh get /cluster/resources --type vm --output-format json 2>/dev/null' \
> "$TMP_DIR/vms.json" 2>/dev/null
ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@$PVE_API_NODE \
'pvesh get /cluster/resources --type node --output-format json 2>/dev/null' \
> "$TMP_DIR/nodes.json" 2>/dev/null
ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@$PVE_API_NODE \
'cat /etc/pve/ha/rules.cfg 2>/dev/null' \
> "$TMP_DIR/rules.cfg" 2>/dev/null
ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@$PVE_API_NODE \
'ha-manager config 2>/dev/null | grep "^vm:" | cut -d: -f2' \
> "$TMP_DIR/ha_vms.txt" 2>/dev/null
export REAL_TMP_DIR="$TMP_DIR"
python3 << PYEOF
import json, sys, os
TMP_DIR = os.environ.get("REAL_TMP_DIR", "/tmp")
THRESHOLD_PCT = 100
MIN_FREE_GB = 4
MAX_MIGRATIONS = 2
with open(f"{TMP_DIR}/vms.json") as f:
vms_raw = json.load(f)
with open(f"{TMP_DIR}/nodes.json") as f:
nodes_raw = json.load(f)
# Parse HA rules — extract strict node-affinity constraints
vm_node_constraints = {}
current_rule = None
rules_path = f"{TMP_DIR}/rules.cfg"
if os.path.exists(rules_path):
with open(rules_path) as f:
for line in f:
line = line.strip()
if line.startswith("node-affinity:"):
if current_rule and current_rule.get("resources") and current_rule.get("strict") == 1:
for vid in current_rule["resources"]:
vm_node_constraints[vid] = set(current_rule["nodes"])
current_rule = {"nodes": [], "resources": [], "strict": 0}
elif current_rule is not None:
if line.startswith("nodes "):
current_rule["nodes"] = [n.strip() for n in line.split(None,1)[1].split(",")]
elif line.startswith("resources "):
for p in line.split(None,1)[1].split(","):
p = p.strip()
if ":" in p:
typ, vid = p.split(":")
if typ.strip() == "vm":
current_rule["resources"].append(int(vid.strip()))
elif line.startswith("strict "):
current_rule["strict"] = int(line.split()[1])
if current_rule and current_rule.get("resources") and current_rule.get("strict") == 1:
for vid in current_rule["resources"]:
vm_node_constraints[vid] = set(current_rule["nodes"])
ha_vms = set()
ha_path = f"{TMP_DIR}/ha_vms.txt"
if os.path.exists(ha_path):
with open(ha_path) as f:
for line in f:
vid = line.strip()
if vid.isdigit():
ha_vms.add(int(vid))
nodes = {}
for n in nodes_raw:
name = n.get("node", "")
maxmem = n.get("maxmem", 0)
if maxmem > 0:
nodes[name] = {
"maxmem": maxmem,
"maxmem_gb": round(maxmem / 1024**3, 1),
}
vms = []
node_allocated = {}
for v in vms_raw:
if v.get("status") != "running":
continue
vmid = v.get("vmid")
nodename = v.get("node", "")
maxmem = v.get("maxmem", 0)
name = v.get("name", "?")
if nodename in nodes and maxmem > 0:
vms.append({
"vmid": vmid,
"name": name,
"node": nodename,
"maxmem_mb": maxmem // 1024**2,
"maxmem_gb": round(maxmem / 1024**3, 1),
"ha_managed": vmid in ha_vms,
"constrained_nodes": vm_node_constraints.get(vmid),
})
node_allocated[nodename] = node_allocated.get(nodename, 0) + maxmem
for nodename, info in nodes.items():
allocated = node_allocated.get(nodename, 0)
info["allocated_mb"] = allocated // 1024**2
info["allocated_gb"] = round(allocated / 1024**3, 1)
max_mb = info["maxmem"] // 1024**2
info["overcommit_pct"] = round(info["allocated_mb"] / max_mb * 100) if max_mb > 0 else 0
info["free_for_vms_gb"] = round((info["maxmem"] - allocated) / 1024**3, 1)
actions = []
for nodename, info in sorted(nodes.items(), key=lambda x: x[1]["overcommit_pct"], reverse=True):
if info["overcommit_pct"] <= THRESHOLD_PCT:
break
node_vms = sorted([v for v in vms if v["node"] == nodename and v["ha_managed"]],
key=lambda x: x["maxmem_mb"], reverse=True)
for vm in node_vms:
allowed_targets = set(nodes.keys()) - {nodename}
if vm["constrained_nodes"]:
allowed_targets = vm["constrained_nodes"] - {nodename}
candidates = []
for n, ni in nodes.items():
if n not in allowed_targets:
continue
free_after = ni["free_for_vms_gb"] - vm["maxmem_gb"]
if ni["free_for_vms_gb"] >= MIN_FREE_GB and free_after >= 0:
new_target_alloc = ni["allocated_mb"] + vm["maxmem_mb"]
new_target_pct = round(new_target_alloc / (ni["maxmem"] // 1024**2) * 100)
if new_target_pct <= THRESHOLD_PCT:
candidates.append((n, ni, new_target_pct))
if candidates:
candidates.sort(key=lambda x: x[1]["free_for_vms_gb"], reverse=True)
target, target_info, new_target_pct = candidates[0]
new_source_pct = round((info["allocated_mb"] - vm["maxmem_mb"]) / (info["maxmem"] // 1024**2) * 100)
actions.append({
"vmid": vm["vmid"],
"name": vm["name"],
"from": nodename,
"to": target,
"ram_gb": vm["maxmem_gb"],
"source_before": info["overcommit_pct"],
"source_after": new_source_pct,
"target_before": target_info["overcommit_pct"],
"target_after": new_target_pct,
})
info["allocated_mb"] -= vm["maxmem_mb"]
info["free_for_vms_gb"] += vm["maxmem_gb"]
target_info["allocated_mb"] += vm["maxmem_mb"]
target_info["free_for_vms_gb"] -= vm["maxmem_gb"]
break
if not actions:
sys.exit(0)
for a in actions[:MAX_MIGRATIONS]:
print(f"MIGRATE {a['vmid']} {a['from']} {a['to']}")
print(f"DETAILS VM {a['vmid']} ({a['name']}, {a['ram_gb']}GB): "
f"{a['from']} ({a['source_before']}%\u2192{a['source_after']}%) \u2192 "
f"{a['to']} ({a['target_before']}%\u2192{a['target_after']}%)")
PYEOF