Compare commits

...

6 Commits

Author SHA1 Message Date
juvdiaz fb57233ce8 Add Jeannie last change review 2026-06-29 20:50:56 -06:00
juvdiaz 1954a2e975 Add capacity limit recommendations 2026-06-29 20:48:17 -06:00
juvdiaz eabc5b049b Add Jeannie capacity advisor 2026-06-29 20:46:15 -06:00
juvdiaz 31723307d5 Add Jeannie change impact analysis 2026-06-29 20:44:49 -06:00
juvdiaz 791c9d5822 Add Jeannie ask command finder 2026-06-29 20:43:08 -06:00
juvdiaz d0004d99f8 Improve Jeannie explain readability 2026-06-29 20:37:03 -06:00
13 changed files with 642 additions and 30 deletions

View File

@ -272,8 +272,12 @@ Run a read-only health snapshot from the Debian server with:
./jeannie help
./jeannie status
./jeannie capacity
./jeannie capacity-advisor
./jeannie capacity-limits
./jeannie recover-plan
./jeannie recover-power --dry-run
./jeannie impact --since HEAD~1
./jeannie review-last-change
./jeannie scorecard
./jeannie explain scorecard
./jeannie gitops-status
@ -322,6 +326,15 @@ Kubernetes, Pimox, RPi, and placement signals without dumping full details. Use
`./jeannie capacity --details`, `--json`, `--only problems`, or
`--verbose` for the older full capacity dump.
`capacity-advisor` interprets capacity, resource-budget, and control-plane
placement signals to recommend whether to add Pimox VMs, taint Debian, or fix
resource definitions first.
`capacity-limits` prints suggested Kubernetes `resources.requests` and
`resources.limits` YAML snippets for containers missing them. It uses
`infra/resource-recommendations.yml` as a conservative starting profile and does
not edit manifests.
`recover-plan` prints the disaster recovery order and lightweight prerequisite
checks, from Debian and Gitea through DNS, Pimox, Kubernetes, GitOps apps, and
edge verification.
@ -330,6 +343,13 @@ edge verification.
Debian runtime, Gitea, RPi DNS, Pimox workers, Kubernetes, GitOps/apps, and
edge/public checks. Use `recover-power --dry-run` to preview the sequence.
`impact` explains what a change may touch before you apply it. Use
`./jeannie impact --since HEAD~1` for the last commit or pass paths
like `bootstrap/edge` and `apps/website`.
`review-last-change` summarizes the last commit, runs impact analysis for the
touched files, and prints validation plus rollback hints.
`scorecard` rolls up backup readiness, GitOps health, DNS/RPi health, cluster
health, edge health, capacity pressure, security posture, public certificates,
inventory, and docs freshness into a compact pass/warn/fail report.
@ -1089,12 +1109,16 @@ Build the local homelab knowledge index separately from the main deployment:
```bash
./jeannie ai-index
./jeannie ai-check
./jeannie ask "how do I check edge to Gitea?"
```
The index is built from the non-secret source list in
`infra/ai/knowledge-sources.txt` and defaults to `/data/homelab-ai/index`.
Doctor commands use it as extra context when it exists, but infrastructure
deployment does not depend on it.
Use `ask` as the runbook and command finder. It returns the closest indexed
docs/scripts by default; set `LAB_AI_ASK_LLM=true` to ask Ollama after
retrieval.
The CV page has two client-side presentation modes:

View File

@ -272,8 +272,12 @@ Run a read-only health snapshot from the Debian server with:
./{{ main_script }} help
./{{ main_script }} status
./{{ main_script }} capacity
./{{ main_script }} capacity-advisor
./{{ main_script }} capacity-limits
./{{ main_script }} recover-plan
./{{ main_script }} recover-power --dry-run
./{{ main_script }} impact --since HEAD~1
./{{ main_script }} review-last-change
./{{ main_script }} scorecard
./{{ main_script }} explain scorecard
./{{ main_script }} gitops-status
@ -322,6 +326,15 @@ Kubernetes, Pimox, RPi, and placement signals without dumping full details. Use
`./{{ main_script }} capacity --details`, `--json`, `--only problems`, or
`--verbose` for the older full capacity dump.
`capacity-advisor` interprets capacity, resource-budget, and control-plane
placement signals to recommend whether to add Pimox VMs, taint Debian, or fix
resource definitions first.
`capacity-limits` prints suggested Kubernetes `resources.requests` and
`resources.limits` YAML snippets for containers missing them. It uses
`infra/resource-recommendations.yml` as a conservative starting profile and does
not edit manifests.
`recover-plan` prints the disaster recovery order and lightweight prerequisite
checks, from Debian and Gitea through DNS, Pimox, Kubernetes, GitOps apps, and
edge verification.
@ -330,6 +343,13 @@ edge verification.
Debian runtime, Gitea, RPi DNS, Pimox workers, Kubernetes, GitOps/apps, and
edge/public checks. Use `recover-power --dry-run` to preview the sequence.
`impact` explains what a change may touch before you apply it. Use
`./{{ main_script }} impact --since HEAD~1` for the last commit or pass paths
like `bootstrap/edge` and `apps/website`.
`review-last-change` summarizes the last commit, runs impact analysis for the
touched files, and prints validation plus rollback hints.
`scorecard` rolls up backup readiness, GitOps health, DNS/RPi health, cluster
health, edge health, capacity pressure, security posture, public certificates,
inventory, and docs freshness into a compact pass/warn/fail report.
@ -1089,12 +1109,16 @@ Build the local homelab knowledge index separately from the main deployment:
```bash
./{{ main_script }} ai-index
./{{ main_script }} ai-check
./{{ main_script }} ask "how do I check edge to Gitea?"
```
The index is built from the non-secret source list in
`infra/ai/knowledge-sources.txt` and defaults to `/data/homelab-ai/index`.
Doctor commands use it as extra context when it exists, but infrastructure
deployment does not depend on it.
Use `ask` as the runbook and command finder. It returns the closest indexed
docs/scripts by default; set `LAB_AI_ASK_LLM=true` to ask Ollama after
retrieval.
The CV page has two client-side presentation modes:

View File

@ -66,6 +66,16 @@ deployment readiness, pod restarts, node pressure, disk pressure, Traefik
Pimox, RPi, and placement signals. Use `capacity --details`, `--json`, `--only
problems`, or `--verbose` for the older full dump.
`capacity-advisor`
: Interpret capacity, resource-budget, and control-plane placement signals to
recommend whether to add Pimox VMs, taint Debian, or fix resource definitions
first.
`capacity-limits [namespace]`
: Recommend Kubernetes `resources.requests` and `resources.limits` YAML snippets
for containers missing them. Uses `infra/resource-recommendations.yml` and does
not edit manifests.
`recover-plan`
: Print the ordered disaster recovery checklist and lightweight prerequisite
checks for Debian, Gitea, RPi DNS, Pimox, Kubernetes, GitOps apps, and edge
@ -104,6 +114,14 @@ and DuckDNS-to-OCI edge IP drift from the canonical inventory.
state, workload images, public URL status, and latest Gitea/OpenTofu backup
pointers.
`impact [--since REF] [PATH...]`
: Explain which homelab areas are affected by changed files or explicit paths,
including risk, touched services, validation commands, and apply path.
`review-last-change [REF]`
: Summarize a commit, list touched files, run impact analysis, and print generic
validation plus rollback hints. Defaults to `HEAD`.
`backup-status`
: Report freshness for Gitea backups, OpenTofu state backups, restore drill
reports, and repo-managed Pi-hole restore inputs. The main `status` cascade
@ -275,6 +293,10 @@ structure.
: Check local AI helper prerequisites, knowledge index, and Ollama availability
when the backstage helper is enabled.
`ask QUESTION...`
: Search the local homelab knowledge index for relevant docs, runbooks, scripts,
and commands. Set `LAB_AI_ASK_LLM=true` to ask Ollama after retrieval.
### Defensive Security
`security-scan`

View File

@ -2,6 +2,7 @@ pattern meaning diagnose_commands jeannie_fix_commands other_fix_commands risk a
unsupported template variable Generated docs found a template placeholder that render-docs does not support. ./jeannie validate none Edit README.md.tmpl or scripts/render-docs low no Usually a docs/template mismatch rather than an infrastructure issue.
generated README.md is stale README.md does not match README.md.tmpl rendering. ./jeannie validate none scripts/render-docs low candidate A future docs-render Jeannie command could safely wrap this.
missing_resource_policy Workloads are missing resource requests or limits, so capacity planning is unreliable. ./jeannie resource-budget;./jeannie capacity none Edit app manifests or Helm values medium no Add requests first, then limits for workloads you own.
pods missing requests/limits Some Kubernetes pods are missing resource requests or limits, so the scheduler and capacity reports cannot fully trust workload sizing. ./jeannie resource-budget --details;./jeannie capacity none Add requests/limits in the app manifests, Helm values, or platform chart values medium no Yes, this is fixed from code/config by adding resources to workloads you own; avoid auto-generating limits blindly.
missing requests One or more containers do not declare CPU or memory requests. ./jeannie resource-budget --details;./jeannie capacity none Edit app manifests or Helm values medium no Start with application namespaces before system namespaces.
missing limits One or more containers do not declare CPU or memory limits. ./jeannie resource-budget --details;./jeannie capacity none Edit app manifests or Helm values medium no Limits are useful but can cause throttling; set them deliberately.
resource policy Capacity or resource policy needs attention. ./jeannie resource-budget;./jeannie capacity none Edit infra/resource-budgets.yml or app manifests medium no Do not hide pressure by only changing the budget file.

1 pattern meaning diagnose_commands jeannie_fix_commands other_fix_commands risk auto_fix notes
2 unsupported template variable Generated docs found a template placeholder that render-docs does not support. ./jeannie validate none Edit README.md.tmpl or scripts/render-docs low no Usually a docs/template mismatch rather than an infrastructure issue.
3 generated README.md is stale README.md does not match README.md.tmpl rendering. ./jeannie validate none scripts/render-docs low candidate A future docs-render Jeannie command could safely wrap this.
4 missing_resource_policy Workloads are missing resource requests or limits, so capacity planning is unreliable. ./jeannie resource-budget;./jeannie capacity none Edit app manifests or Helm values medium no Add requests first, then limits for workloads you own.
5 pods missing requests/limits Some Kubernetes pods are missing resource requests or limits, so the scheduler and capacity reports cannot fully trust workload sizing. ./jeannie resource-budget --details;./jeannie capacity none Add requests/limits in the app manifests, Helm values, or platform chart values medium no Yes, this is fixed from code/config by adding resources to workloads you own; avoid auto-generating limits blindly.
6 missing requests One or more containers do not declare CPU or memory requests. ./jeannie resource-budget --details;./jeannie capacity none Edit app manifests or Helm values medium no Start with application namespaces before system namespaces.
7 missing limits One or more containers do not declare CPU or memory limits. ./jeannie resource-budget --details;./jeannie capacity none Edit app manifests or Helm values medium no Limits are useful but can cause throttling; set them deliberately.
8 resource policy Capacity or resource policy needs attention. ./jeannie resource-budget;./jeannie capacity none Edit infra/resource-budgets.yml or app manifests medium no Do not hide pressure by only changing the budget file.

View File

@ -0,0 +1,17 @@
pattern area risk touches validate apply
bootstrap/edge/ Edge/high public routing high OCI edge nginx;public website route;Gitea public route;Tailscale backend path ./jeannie doctor-edge;./jeannie cert-check;curl -I https://lab2025.duckdns.org/ ./jeannie plan edge
bootstrap/cluster/ Kubernetes cluster high kubeadm control plane;worker joins;CNI;cluster state ./jeannie doctor-cluster;./jeannie status ./jeannie plan cluster
bootstrap/platform/ Platform services medium Argo CD;monitoring;registry;Traefik dependencies ./jeannie status;./jeannie gitops-status;./jeannie grafana-dashboards list ./jeannie plan platform
bootstrap/provisioning/ Pimox provisioning high Pimox template;worker VMs;preseed/PXE ./jeannie preflight;./jeannie doctor-cluster ./jeannie plan provisioning
apps/ Applications medium GitOps apps;website;security lab;dashboards ./jeannie gitops-status;./jeannie status ./jeannie apps
apps/website/ Public website medium website deployment;public route;image build;Redis/Ollama dependencies ./jeannie status;./jeannie cert-check ./jeannie apps
infra/rpi-services/ RPi DNS services medium Pi-hole;Unbound;Uptime Kuma;RPi Docker ./jeannie doctor-rpi;./jeannie status ./jeannie rpi-services
infra/gitea/ Gitea medium Gitea Docker stack;repo bootstrap;public /git route ./jeannie doctor-gitea;./jeannie doctor-edge ./jeannie deploy-gitea
infra/arr-stack/ ARR stack medium Debian Docker ARR services;volumes under /data/arr ./jeannie status manual docker compose in infra/arr-stack
infra/tailscale/ Tailscale policy medium Tailnet ACLs;routes;peer access ./jeannie tailnet-policy-check;./jeannie doctor-edge manual tailnet policy apply
infra/resource-budgets.yml Capacity policy low resource budget reports;capacity warnings ./jeannie resource-budget;./jeannie capacity none
homelab.yml Canonical inventory high all rendered docs;scripts;edge/Gitea/RPi/Pimox IPs ./jeannie inventory-check;./jeannie validate;./jeannie preflight none
jeannie Main operator script high all Jeannie commands and pipeline flow bash -n jeannie;shellcheck jeannie;./jeannie help none
scripts/ Jeannie helper scripts medium reporting;doctors;automation helpers bash -n scripts/*;shellcheck scripts/* none
README.md.tmpl Generated docs low README.md;operator documentation scripts/render-docs --check scripts/render-docs
docs/ Documentation low runbooks;manual pages scripts/render-docs --check none
1 pattern area risk touches validate apply
2 bootstrap/edge/ Edge/high public routing high OCI edge nginx;public website route;Gitea public route;Tailscale backend path ./jeannie doctor-edge;./jeannie cert-check;curl -I https://lab2025.duckdns.org/ ./jeannie plan edge
3 bootstrap/cluster/ Kubernetes cluster high kubeadm control plane;worker joins;CNI;cluster state ./jeannie doctor-cluster;./jeannie status ./jeannie plan cluster
4 bootstrap/platform/ Platform services medium Argo CD;monitoring;registry;Traefik dependencies ./jeannie status;./jeannie gitops-status;./jeannie grafana-dashboards list ./jeannie plan platform
5 bootstrap/provisioning/ Pimox provisioning high Pimox template;worker VMs;preseed/PXE ./jeannie preflight;./jeannie doctor-cluster ./jeannie plan provisioning
6 apps/ Applications medium GitOps apps;website;security lab;dashboards ./jeannie gitops-status;./jeannie status ./jeannie apps
7 apps/website/ Public website medium website deployment;public route;image build;Redis/Ollama dependencies ./jeannie status;./jeannie cert-check ./jeannie apps
8 infra/rpi-services/ RPi DNS services medium Pi-hole;Unbound;Uptime Kuma;RPi Docker ./jeannie doctor-rpi;./jeannie status ./jeannie rpi-services
9 infra/gitea/ Gitea medium Gitea Docker stack;repo bootstrap;public /git route ./jeannie doctor-gitea;./jeannie doctor-edge ./jeannie deploy-gitea
10 infra/arr-stack/ ARR stack medium Debian Docker ARR services;volumes under /data/arr ./jeannie status manual docker compose in infra/arr-stack
11 infra/tailscale/ Tailscale policy medium Tailnet ACLs;routes;peer access ./jeannie tailnet-policy-check;./jeannie doctor-edge manual tailnet policy apply
12 infra/resource-budgets.yml Capacity policy low resource budget reports;capacity warnings ./jeannie resource-budget;./jeannie capacity none
13 homelab.yml Canonical inventory high all rendered docs;scripts;edge/Gitea/RPi/Pimox IPs ./jeannie inventory-check;./jeannie validate;./jeannie preflight none
14 jeannie Main operator script high all Jeannie commands and pipeline flow bash -n jeannie;shellcheck jeannie;./jeannie help none
15 scripts/ Jeannie helper scripts medium reporting;doctors;automation helpers bash -n scripts/*;shellcheck scripts/* none
16 README.md.tmpl Generated docs low README.md;operator documentation scripts/render-docs --check scripts/render-docs
17 docs/ Documentation low runbooks;manual pages scripts/render-docs --check none

View File

@ -0,0 +1,32 @@
defaults:
request_cpu_m: 50
request_memory_mib: 128
limit_cpu_multiplier: 4
limit_memory_multiplier: 2
namespaces:
website-production:
request_cpu_m: 75
request_memory_mib: 128
limit_cpu_multiplier: 4
limit_memory_multiplier: 2
demos-static:
request_cpu_m: 25
request_memory_mib: 64
limit_cpu_multiplier: 4
limit_memory_multiplier: 2
heimdall:
request_cpu_m: 50
request_memory_mib: 128
limit_cpu_multiplier: 4
limit_memory_multiplier: 2
arr-stack:
request_cpu_m: 150
request_memory_mib: 512
limit_cpu_multiplier: 3
limit_memory_multiplier: 2
argocd:
request_cpu_m: 100
request_memory_mib: 256
limit_cpu_multiplier: 3
limit_memory_multiplier: 2

43
jeannie
View File

@ -5866,6 +5866,19 @@ PY
echo "AI check passed."
}
ask_homelab() {
require_debian_server "ask"
"${REPO_ROOT}/scripts/ask" "${@:2}"
}
impact() {
"${REPO_ROOT}/scripts/impact" "${@:2}"
}
review_last_change() {
"${REPO_ROOT}/scripts/review-last-change" "${@:2}"
}
security_scan() {
require_debian_server "security-scan"
"${REPO_ROOT}/scripts/security-scan" all
@ -6002,6 +6015,16 @@ resource_budget() {
"${REPO_ROOT}/scripts/resource-budget" "${@:2}"
}
capacity_advisor() {
require_debian_server "capacity-advisor"
"${REPO_ROOT}/scripts/capacity-advisor" "${@:2}"
}
capacity_limits() {
require_debian_server "capacity-limits"
"${REPO_ROOT}/scripts/capacity-limits" "${@:2}"
}
control_plane() {
require_debian_server "control-plane"
"${REPO_ROOT}/scripts/control-plane" "${@:2}"
@ -6118,8 +6141,12 @@ Operator View And Reports
status Cascading health check from host to public URLs.
scorecard Compact pass/warn/fail operator scorecard.
capacity Compact capacity and placement report.
capacity-advisor Decide whether to add VMs or fix placement/resources.
capacity-limits [namespace] Recommend requests/limits YAML snippets.
recover-plan Print disaster recovery order and prerequisites.
recover-power [--dry-run] Run or preview post-outage recovery.
impact [--since REF] [PATH...] Explain affected lab areas before changes.
review-last-change [REF] Summarize changed files, impact, validation.
map [--dot] Print the homelab dependency map.
change-journal {list|path} Show risky-command journal entries.
release-snapshot Write a pre-change release snapshot.
@ -6181,6 +6208,7 @@ Security Learning
AI And Indexing
ai-index Build the local homelab RAG index.
ai-check Query/check the local AI index.
ask QUESTION... Find relevant docs/runbooks/commands.
Destructive
nuke Guarded cluster state destruction path.
@ -6216,6 +6244,12 @@ case "${1:-}" in
capacity)
capacity_report "$@"
;;
capacity-advisor)
capacity_advisor "$@"
;;
capacity-limits)
capacity_limits "$@"
;;
recover-plan)
recover_plan
;;
@ -6380,6 +6414,15 @@ case "${1:-}" in
ai-check)
ai_check
;;
ask)
ask_homelab "$@"
;;
impact)
impact "$@"
;;
review-last-change)
review_last_change "$@"
;;
security-scan)
security_scan
;;

26
scripts/ask Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
usage() {
cat <<'EOF'
Usage: ./jeannie ask QUESTION...
Search the homelab knowledge index for the closest docs, runbooks, scripts, and
Jeannie commands. Set LAB_AI_ASK_LLM=true to ask Ollama after retrieval.
EOF
}
case "${1:-}" in
""|-h|--help|help)
usage
exit 0
;;
esac
if [ "${LAB_AI_ASK_LLM:-false}" = "true" ]; then
exec "${REPO_ROOT}/scripts/query-homelab-ai-index" --ask "$@"
fi
exec "${REPO_ROOT}/scripts/query-homelab-ai-index" --context-only "$@"

97
scripts/capacity-advisor Executable file
View File

@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
run_check() {
local name="$1"
shift
echo "## ${name}"
set +e
"$@" 2>&1
local status=$?
set -e
echo "exit_status=${status}"
}
tmp_file="$(mktemp "${TMPDIR:-/tmp}/jeannie-capacity-advisor.XXXXXX")"
trap 'rm -f "$tmp_file"' EXIT
{
run_check "capacity" "${REPO_ROOT}/scripts/capacity-report" --only problems
run_check "resource-budget" "${REPO_ROOT}/scripts/resource-budget"
run_check "control-plane" "${REPO_ROOT}/scripts/control-plane" status
} >"$tmp_file"
python3 - "$tmp_file" <<'PY'
import re
import sys
text = open(sys.argv[1], encoding="utf-8").read()
lowered = text.lower()
missing_match = re.search(r"(\d+)\s+pods missing requests/limits", text)
missing_count = int(missing_match.group(1)) if missing_match else 0
pressure = any(token in lowered for token in ["diskpressure", "memorypressure", "node pressure"])
worker_storage_warn = "worker storage" in lowered and "warn" in lowered
non_system_control_plane = False
control_plane_section = text.split("## control-plane", 1)[-1]
for line in control_plane_section.splitlines():
if re.match(r"\s*(argocd|website-production|demos-static|heimdall|arr-stack|security-lab|monitoring)\s+", line):
non_system_control_plane = True
break
print("Jeannie Capacity Advisor")
print("========================")
recommendations = []
if missing_count:
recommendations.append(
(
"Fix resource requests/limits first",
f"{missing_count} pods are missing requests/limits, so scheduling and capacity math are not trustworthy yet.",
"./jeannie resource-budget --details",
"code/config: add resources to app manifests or Helm values",
)
)
if non_system_control_plane:
recommendations.append(
(
"Keep app/data-plane pods off Debian",
"Non-system workloads are still scheduled on the Debian control-plane host.",
"./jeannie control-plane taint",
"scheduling: taint Debian after Pimox workers are Ready",
)
)
if pressure or worker_storage_warn:
recommendations.append(
(
"Consider adding or recreating Pimox worker capacity",
"Cluster pressure or worker storage pressure is visible after current placement checks.",
"./jeannie workers list",
"capacity: increase LAB_PIMOX_WORKER_COUNT or recreate the pressured worker",
)
)
if not recommendations:
recommendations.append(
(
"No new Pimox VM needed from current signals",
"No capacity warning, node pressure, or control-plane placement issue was detected in the sampled reports.",
"./jeannie capacity",
"watch: rerun after adding workloads",
)
)
for index, (title, why, command, fix) in enumerate(recommendations, start=1):
print(f"\n{index}. {title}")
print(f" Why: {why}")
print(f" Next command: {command}")
print(f" Fix path: {fix}")
print("\nRule of thumb")
print("- Add requests before deciding you need more VMs.")
print("- Taint Debian once Pimox workers are stable.")
print("- Add a Pimox VM only when real pressure remains after placement and requests are cleaned up.")
PY

129
scripts/capacity-limits Executable file
View File

@ -0,0 +1,129 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
KUBECONFIG_PATH="${KUBECONFIG:-${LAB_KUBECONFIG_PATH:-/home/jv/.kube/config}}"
PROFILE_FILE="${HOMELAB_RESOURCE_RECOMMENDATIONS:-${REPO_ROOT}/infra/resource-recommendations.yml}"
NAMESPACE="${1:-}"
PODS_JSON=""
cleanup() {
if [ -n "$PODS_JSON" ] && [ -f "$PODS_JSON" ]; then
rm -f "$PODS_JSON"
fi
}
trap cleanup EXIT
usage() {
cat <<'EOF'
Usage: ./jeannie capacity-limits [namespace]
Recommend Kubernetes requests/limits for containers missing them. This command
prints YAML snippets only; it does not edit manifests.
EOF
}
case "${1:-}" in
-h|--help|help)
usage
exit 0
;;
esac
if ! command -v kubectl >/dev/null 2>&1; then
echo "kubectl is required." >&2
exit 1
fi
if [ ! -s "$KUBECONFIG_PATH" ]; then
echo "kubeconfig is missing: $KUBECONFIG_PATH" >&2
exit 1
fi
PODS_JSON="$(mktemp "${TMPDIR:-/tmp}/jeannie-capacity-limits.XXXXXX")"
if [ -n "$NAMESPACE" ]; then
kubectl --kubeconfig "$KUBECONFIG_PATH" -n "$NAMESPACE" get pods -o json >"$PODS_JSON"
else
kubectl --kubeconfig "$KUBECONFIG_PATH" get pods -A -o json >"$PODS_JSON"
fi
python3 - "$PROFILE_FILE" "$PODS_JSON" <<'PY'
import json
import sys
profile_file, pods_json = sys.argv[1:3]
try:
import yaml
except ImportError:
yaml = None
profiles = {
"defaults": {
"request_cpu_m": 50,
"request_memory_mib": 128,
"limit_cpu_multiplier": 4,
"limit_memory_multiplier": 2,
},
"namespaces": {},
}
if yaml:
with open(profile_file, encoding="utf-8") as handle:
loaded = yaml.safe_load(handle) or {}
profiles["defaults"].update(loaded.get("defaults", {}))
profiles["namespaces"].update(loaded.get("namespaces", {}))
with open(pods_json, encoding="utf-8") as handle:
pods = json.load(handle).get("items", [])
seen = set()
def profile_for(namespace):
profile = dict(profiles["defaults"])
profile.update(profiles["namespaces"].get(namespace, {}))
return profile
print("Jeannie Capacity Limits")
print("=======================")
print("mode: recommendations only")
print(f"profile_file: {profile_file}")
count = 0
for pod in pods:
namespace = pod["metadata"].get("namespace", "default")
owner = namespace
for ref in pod["metadata"].get("ownerReferences", []):
if ref.get("kind") in {"ReplicaSet", "StatefulSet", "DaemonSet", "Job"}:
owner = ref.get("name", owner)
break
for container in pod.get("spec", {}).get("containers", []):
resources = container.get("resources", {})
requests = resources.get("requests", {})
limits = resources.get("limits", {})
if requests.get("cpu") and requests.get("memory") and limits.get("cpu") and limits.get("memory"):
continue
key = (namespace, owner, container["name"])
if key in seen:
continue
seen.add(key)
profile = profile_for(namespace)
request_cpu = int(profile["request_cpu_m"])
request_memory = int(profile["request_memory_mib"])
limit_cpu = request_cpu * int(profile["limit_cpu_multiplier"])
limit_memory = request_memory * int(profile["limit_memory_multiplier"])
count += 1
print(f"\n{count}. {namespace}/{owner}:{container['name']}")
print(" Add or tune this resources block in the owning manifest/chart:")
print(" resources:")
print(" requests:")
print(f" cpu: {request_cpu}m")
print(f" memory: {request_memory}Mi")
print(" limits:")
print(f" cpu: {limit_cpu}m")
print(f" memory: {limit_memory}Mi")
if count == 0:
print("\nNo containers missing requests/limits were found.")
else:
print("\nNotes:")
print("- Treat these as starting values, not truth.")
print("- Prefer app namespaces first; be careful with kube-system/control-plane pods.")
print("- After changes, run ./jeannie resource-budget and ./jeannie capacity.")
PY

View File

@ -105,8 +105,12 @@ def read_tsv(path, key):
with open(path, encoding="utf-8", newline="") as handle:
return {row[key].lower(): row for row in csv.DictReader(handle, delimiter="\t")}
def read_tsv_rows(path):
with open(path, encoding="utf-8", newline="") as handle:
return list(csv.DictReader(handle, delimiter="\t"))
scorecard_info = read_tsv(scorecard_map, "label")
finding_info = read_tsv(findings_map, "pattern")
finding_rows = read_tsv_rows(findings_map)
with open(input_file, encoding="utf-8") as handle:
text = handle.read()
@ -116,30 +120,32 @@ lines = text.splitlines()
def split_commands(value):
return [item.strip() for item in (value or "").split(";") if item.strip() and item.strip() != "none"]
def print_info(info):
def print_info(info, indent=" "):
diagnose = split_commands(info.get("diagnose_commands"))
jeannie_fix = split_commands(info.get("jeannie_fix_commands"))
other_fix = split_commands(info.get("other_fix_commands"))
print(f" means: {info.get('meaning') or 'No explain entry exists yet.'}")
print(f"{indent}Why: {info.get('meaning') or 'No explain entry exists yet.'}")
if diagnose:
print(" diagnose:")
for command in diagnose:
print(f" {command}")
print(f"{indent}Next command: {diagnose[0]}")
if len(diagnose) > 1:
print(f"{indent}More checks:")
for command in diagnose[1:]:
print(f"{indent} {command}")
if jeannie_fix:
print(" Jeannie fix command exists: yes")
print(f"{indent}Jeannie fix command:")
for command in jeannie_fix:
print(f" {command}")
print(f"{indent} {command}")
else:
print(" Jeannie fix command exists: no")
print(f"{indent}Jeannie fix command: none yet")
if other_fix:
print(" non-Jeannie fix:")
print(f"{indent}Code/config fix:")
for command in other_fix:
print(f" {command}")
print(f" risk: {info.get('risk') or 'unknown'}")
print(f" auto-fix safe: {info.get('auto_fix') or 'no'}")
print(f"{indent} {command}")
print(f"{indent}Risk: {info.get('risk') or 'unknown'}")
print(f"{indent}Auto-fix safe: {info.get('auto_fix') or 'no'}")
if info.get("notes"):
print(f" note: {info['notes']}")
print(f"{indent}Note: {info['notes']}")
def parse_scorecard():
rows = []
@ -191,11 +197,35 @@ def interesting_lines():
def match_findings(line):
lowered = line.lower()
matches = []
for pattern, info in finding_info.items():
for info in finding_rows:
pattern = (info.get("pattern") or "").lower()
if pattern and pattern in lowered:
matches.append((pattern, info))
return matches
def best_finding(line):
matches = match_findings(line)
if not matches:
return None
return sorted(matches, key=lambda item: (len(item[0]), item[0]), reverse=True)[0]
def parse_report_line(line):
report_match = re.match(r"^(?P<status>ok|warn|fail|skip)\s+(?P<check>.+?)(?:\s{2,}(?P<summary>.+))?$", line)
if report_match:
return (
report_match.group("check").strip(),
report_match.group("status").strip(),
(report_match.group("summary") or "").strip(),
)
scorecard_match = re.match(r"^(?P<label>.{1,28}) (?P<status>pass|warn|fail)(?: - (?P<summary>.*))?$", line)
if scorecard_match:
return (
scorecard_match.group("label").strip(),
scorecard_match.group("status").strip(),
(scorecard_match.group("summary") or "").strip(),
)
return (line[:72], "", "")
print(f"Jeannie Explain: {topic}")
print("=" * (17 + len(topic)))
print("mode: read-only")
@ -204,13 +234,13 @@ if topic == "scorecard":
rows = parse_scorecard()
if not rows:
print("\nNo scorecard warnings or failures found.")
for label, status, summary in rows:
for index, (label, status, summary) in enumerate(rows, start=1):
info = scorecard_info.get(label.lower(), {})
print(f"\n{label}")
print(f" status: {status}")
print(f"\n{index}. {label}")
print(f" Status: {status}")
if summary:
print(f" observed: {summary}")
print_info(info)
print(f" Observed: {summary}")
print_info(info, " ")
raise SystemExit(0)
found_lines = interesting_lines()
@ -218,17 +248,26 @@ seen_patterns = set()
matched_any = False
for line in found_lines:
matches = match_findings(line)
if not matches:
title, status, summary = parse_report_line(line)
match = best_finding(summary) if summary else None
if not match:
match = best_finding(line)
if not match:
continue
print(f"\nFinding: {line}")
for pattern, info in matches:
if pattern in seen_patterns:
continue
seen_patterns.add(pattern)
matched_any = True
print(f" matched: {pattern}")
print_info(info)
pattern, info = match
if pattern in seen_patterns:
continue
seen_patterns.add(pattern)
matched_any = True
print(f"\n{len(seen_patterns)}. {title}")
if status:
print(f" Status: {status}")
if summary:
print(f" Observed: {summary}")
else:
print(f" Observed: {line}")
print(f" Matched rule: {pattern}")
print_info(info, " ")
if not matched_any:
if found_lines:

104
scripts/impact Executable file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RULES_FILE="${HOMELAB_IMPACT_RULES:-${REPO_ROOT}/infra/jeannie-impact/rules.tsv}"
usage() {
cat <<'EOF'
Usage: ./jeannie impact [--since REF] [PATH...]
Explain which homelab areas are touched by changed files or explicit paths.
Default mode analyzes the current working tree diff.
EOF
}
since=""
paths=()
while [ "$#" -gt 0 ]; do
case "$1" in
--since)
since="${2:-}"
shift 2
;;
-h|--help|help)
usage
exit 0
;;
*)
paths+=("$1")
shift
;;
esac
done
if [ ! -s "$RULES_FILE" ]; then
echo "Missing impact rules: $RULES_FILE" >&2
exit 1
fi
if [ "${#paths[@]}" -eq 0 ]; then
if [ -n "$since" ]; then
mapfile -t paths < <(git -C "$REPO_ROOT" diff --name-only "$since"...HEAD)
else
mapfile -t paths < <(git -C "$REPO_ROOT" status --short | awk '{print $NF}')
fi
fi
if [ "${#paths[@]}" -eq 0 ]; then
echo "No changed files or paths to analyze."
exit 0
fi
python3 - "$RULES_FILE" "${paths[@]}" <<'PY'
import csv
import sys
rules_file = sys.argv[1]
paths = [path.strip() for path in sys.argv[2:] if path.strip()]
with open(rules_file, encoding="utf-8", newline="") as handle:
rules = list(csv.DictReader(handle, delimiter="\t"))
matches = []
for path in paths:
normalized = path.removeprefix("./")
for rule in rules:
pattern = rule["pattern"]
if normalized == pattern.rstrip("/") or normalized.startswith(pattern):
matches.append((normalized, rule))
print("Jeannie Impact")
print("==============")
print("Files:")
for path in paths:
print(f" {path}")
if not matches:
print("\nNo specific impact rule matched.")
print("Validate:")
print(" ./jeannie validate")
raise SystemExit(0)
seen = set()
print("\nImpacted Areas")
for path, rule in matches:
key = (rule["area"], rule["risk"])
if key in seen:
continue
seen.add(key)
print(f"\n- {rule['area']}")
print(f" risk: {rule['risk']}")
print(" touches:")
for item in rule["touches"].split(";"):
print(f" - {item}")
print(" validate:")
for command in rule["validate"].split(";"):
print(f" {command}")
if rule["apply"] != "none":
print(" apply path:")
for command in rule["apply"].split(";"):
print(f" {command}")
else:
print(" apply path: none")
PY

54
scripts/review-last-change Executable file
View File

@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REF="${1:-HEAD}"
changed_files=()
usage() {
cat <<'EOF'
Usage: ./jeannie review-last-change [REF]
Summarize a commit, show touched files, run impact analysis, and print useful
validation and rollback hints. Defaults to HEAD.
EOF
}
case "$REF" in
-h|--help|help)
usage
exit 0
;;
esac
if ! git -C "$REPO_ROOT" rev-parse --verify "$REF" >/dev/null 2>&1; then
echo "Unknown git ref: $REF" >&2
exit 1
fi
echo "Jeannie Last Change Review"
echo "=========================="
echo
git -C "$REPO_ROOT" show --no-renames --stat --oneline "$REF"
echo
echo "Changed files"
mapfile -t changed_files < <(git -C "$REPO_ROOT" diff-tree --no-commit-id --name-only -r "$REF")
printf ' %s\n' "${changed_files[@]}"
echo
"${REPO_ROOT}/scripts/impact" "${changed_files[@]}"
cat <<EOF
Generic validation
./jeannie validate
./jeannie status
Rollback hint
git revert ${REF}
Notes
- Prefer the impact-specific validation commands above before running ./jeannie up.
- For public route changes, validate edge and certs before app rollout.
EOF