Add resource budget policy checks
This commit is contained in:
parent
59eda9feff
commit
e4ff0bac49
|
|
@ -277,6 +277,7 @@ Run a read-only health snapshot from the Debian server with:
|
||||||
./jeannie release-snapshot
|
./jeannie release-snapshot
|
||||||
./jeannie backup-status
|
./jeannie backup-status
|
||||||
./jeannie synthetic-checks
|
./jeannie synthetic-checks
|
||||||
|
./jeannie resource-budget
|
||||||
./jeannie workers list
|
./jeannie workers list
|
||||||
./jeannie change-journal list
|
./jeannie change-journal list
|
||||||
./jeannie map
|
./jeannie map
|
||||||
|
|
@ -328,6 +329,10 @@ Pi-hole DNS, Traefik LAN HTTP, and temporary Kubernetes DNS/ingress pods. The
|
||||||
registry push/pull smoke test is opt-in with
|
registry push/pull smoke test is opt-in with
|
||||||
`LAB_SYNTHETIC_REGISTRY_PUSH=true`.
|
`LAB_SYNTHETIC_REGISTRY_PUSH=true`.
|
||||||
|
|
||||||
|
`resource-budget` checks the report-only policy in `infra/resource-budgets.yml`
|
||||||
|
against Kubernetes pod requests/limits and Debian host disk/memory signals. Use
|
||||||
|
it before adding heavier workloads so the HP laptop remains usable.
|
||||||
|
|
||||||
`workers` provides named Kubernetes/Pimox worker lifecycle operations:
|
`workers` provides named Kubernetes/Pimox worker lifecycle operations:
|
||||||
`list`, `start`, `stop`, `drain`, `uncordon`, `recreate-plan`, and `rebalance`.
|
`list`, `start`, `stop`, `drain`, `uncordon`, `recreate-plan`, and `rebalance`.
|
||||||
Use `recreate-plan <index>` before replacing one broken Pimox worker so the
|
Use `recreate-plan <index>` before replacing one broken Pimox worker so the
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,7 @@ Run a read-only health snapshot from the Debian server with:
|
||||||
./{{ main_script }} release-snapshot
|
./{{ main_script }} release-snapshot
|
||||||
./{{ main_script }} backup-status
|
./{{ main_script }} backup-status
|
||||||
./{{ main_script }} synthetic-checks
|
./{{ main_script }} synthetic-checks
|
||||||
|
./{{ main_script }} resource-budget
|
||||||
./{{ main_script }} workers list
|
./{{ main_script }} workers list
|
||||||
./{{ main_script }} change-journal list
|
./{{ main_script }} change-journal list
|
||||||
./{{ main_script }} map
|
./{{ main_script }} map
|
||||||
|
|
@ -328,6 +329,10 @@ Pi-hole DNS, Traefik LAN HTTP, and temporary Kubernetes DNS/ingress pods. The
|
||||||
registry push/pull smoke test is opt-in with
|
registry push/pull smoke test is opt-in with
|
||||||
`LAB_SYNTHETIC_REGISTRY_PUSH=true`.
|
`LAB_SYNTHETIC_REGISTRY_PUSH=true`.
|
||||||
|
|
||||||
|
`resource-budget` checks the report-only policy in `infra/resource-budgets.yml`
|
||||||
|
against Kubernetes pod requests/limits and Debian host disk/memory signals. Use
|
||||||
|
it before adding heavier workloads so the HP laptop remains usable.
|
||||||
|
|
||||||
`workers` provides named Kubernetes/Pimox worker lifecycle operations:
|
`workers` provides named Kubernetes/Pimox worker lifecycle operations:
|
||||||
`list`, `start`, `stop`, `drain`, `uncordon`, `recreate-plan`, and `rebalance`.
|
`list`, `start`, `stop`, `drain`, `uncordon`, `recreate-plan`, and `rebalance`.
|
||||||
Use `recreate-plan <index>` before replacing one broken Pimox worker so the
|
Use `recreate-plan <index>` before replacing one broken Pimox worker so the
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,10 @@ includes this as a warning signal.
|
||||||
LAN HTTP, and temporary Kubernetes DNS/ingress pods. Registry push/pull is
|
LAN HTTP, and temporary Kubernetes DNS/ingress pods. Registry push/pull is
|
||||||
opt-in through `LAB_SYNTHETIC_REGISTRY_PUSH=true`.
|
opt-in through `LAB_SYNTHETIC_REGISTRY_PUSH=true`.
|
||||||
|
|
||||||
|
`resource-budget`
|
||||||
|
: Check the report-only resource policy in `infra/resource-budgets.yml` against
|
||||||
|
Kubernetes pod requests/limits and Debian disk/memory signals.
|
||||||
|
|
||||||
`workers <command>`
|
`workers <command>`
|
||||||
: Manage Kubernetes/Pimox worker lifecycle operations. Commands include `list`,
|
: Manage Kubernetes/Pimox worker lifecycle operations. Commands include `list`,
|
||||||
`start`, `stop`, `drain`, `uncordon`, `recreate-plan`, and `rebalance`.
|
`start`, `stop`, `drain`, `uncordon`, `recreate-plan`, and `rebalance`.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Report-only resource budgets for the current homelab hardware.
|
||||||
|
# Tune these as the HP laptop, Pimox workers, and Raspberry Pi roles change.
|
||||||
|
cluster:
|
||||||
|
max_total_cpu_millicores: 9000
|
||||||
|
max_total_memory_mib: 22000
|
||||||
|
require_requests: true
|
||||||
|
require_limits: true
|
||||||
|
namespaces:
|
||||||
|
website-production:
|
||||||
|
max_cpu_millicores: 1000
|
||||||
|
max_memory_mib: 1024
|
||||||
|
demos-static:
|
||||||
|
max_cpu_millicores: 1000
|
||||||
|
max_memory_mib: 1024
|
||||||
|
argocd:
|
||||||
|
max_cpu_millicores: 2000
|
||||||
|
max_memory_mib: 3072
|
||||||
|
traefik:
|
||||||
|
max_cpu_millicores: 1000
|
||||||
|
max_memory_mib: 1024
|
||||||
|
hosts:
|
||||||
|
debian:
|
||||||
|
min_root_free_percent: 15
|
||||||
|
min_data_free_percent: 20
|
||||||
|
max_memory_used_percent: 80
|
||||||
|
rpi4:
|
||||||
|
min_docker_free_percent: 15
|
||||||
|
pimox:
|
||||||
|
min_worker_storage_free_percent: 15
|
||||||
10
jeannie
10
jeannie
|
|
@ -5997,6 +5997,11 @@ synthetic_checks() {
|
||||||
"${REPO_ROOT}/scripts/synthetic-checks"
|
"${REPO_ROOT}/scripts/synthetic-checks"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource_budget() {
|
||||||
|
require_debian_server "resource-budget"
|
||||||
|
"${REPO_ROOT}/scripts/resource-budget"
|
||||||
|
}
|
||||||
|
|
||||||
workers_manage() {
|
workers_manage() {
|
||||||
require_debian_server "workers"
|
require_debian_server "workers"
|
||||||
"${REPO_ROOT}/scripts/workers" "${@:2}"
|
"${REPO_ROOT}/scripts/workers" "${@:2}"
|
||||||
|
|
@ -6062,6 +6067,9 @@ case "${1:-}" in
|
||||||
synthetic-checks)
|
synthetic-checks)
|
||||||
synthetic_checks
|
synthetic_checks
|
||||||
;;
|
;;
|
||||||
|
resource-budget)
|
||||||
|
resource_budget
|
||||||
|
;;
|
||||||
workers)
|
workers)
|
||||||
workers_manage "$@"
|
workers_manage "$@"
|
||||||
;;
|
;;
|
||||||
|
|
@ -6219,7 +6227,7 @@ case "${1:-}" in
|
||||||
echo "Log: ${JEANNIE_LOG_FILE}"
|
echo "Log: ${JEANNIE_LOG_FILE}"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: $0 {up|plan [all|provisioning|cluster|platform|apps|edge]|rebuild-cluster|stop-cluster|start-cluster|status|capacity|recover-plan|gitops-status|cert-check|release-snapshot|backup-status|synthetic-checks|workers <list|start|stop|drain|uncordon|recreate-plan|rebalance>|change-journal {list|path}|map [--dot]|validate|access-audit|kubeconfig-readonly|apps|website-translation-model|website-ollama-listen|ollama-setup|deploy-gitea|rpi-services|bootstrap-gitea-repo|backup-gitea|drill-restore|drill-gitea-restore|drill-pihole-restore|install-gitea-runner|move-prometheus-stack-workers|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|state-backup|fix-debian-docker-root|secrets-init|secrets-check|tailnet-policy-check|ai-index|ai-check|security-scan|security-prepare|security-zap|security-k8s|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-runtime|security-attack-path|openwrt|nuke}"
|
echo "Usage: $0 {up|plan [all|provisioning|cluster|platform|apps|edge]|rebuild-cluster|stop-cluster|start-cluster|status|capacity|recover-plan|gitops-status|cert-check|release-snapshot|backup-status|synthetic-checks|resource-budget|workers <list|start|stop|drain|uncordon|recreate-plan|rebalance>|change-journal {list|path}|map [--dot]|validate|access-audit|kubeconfig-readonly|apps|website-translation-model|website-ollama-listen|ollama-setup|deploy-gitea|rpi-services|bootstrap-gitea-repo|backup-gitea|drill-restore|drill-gitea-restore|drill-pihole-restore|install-gitea-runner|move-prometheus-stack-workers|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|state-backup|fix-debian-docker-root|secrets-init|secrets-check|tailnet-policy-check|ai-index|ai-check|security-scan|security-prepare|security-zap|security-k8s|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-runtime|security-attack-path|openwrt|nuke}"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
BUDGET_FILE="${HOMELAB_RESOURCE_BUDGET_FILE:-${REPO_ROOT}/infra/resource-budgets.yml}"
|
||||||
|
KUBECONFIG_PATH="${KUBECONFIG:-${LAB_KUBECONFIG_PATH:-/home/jv/.kube/config}}"
|
||||||
|
|
||||||
|
failures=0
|
||||||
|
warnings=0
|
||||||
|
|
||||||
|
section() {
|
||||||
|
printf '\n== %s ==\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
warnings=$((warnings + 1))
|
||||||
|
printf 'warn - %s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
failures=$((failures + 1))
|
||||||
|
printf 'fail - %s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
have() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
host_budget() {
|
||||||
|
section "Host Budget"
|
||||||
|
|
||||||
|
if have free; then
|
||||||
|
free -m | awk '/^Mem:/ {
|
||||||
|
used_pct = int(($3 / $2) * 100)
|
||||||
|
printf "Debian memory used %s%%\n", used_pct
|
||||||
|
if (used_pct > 80) exit 2
|
||||||
|
}' || warn "Debian memory usage is above the default 80% budget"
|
||||||
|
else
|
||||||
|
warn "free command unavailable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if have df; then
|
||||||
|
df -P / /data 2>/dev/null | awk 'NR > 1 { gsub("%", "", $5); free = 100 - $5; printf "%-25s free=%s%%\n", $6, free }' || true
|
||||||
|
else
|
||||||
|
warn "df command unavailable"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
kubernetes_budget() {
|
||||||
|
section "Kubernetes Budget"
|
||||||
|
|
||||||
|
if ! have kubectl || [ ! -s "$KUBECONFIG_PATH" ]; then
|
||||||
|
warn "kubectl or kubeconfig unavailable; skipping Kubernetes budget checks"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! kubectl --kubeconfig "$KUBECONFIG_PATH" get --raw=/readyz >/dev/null 2>&1; then
|
||||||
|
warn "Kubernetes API unavailable; skipping Kubernetes budget checks"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "$BUDGET_FILE" "$KUBECONFIG_PATH" <<'PY'
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
budget_file, kubeconfig = sys.argv[1:3]
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
yaml = None
|
||||||
|
|
||||||
|
def load_budget(path):
|
||||||
|
if yaml:
|
||||||
|
with open(path, encoding="utf-8") as handle:
|
||||||
|
return yaml.safe_load(handle) or {}
|
||||||
|
return {
|
||||||
|
"cluster": {
|
||||||
|
"max_total_cpu_millicores": 9000,
|
||||||
|
"max_total_memory_mib": 22000,
|
||||||
|
"require_requests": True,
|
||||||
|
"require_limits": True,
|
||||||
|
},
|
||||||
|
"namespaces": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
def cpu_to_m(value):
|
||||||
|
if not value:
|
||||||
|
return 0
|
||||||
|
text = str(value)
|
||||||
|
if text.endswith("m"):
|
||||||
|
return int(text[:-1])
|
||||||
|
return int(float(text) * 1000)
|
||||||
|
|
||||||
|
def mem_to_mib(value):
|
||||||
|
if not value:
|
||||||
|
return 0
|
||||||
|
text = str(value)
|
||||||
|
units = {
|
||||||
|
"Ki": 1 / 1024,
|
||||||
|
"Mi": 1,
|
||||||
|
"Gi": 1024,
|
||||||
|
"Ti": 1024 * 1024,
|
||||||
|
"K": 1 / 1000,
|
||||||
|
"M": 1000 / 1024,
|
||||||
|
"G": 1000 * 1000 / 1024,
|
||||||
|
}
|
||||||
|
for suffix, multiplier in units.items():
|
||||||
|
if text.endswith(suffix):
|
||||||
|
return int(float(text[: -len(suffix)]) * multiplier)
|
||||||
|
return int(float(text) / (1024 * 1024))
|
||||||
|
|
||||||
|
budget = load_budget(budget_file)
|
||||||
|
cluster_budget = budget.get("cluster", {})
|
||||||
|
namespace_budgets = budget.get("namespaces", {})
|
||||||
|
raw = subprocess.check_output(
|
||||||
|
["kubectl", "--kubeconfig", kubeconfig, "get", "pods", "-A", "-o", "json"],
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
pods = json.loads(raw).get("items", [])
|
||||||
|
namespace_totals = {}
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
for pod in pods:
|
||||||
|
ns = pod["metadata"]["namespace"]
|
||||||
|
pod_name = pod["metadata"]["name"]
|
||||||
|
namespace_totals.setdefault(ns, {"cpu": 0, "memory": 0})
|
||||||
|
for container in pod.get("spec", {}).get("containers", []):
|
||||||
|
name = container["name"]
|
||||||
|
resources = container.get("resources", {})
|
||||||
|
requests = resources.get("requests", {})
|
||||||
|
limits = resources.get("limits", {})
|
||||||
|
namespace_totals[ns]["cpu"] += cpu_to_m(requests.get("cpu"))
|
||||||
|
namespace_totals[ns]["memory"] += mem_to_mib(requests.get("memory"))
|
||||||
|
if cluster_budget.get("require_requests", True) and ("cpu" not in requests or "memory" not in requests):
|
||||||
|
missing.append(f"{ns}/{pod_name}:{name} missing requests")
|
||||||
|
if cluster_budget.get("require_limits", True) and ("cpu" not in limits or "memory" not in limits):
|
||||||
|
missing.append(f"{ns}/{pod_name}:{name} missing limits")
|
||||||
|
|
||||||
|
total_cpu = sum(item["cpu"] for item in namespace_totals.values())
|
||||||
|
total_memory = sum(item["memory"] for item in namespace_totals.values())
|
||||||
|
print(f"cluster_requests_cpu_m={total_cpu}")
|
||||||
|
print(f"cluster_requests_memory_mib={total_memory}")
|
||||||
|
|
||||||
|
exit_code = 0
|
||||||
|
if total_cpu > int(cluster_budget.get("max_total_cpu_millicores", 9000)):
|
||||||
|
print("fail - cluster CPU requests exceed budget")
|
||||||
|
exit_code = 1
|
||||||
|
if total_memory > int(cluster_budget.get("max_total_memory_mib", 22000)):
|
||||||
|
print("fail - cluster memory requests exceed budget")
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
|
for ns, totals in sorted(namespace_totals.items()):
|
||||||
|
ns_budget = namespace_budgets.get(ns)
|
||||||
|
if not ns_budget:
|
||||||
|
continue
|
||||||
|
print(f"{ns}: cpu_m={totals['cpu']} memory_mib={totals['memory']}")
|
||||||
|
if totals["cpu"] > int(ns_budget.get("max_cpu_millicores", 999999)):
|
||||||
|
print(f"fail - {ns} CPU requests exceed budget")
|
||||||
|
exit_code = 1
|
||||||
|
if totals["memory"] > int(ns_budget.get("max_memory_mib", 999999)):
|
||||||
|
print(f"fail - {ns} memory requests exceed budget")
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
print("missing_resource_policy:")
|
||||||
|
for item in missing[:50]:
|
||||||
|
print(f" {item}")
|
||||||
|
if len(missing) > 50:
|
||||||
|
print(f" ... {len(missing) - 50} more")
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
|
raise SystemExit(exit_code)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
section "Policy"
|
||||||
|
if [ -s "$BUDGET_FILE" ]; then
|
||||||
|
printf 'budget_file=%s\n' "$BUDGET_FILE"
|
||||||
|
else
|
||||||
|
fail "budget file is missing: $BUDGET_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
host_budget
|
||||||
|
kubernetes_budget || failures=$((failures + 1))
|
||||||
|
|
||||||
|
section "Summary"
|
||||||
|
printf 'failures=%s warnings=%s\n' "$failures" "$warnings"
|
||||||
|
|
||||||
|
if [ "$failures" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Loading…
Reference in New Issue