From 838a3a4eb7bc3757197b55af1348c8ea18c51efc Mon Sep 17 00:00:00 2001 From: Rufus King Date: Mon, 27 Jul 2026 20:01:02 -0400 Subject: [PATCH] Cleanup and Stocks Webservices --- Stocks/WebService/Dockerfile | 14 + Stocks/WebService/app.py | 170 ++++++++ Stocks/WebService/docker-compose.yml | 14 + Stocks/WebService/requirements.txt | 3 + nextcloud/nextcloud_update_check (1).sh | 371 ----------------- nextcloud/nextcloud_update_check.sh | 23 +- scripts/nextcloud_update_check.sh | 288 -------------- zfs/NAS08-zfs_pools.html~ | 506 ------------------------ zfs/nas08_zfs_scrub-1.sh | 322 --------------- zfs/nas08_zfs_scrub.sh | 143 ++++--- 10 files changed, 307 insertions(+), 1547 deletions(-) create mode 100644 Stocks/WebService/Dockerfile create mode 100644 Stocks/WebService/app.py create mode 100644 Stocks/WebService/docker-compose.yml create mode 100644 Stocks/WebService/requirements.txt delete mode 100644 nextcloud/nextcloud_update_check (1).sh delete mode 100644 scripts/nextcloud_update_check.sh delete mode 100644 zfs/NAS08-zfs_pools.html~ delete mode 100644 zfs/nas08_zfs_scrub-1.sh diff --git a/Stocks/WebService/Dockerfile b/Stocks/WebService/Dockerfile new file mode 100644 index 0000000..df013c5 --- /dev/null +++ b/Stocks/WebService/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . + +RUN mkdir -p /data + +EXPOSE 5005 + +CMD ["gunicorn", "--bind", "0.0.0.0:5005", "--workers", "2", "--timeout", "30",> diff --git a/Stocks/WebService/app.py b/Stocks/WebService/app.py new file mode 100644 index 0000000..44c6db7 --- /dev/null +++ b/Stocks/WebService/app.py @@ -0,0 +1,170 @@ +import os +import time +import sqlite3 +import threading +from datetime import datetime, timedelta + +import requests +from flask import Flask, request, jsonify + +app = Flask(__name__) + +# ---- Config ---- +FINNHUB_API_KEY = os.environ.get("FINNHUB_API_KEY", "") +SHARED_SECRET = os.environ.get("PROXY_SECRET", "") +DB_PATH = os.environ.get("DB_PATH", "/data/cache.db") +FINNHUB_MAX_PER_MIN = 40 # stay safely under Finnhub's 60/min cap + +_db_lock = threading.Lock() + +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.execute(""" + CREATE TABLE IF NOT EXISTS historical_cache ( + ticker TEXT NOT NULL, + requested_date TEXT NOT NULL, + resolved_date TEXT NOT NULL, + price REAL NOT NULL, + PRIMARY KEY (ticker, requested_date) + ) + """) + return conn + +_current_cache = {} +_current_cache_lock = threading.Lock() +CURRENT_TTL_SECONDS = 60 + +_finnhub_calls = [] +_finnhub_lock = threading.Lock() + +def finnhub_throttle(): + with _finnhub_lock: + now = time.time() + while _finnhub_calls and _finnhub_calls[0] < now - 60: + _finnhub_calls.pop(0) + if len(_finnhub_calls) >= FINNHUB_MAX_PER_MIN: + sleep_for = 60 - (now - _finnhub_calls[0]) + 0.1 + time.sleep(max(sleep_for, 0)) + _finnhub_calls.append(time.time()) + + +def check_auth(): + key = request.args.get("key", "") + return bool(SHARED_SECRET) and key == SHARED_SECRET + + +@app.route("/current") +def current_price(): + if not check_auth(): + return jsonify({"error": "unauthorized"}), 401 + + ticker = request.args.get("ticker", "").upper().strip() + if not ticker: + return jsonify({"error": "missing ticker"}), 400 + + now = time.time() + with _current_cache_lock: + cached = _current_cache.get(ticker) + if cached and (now - cached[1]) < CURRENT_TTL_SECONDS: + return jsonify({"ticker": ticker, "price": cached[0], "quote_time": cached[2], "cached": True}) + + finnhub_throttle() + r = requests.get( + "https://finnhub.io/api/v1/quote", + params={"symbol": ticker, "token": FINNHUB_API_KEY}, + timeout=10, + ) + data = r.json() + + if not data or data.get("c") in (None, 0): + return jsonify({"ticker": ticker, "error": "no data"}), 502 + + price = data["c"] + quote_time = None + if data.get("t"): + quote_time = datetime.utcfromtimestamp(data["t"]).isoformat() + "Z" + + with _current_cache_lock: + _current_cache[ticker] = (price, now, quote_time) + + return jsonify({"ticker": ticker, "price": price, "quote_time": quote_time, "cached": False}) + + +@app.route("/historical") +def historical_price(): + if not check_auth(): + return jsonify({"error": "unauthorized"}), 401 + + ticker = request.args.get("ticker", "").upper().strip() + date_str = request.args.get("date", "").strip() + if not ticker or not date_str: + return jsonify({"error": "missing ticker or date"}), 400 + + try: + target_date = datetime.strptime(date_str, "%Y-%m-%d") + except ValueError: + return jsonify({"error": "date must be YYYY-MM-DD"}), 400 + + with _db_lock: + conn = get_db() + row = conn.execute( + "SELECT resolved_date, price FROM historical_cache WHERE ticker=? AND requested_date=?", + (ticker, date_str), + ).fetchone() + conn.close() + + if row: + return jsonify({"ticker": ticker, "requested_date": date_str, "resolved_date": row[0], "price": row[1], "cached": True}) + + period2 = int((target_date + timedelta(days=1)).timestamp()) + period1 = int((target_date - timedelta(days=10)).timestamp()) + + url = f"https://query1.finance.yahoo.com/v8/finance/chart/{ticker}" + headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} + r = requests.get(url, params={"period1": period1, "period2": period2, "interval": "1d"}, headers=headers, timeout=10) + + if r.status_code != 200: + return jsonify({"ticker": ticker, "error": f"yahoo http {r.status_code}"}), 502 + + data = r.json() + try: + result = data["chart"]["result"][0] + timestamps = result["timestamp"] + closes = result["indicators"]["quote"][0]["close"] + except (KeyError, IndexError, TypeError): + return jsonify({"ticker": ticker, "error": "no historical data"}), 502 + + best = None + for ts, close in zip(timestamps, closes): + if close is None: + continue + day = datetime.utcfromtimestamp(ts) + if day.date() <= target_date.date(): + if best is None or day > best[0]: + best = (day, close) + + if best is None: + return jsonify({"ticker": ticker, "error": "no trading day found in window"}), 502 + + resolved_date = best[0].strftime("%Y-%m-%d") + price = round(best[1], 4) + + with _db_lock: + conn = get_db() + conn.execute( + "INSERT OR REPLACE INTO historical_cache (ticker, requested_date, resolved_date, price) VALUES (?, ?, ?, ?)", + (ticker, date_str, resolved_date, price), + ) + conn.commit() + conn.close() + + return jsonify({"ticker": ticker, "requested_date": date_str, "resolved_date": resolved_date, "price": price, "cached": False}) + + +@app.route("/health") +def health(): + return jsonify({"status": "ok"}) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5005) diff --git a/Stocks/WebService/docker-compose.yml b/Stocks/WebService/docker-compose.yml new file mode 100644 index 0000000..a2a9c0f --- /dev/null +++ b/Stocks/WebService/docker-compose.yml @@ -0,0 +1,14 @@ +services: + stockproxy: + build: . + container_name: stockproxy + restart: unless-stopped + ports: + - "5005:5005" + environment: + - FINNHUB_API_KEY=d9e2k2hr01qh241a54ngd9e2k2hr01qh241a54o0 + - PROXY_SECRET=90c2528e9b5221c110f7c2c9cd6c65dc + - DB_PATH=/data/cache.db + volumes: + - /opt/stockproxy/data:/data + diff --git a/Stocks/WebService/requirements.txt b/Stocks/WebService/requirements.txt new file mode 100644 index 0000000..e3562d5 --- /dev/null +++ b/Stocks/WebService/requirements.txt @@ -0,0 +1,3 @@ +flask +requests +gunicorn diff --git a/nextcloud/nextcloud_update_check (1).sh b/nextcloud/nextcloud_update_check (1).sh deleted file mode 100644 index 9f0850a..0000000 --- a/nextcloud/nextcloud_update_check (1).sh +++ /dev/null @@ -1,371 +0,0 @@ -#!/bin/bash -# ============================================================= -# nextcloud_update_check.sh -# Checks for Nextcloud CORE and app updates daily. -# Sends an HTML email report with update commands ready to -# copy-paste into terminal. -# -# Schedule: Daily (recommended 7:00 AM) -# Install: /usr/scripts/omv/nextcloud_update_check.sh -# Log: /var/log/nextcloud-updates/ -# -# 2026-07-24 fix: previously only checked `occ app:update --all -# --showonly`, which reports APP updates only. It never checked -# for a Nextcloud CORE/server version update, so a core release -# (e.g. Nextcloud 34.0.2) was silently missed and the report -# said "up to date" even when it wasn't. Added a separate core -# check via `occ update:check`. -# ============================================================= - -# --- Config -------------------------------------------------- -CONTAINER="nextcloud" -OCC="sudo docker exec -u www-data ${CONTAINER} php occ" -# OMV Compose project location — NOT ~/docker/nextcloud. Confirmed via: -# docker inspect nextcloud --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}' -# Directory is root-only (drwx------ root root), so compose commands -# must run under sudo. OMV names its compose files ".yml" + -# "compose.override.yml" — NOT the default "docker-compose.yml" — so -# both -f flags are required or `docker compose` won't find them. -NEXTCLOUD_COMPOSE_DIR="/kingdezignsnas/Docker/Compose/nextcloud" -NEXTCLOUD_COMPOSE_FILES="-f nextcloud.yml -f compose.override.yml" -SMTP_SERVER="smtppro.zoho.com" -SMTP_PORT="465" -SMTP_USER="rufus.king@kingdezigns.com" -SMTP_PASS_FILE="/etc/nextcloud-smtp-pass" -FROM_NAME="NAS08" -FROM_EMAIL="rufus.king@kingdezigns.com" -TO_EMAIL="rufus.king@kingdezigns.com" -HOSTNAME_LABEL="NAS08" -LOG_DIR="/var/log/nextcloud-updates" -LOG_RETENTION_DAYS=90 -# ------------------------------------------------------------- - -mkdir -p "$LOG_DIR" -DATESTAMP=$(date +"%Y%m%d") -TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S") -LOGFILE="$LOG_DIR/update-check-${DATESTAMP}.log" - -log() { echo "[$(date +"%H:%M:%S")] $*" | tee -a "$LOGFILE"; } - -# --- Load SMTP password -------------------------------------- -if [[ ! -f "$SMTP_PASS_FILE" ]]; then - log "ERROR: SMTP password file not found at $SMTP_PASS_FILE" - exit 1 -fi -SMTP_PASS=$(cat "$SMTP_PASS_FILE") - -# --- Check container is running ------------------------------ -if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then - log "ERROR: Container '${CONTAINER}' is not running." - # Send critical alert - STATUS_COLOR="#b91c1c" - STATUS_LABEL="CRITICAL" - STATUS_MSG="⛔  Nextcloud container is not running" - BODY_HTML="

The Nextcloud Docker container was not found running on ${HOSTNAME_LABEL}. No update check could be performed.

" - NEEDS_ACTION=true - APP_ROWS="" - CMD_BLOCK="" -else - # --- Get Nextcloud status ---------------------------------- - log "Checking Nextcloud status..." - NC_STATUS=$($OCC status 2>/dev/null | grep -v '^{') - MAINTENANCE=$(echo "$NC_STATUS" | grep "maintenance:" | awk '{print $3}') - NEEDS_UPGRADE=$(echo "$NC_STATUS" | grep "needsDbUpgrade:" | awk '{print $3}') - NC_VERSION=$(echo "$NC_STATUS" | grep "versionstring:" | awk '{print $3}') - - log "Nextcloud version: $NC_VERSION | maintenance: $MAINTENANCE | needsDbUpgrade: $NEEDS_UPGRADE" - - # --- Check for CORE (server) update ------------------------- - # occ app:update only reports app updates, never core/server - # releases. occ update:check is the command that actually - # checks the updater channel for a new Nextcloud version. - log "Checking core (server) update..." - CORE_CHECK_RAW=$($OCC update:check 2>/dev/null | grep -v '^{') - CORE_UPDATE_AVAILABLE=false - CORE_NEW_VERSION="" - if echo "$CORE_CHECK_RAW" | grep -qi "is available"; then - CORE_UPDATE_AVAILABLE=true - CORE_NEW_VERSION=$(echo "$CORE_CHECK_RAW" | grep -oE 'Nextcloud [0-9]+\.[0-9]+\.[0-9]+' | head -1 | awk '{print $2}') - log "Core update available: Nextcloud ${CORE_NEW_VERSION}" - else - log "Core is up to date." - fi - - # --- Check for app updates -------------------------------- - log "Checking app updates..." - # Filter out admin_audit JSON log lines that appear when admin_audit app is enabled - APP_UPDATE_RAW=$($OCC app:update --all --showonly 2>/dev/null | grep -v '^{' ) - - # Parse apps with available updates - # Format from occ app:update --showonly: "appname new version available: X.Y.Z" - UPDATABLE_APPS=() - while IFS= read -r line; do - if echo "$line" | grep -q "new version available"; then - APP_NAME=$(echo "$line" | awk '{print $1}') - NEW_VER=$(echo "$line" | awk '{print $NF}') - UPDATABLE_APPS+=("${APP_NAME}|||${NEW_VER}") - log "Update available: $APP_NAME → $NEW_VER" - fi - done <<< "$APP_UPDATE_RAW" - - APP_COUNT=${#UPDATABLE_APPS[@]} - - # --- Build status ----------------------------------------- - NEEDS_ACTION=false - WARNINGS=() - - if [[ "$MAINTENANCE" == "true" ]]; then - WARNINGS+=("Nextcloud is currently in maintenance mode") - NEEDS_ACTION=true - fi - if [[ "$NEEDS_UPGRADE" == "true" ]]; then - WARNINGS+=("Database upgrade required (needsDbUpgrade: true)") - NEEDS_ACTION=true - fi - if [[ $APP_COUNT -gt 0 ]]; then - NEEDS_ACTION=true - fi - if [[ "$CORE_UPDATE_AVAILABLE" == "true" ]]; then - NEEDS_ACTION=true - fi - - # --- Determine overall status color ----------------------- - if [[ "$MAINTENANCE" == "true" ]] || [[ "$NEEDS_UPGRADE" == "true" ]]; then - STATUS_COLOR="#b91c1c" - STATUS_LABEL="CRITICAL" - STATUS_MSG="⛔  Nextcloud requires immediate attention" - elif [[ "$CORE_UPDATE_AVAILABLE" == "true" ]] && [[ $APP_COUNT -gt 0 ]]; then - STATUS_COLOR="#b45309" - STATUS_LABEL="UPDATES AVAILABLE" - STATUS_MSG="🔄  Core update to ${CORE_NEW_VERSION} + ${APP_COUNT} app update(s) available" - elif [[ "$CORE_UPDATE_AVAILABLE" == "true" ]]; then - STATUS_COLOR="#b45309" - STATUS_LABEL="UPDATES AVAILABLE" - STATUS_MSG="🔄  Nextcloud core update available (${NC_VERSION} → ${CORE_NEW_VERSION})" - elif [[ $APP_COUNT -gt 0 ]]; then - STATUS_COLOR="#b45309" - STATUS_LABEL="UPDATES AVAILABLE" - STATUS_MSG="🔄  ${APP_COUNT} app update(s) available" - else - STATUS_COLOR="#1a7f4b" - STATUS_LABEL="UP TO DATE" - STATUS_MSG="✅  Nextcloud is fully up to date" - fi - - # --- Maintenance mode color (was previously unset/unused) -- - if [[ "$MAINTENANCE" == "true" ]]; then - MAINTENANCE_COLOR="#b91c1c" - else - MAINTENANCE_COLOR="#1a7f4b" - fi - - # --- Build app update rows -------------------------------- - APP_ROWS="" - if [[ $APP_COUNT -gt 0 ]]; then - for entry in "${UPDATABLE_APPS[@]}"; do - APP_NAME=$(echo "$entry" | cut -d'|' -f1) - NEW_VER=$(echo "$entry" | cut -d'|' -f4) - APP_ROWS+=" - ${APP_NAME} - ${NEW_VER} - " - done - fi - - # --- Build warning rows ----------------------------------- - WARN_HTML="" - for w in "${WARNINGS[@]}"; do - WARN_HTML+="
- ⚠ ${w} -
" - done - - # --- Build copy-paste command block ----------------------- - # NOTE: must use $'...' (ANSI-C quoting) below, not "...". Plain - # double quotes do NOT interpret \n as a newline in bash — they - # insert the two literal characters \ and n, which is why the - # previous version of this script emailed out commands with - # literal "\n" text instead of real line breaks. - CMD_LINES="" - if [[ "$MAINTENANCE" == "true" ]]; then - CMD_LINES+=$'# Disable maintenance mode\nsudo docker exec -u www-data nextcloud php occ maintenance:mode --off\n\n' - fi - if [[ "$CORE_UPDATE_AVAILABLE" == "true" ]]; then - # The nextcloud image's own entrypoint auto-detects the version - # bump and runs the upgrade itself on container start — it does - # NOT need `occ upgrade` run manually. Running `occ upgrade` - # immediately after `up -d` can race the container's own startup - # and falsely report "No upgrade required" if checked too early. - # The 30s sleep gives the entrypoint's internal upgrade time to - # finish before the confirming `occ status` call. - CMD_LINES+="# Update Nextcloud CORE to ${CORE_NEW_VERSION} (do this before app updates)"$'\n' - CMD_LINES+="sudo bash -c \"cd ${NEXTCLOUD_COMPOSE_DIR} && docker compose ${NEXTCLOUD_COMPOSE_FILES} pull && docker compose ${NEXTCLOUD_COMPOSE_FILES} up -d\""$'\n\n' - CMD_LINES+="# Give the container's built-in upgrade entrypoint time to finish,"$'\n' - CMD_LINES+="# then confirm the new version is active (watch for versionstring: ${CORE_NEW_VERSION})"$'\n' - CMD_LINES+="sleep 30"$'\n' - CMD_LINES+="sudo docker exec -u www-data nextcloud php occ status"$'\n\n' - fi - if [[ "$NEEDS_UPGRADE" == "true" ]]; then - CMD_LINES+=$'# Run database upgrade\nsudo docker exec -u www-data nextcloud php occ upgrade --no-interaction\n\n' - fi - if [[ $APP_COUNT -gt 0 ]]; then - CMD_LINES+=$'# Update all apps\nsudo docker exec -u www-data nextcloud php occ app:update --all\n\n' - fi - if [[ -z "$CMD_LINES" ]]; then - CMD_LINES="# No action required — everything is up to date"$'\n' - fi - CMD_LINES+=$'# Verify status after updates\nsudo docker exec -u www-data nextcloud php occ status' - - CMD_BLOCK="
-
📋 Commands to run on NAS08
-
${CMD_LINES}
-
" - - # --- Maintenance/upgrade block ---------------------------- - MAINT_BLOCK="" - if [[ -n "$WARN_HTML" ]]; then - MAINT_BLOCK="
-
⚠ Issues Detected
- ${WARN_HTML} -
" - fi - - # --- Core update block -------------------------------------- - CORE_BLOCK="" - if [[ "$CORE_UPDATE_AVAILABLE" == "true" ]]; then - CORE_BLOCK="
-
🆙 Core Update Available
- - - - - - - - - -
Current Version${NC_VERSION}
New Version${CORE_NEW_VERSION}
-
" - fi - - # --- App update table ------------------------------------- - APP_TABLE="" - if [[ $APP_COUNT -gt 0 ]]; then - APP_TABLE="
-
🔄 App Updates Available (${APP_COUNT})
- - - - - - - - ${APP_ROWS} -
AppNew Version
-
" - else - APP_TABLE="
- ✓ All apps are up to date -
" - fi - - BODY_HTML="${MAINT_BLOCK} - - -
- Nextcloud Version - ${NC_VERSION} - Maintenance Mode - ${MAINTENANCE:-false} -
- - ${CORE_BLOCK} - ${APP_TABLE} - ${CMD_BLOCK}" -fi - -# --- Determine email subject --------------------------------- -if [[ "$NEEDS_ACTION" == "true" ]]; then - SUBJECT="[NAS08] Nextcloud — ${STATUS_LABEL} — Action Required" -else - SUBJECT="[NAS08] Nextcloud — All Up To Date" -fi - -log "Composing email: $SUBJECT" - -# --- Build full HTML email ----------------------------------- -HTML_EMAIL=$(cat < - - - - - -
- - - - - - - - - - - - - - -
- - - -
-
Nextcloud · Daily Update Report
-
☁ ${HOSTNAME_LABEL}
-
${TIMESTAMP} | cloud.kingdezigns.com
-
-
${STATUS_LABEL}
-
-
- ${STATUS_MSG} -
- ${BODY_HTML} -
- Nextcloud Update Check  |  ${HOSTNAME_LABEL}  |  KingDezigns Infrastructure -
-
- - -HTML -) - -# --- Send email via curl/SMTP -------------------------------- -log "Sending email to ${TO_EMAIL}..." - -SEND_RESULT=$(curl --silent --show-error \ - --url "smtps://${SMTP_SERVER}:${SMTP_PORT}" \ - --ssl-reqd \ - --mail-from "${FROM_EMAIL}" \ - --mail-rcpt "${TO_EMAIL}" \ - --user "${SMTP_USER}:${SMTP_PASS}" \ - --upload-file - <&1 -From: ${FROM_NAME} <${FROM_EMAIL}> -To: ${TO_EMAIL} -Subject: ${SUBJECT} -MIME-Version: 1.0 -Content-Type: text/html; charset=UTF-8 - -${HTML_EMAIL} -EOF -) - -if [[ $? -eq 0 ]]; then - log "Email sent successfully." -else - log "ERROR sending email: ${SEND_RESULT}" -fi - -# --- Rotate old logs ----------------------------------------- -find "$LOG_DIR" -name "update-check-*.log" -mtime +${LOG_RETENTION_DAYS} -delete -log "Done." diff --git a/nextcloud/nextcloud_update_check.sh b/nextcloud/nextcloud_update_check.sh index 1cd824c..9f0850a 100644 --- a/nextcloud/nextcloud_update_check.sh +++ b/nextcloud/nextcloud_update_check.sh @@ -20,6 +20,14 @@ # --- Config -------------------------------------------------- CONTAINER="nextcloud" OCC="sudo docker exec -u www-data ${CONTAINER} php occ" +# OMV Compose project location — NOT ~/docker/nextcloud. Confirmed via: +# docker inspect nextcloud --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}' +# Directory is root-only (drwx------ root root), so compose commands +# must run under sudo. OMV names its compose files ".yml" + +# "compose.override.yml" — NOT the default "docker-compose.yml" — so +# both -f flags are required or `docker compose` won't find them. +NEXTCLOUD_COMPOSE_DIR="/kingdezignsnas/Docker/Compose/nextcloud" +NEXTCLOUD_COMPOSE_FILES="-f nextcloud.yml -f compose.override.yml" SMTP_SERVER="smtppro.zoho.com" SMTP_PORT="465" SMTP_USER="rufus.king@kingdezigns.com" @@ -56,7 +64,6 @@ if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then BODY_HTML="

The Nextcloud Docker container was not found running on ${HOSTNAME_LABEL}. No update check could be performed.

" NEEDS_ACTION=true APP_ROWS="" - CORE_ROW="" CMD_BLOCK="" else # --- Get Nextcloud status ---------------------------------- @@ -184,7 +191,19 @@ else CMD_LINES+=$'# Disable maintenance mode\nsudo docker exec -u www-data nextcloud php occ maintenance:mode --off\n\n' fi if [[ "$CORE_UPDATE_AVAILABLE" == "true" ]]; then - CMD_LINES+="# Update Nextcloud CORE to ${CORE_NEW_VERSION} (do this before app updates)"$'\n'"cd ~/docker/nextcloud"$'\n'"docker compose pull"$'\n'"docker compose up -d"$'\n'"sudo docker exec -u www-data nextcloud php occ upgrade --no-interaction"$'\n\n' + # The nextcloud image's own entrypoint auto-detects the version + # bump and runs the upgrade itself on container start — it does + # NOT need `occ upgrade` run manually. Running `occ upgrade` + # immediately after `up -d` can race the container's own startup + # and falsely report "No upgrade required" if checked too early. + # The 30s sleep gives the entrypoint's internal upgrade time to + # finish before the confirming `occ status` call. + CMD_LINES+="# Update Nextcloud CORE to ${CORE_NEW_VERSION} (do this before app updates)"$'\n' + CMD_LINES+="sudo bash -c \"cd ${NEXTCLOUD_COMPOSE_DIR} && docker compose ${NEXTCLOUD_COMPOSE_FILES} pull && docker compose ${NEXTCLOUD_COMPOSE_FILES} up -d\""$'\n\n' + CMD_LINES+="# Give the container's built-in upgrade entrypoint time to finish,"$'\n' + CMD_LINES+="# then confirm the new version is active (watch for versionstring: ${CORE_NEW_VERSION})"$'\n' + CMD_LINES+="sleep 30"$'\n' + CMD_LINES+="sudo docker exec -u www-data nextcloud php occ status"$'\n\n' fi if [[ "$NEEDS_UPGRADE" == "true" ]]; then CMD_LINES+=$'# Run database upgrade\nsudo docker exec -u www-data nextcloud php occ upgrade --no-interaction\n\n' diff --git a/scripts/nextcloud_update_check.sh b/scripts/nextcloud_update_check.sh deleted file mode 100644 index 53d18de..0000000 --- a/scripts/nextcloud_update_check.sh +++ /dev/null @@ -1,288 +0,0 @@ -#!/bin/bash -# ============================================================= -# nextcloud_update_check.sh -# Checks for Nextcloud core and app updates daily. -# Sends an HTML email report with update commands ready to -# copy-paste into terminal. -# -# Schedule: Daily (recommended 7:00 AM) -# Install: /usr/scripts/omv/nextcloud_update_check.sh -# Log: /var/log/nextcloud-updates/ -# ============================================================= - -# --- Config -------------------------------------------------- -CONTAINER="nextcloud" -OCC="sudo docker exec -u www-data ${CONTAINER} php occ" -SMTP_SERVER="smtppro.zoho.com" -SMTP_PORT="465" -SMTP_USER="rufus.king@kingdezigns.com" -SMTP_PASS_FILE="/etc/nextcloud-smtp-pass" -FROM_NAME="NAS08" -FROM_EMAIL="rufus.king@kingdezigns.com" -TO_EMAIL="rufus.king@kingdezigns.com" -HOSTNAME_LABEL="NAS08" -LOG_DIR="/var/log/nextcloud-updates" -LOG_RETENTION_DAYS=90 -# ------------------------------------------------------------- - -mkdir -p "$LOG_DIR" -DATESTAMP=$(date +"%Y%m%d") -TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S") -LOGFILE="$LOG_DIR/update-check-${DATESTAMP}.log" - -log() { echo "[$(date +"%H:%M:%S")] $*" | tee -a "$LOGFILE"; } - -# --- Load SMTP password -------------------------------------- -if [[ ! -f "$SMTP_PASS_FILE" ]]; then - log "ERROR: SMTP password file not found at $SMTP_PASS_FILE" - exit 1 -fi -SMTP_PASS=$(cat "$SMTP_PASS_FILE") - -# --- Check container is running ------------------------------ -if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then - log "ERROR: Container '${CONTAINER}' is not running." - # Send critical alert - STATUS_COLOR="#b91c1c" - STATUS_LABEL="CRITICAL" - STATUS_MSG="⛔  Nextcloud container is not running" - BODY_HTML="

The Nextcloud Docker container was not found running on ${HOSTNAME_LABEL}. No update check could be performed.

" - NEEDS_ACTION=true - APP_ROWS="" - CORE_ROW="" - CMD_BLOCK="" -else - # --- Get Nextcloud status ---------------------------------- - log "Checking Nextcloud status..." - NC_STATUS=$($OCC status 2>/dev/null) - MAINTENANCE=$(echo "$NC_STATUS" | grep "maintenance:" | awk '{print $3}') - NEEDS_UPGRADE=$(echo "$NC_STATUS" | grep "needsDbUpgrade:" | awk '{print $3}') - NC_VERSION=$(echo "$NC_STATUS" | grep "versionstring:" | awk '{print $3}') - - log "Nextcloud version: $NC_VERSION | maintenance: $MAINTENANCE | needsDbUpgrade: $NEEDS_UPGRADE" - - # --- Check for app updates -------------------------------- - log "Checking app updates..." - APP_UPDATE_RAW=$($OCC app:update --all --dry-run 2>/dev/null) - # Fall back if --dry-run not supported (older NC versions) - if echo "$APP_UPDATE_RAW" | grep -q "Unknown option"; then - log "dry-run not supported, using app:list to detect updates..." - APP_UPDATE_RAW=$($OCC app:list 2>/dev/null | grep -E "\(installed") - fi - - # Parse apps with available updates - # Format from occ app:update --dry-run: "appname new version available: X.Y.Z" - UPDATABLE_APPS=() - while IFS= read -r line; do - if echo "$line" | grep -q "new version available"; then - APP_NAME=$(echo "$line" | awk '{print $1}') - NEW_VER=$(echo "$line" | awk '{print $NF}') - UPDATABLE_APPS+=("${APP_NAME}|||${NEW_VER}") - log "Update available: $APP_NAME → $NEW_VER" - fi - done <<< "$APP_UPDATE_RAW" - - APP_COUNT=${#UPDATABLE_APPS[@]} - - # --- Build status ----------------------------------------- - NEEDS_ACTION=false - WARNINGS=() - - if [[ "$MAINTENANCE" == "true" ]]; then - WARNINGS+=("Nextcloud is currently in maintenance mode") - NEEDS_ACTION=true - fi - if [[ "$NEEDS_UPGRADE" == "true" ]]; then - WARNINGS+=("Database upgrade required (needsDbUpgrade: true)") - NEEDS_ACTION=true - fi - if [[ $APP_COUNT -gt 0 ]]; then - NEEDS_ACTION=true - fi - - # --- Determine overall status color ----------------------- - if [[ "$MAINTENANCE" == "true" ]] || [[ "$NEEDS_UPGRADE" == "true" ]]; then - STATUS_COLOR="#b91c1c" - STATUS_LABEL="CRITICAL" - STATUS_MSG="⛔  Nextcloud requires immediate attention" - elif [[ $APP_COUNT -gt 0 ]]; then - STATUS_COLOR="#b45309" - STATUS_LABEL="UPDATES AVAILABLE" - STATUS_MSG="🔄  ${APP_COUNT} app update(s) available" - else - STATUS_COLOR="#1a7f4b" - STATUS_LABEL="UP TO DATE" - STATUS_MSG="✅  Nextcloud is fully up to date" - fi - - # --- Build app update rows -------------------------------- - APP_ROWS="" - if [[ $APP_COUNT -gt 0 ]]; then - for entry in "${UPDATABLE_APPS[@]}"; do - APP_NAME=$(echo "$entry" | cut -d'|' -f1) - NEW_VER=$(echo "$entry" | cut -d'|' -f4) - APP_ROWS+=" - ${APP_NAME} - ${NEW_VER} - " - done - fi - - # --- Build warning rows ----------------------------------- - WARN_HTML="" - for w in "${WARNINGS[@]}"; do - WARN_HTML+="
- ⚠ ${w} -
" - done - - # --- Build copy-paste command block ----------------------- - CMD_LINES="" - if [[ "$MAINTENANCE" == "true" ]]; then - CMD_LINES+="# Disable maintenance mode\nsudo docker exec -u www-data nextcloud php occ maintenance:mode --off\n\n" - fi - if [[ "$NEEDS_UPGRADE" == "true" ]]; then - CMD_LINES+="# Run database upgrade\nsudo docker exec -u www-data nextcloud php occ upgrade --no-interaction\n\n" - fi - if [[ $APP_COUNT -gt 0 ]]; then - CMD_LINES+="# Update all apps\nsudo docker exec -u www-data nextcloud php occ app:update --all\n\n" - fi - if [[ -z "$CMD_LINES" ]]; then - CMD_LINES="# No action required — everything is up to date" - fi - CMD_LINES+="# Verify status after updates\nsudo docker exec -u www-data nextcloud php occ status" - - CMD_BLOCK="
-
📋 Commands to run on NAS08
-
${CMD_LINES}
-
" - - # --- Maintenance/upgrade block ---------------------------- - MAINT_BLOCK="" - if [[ -n "$WARN_HTML" ]]; then - MAINT_BLOCK="
-
⚠ Issues Detected
- ${WARN_HTML} -
" - fi - - # --- App update table ------------------------------------- - APP_TABLE="" - if [[ $APP_COUNT -gt 0 ]]; then - APP_TABLE="
-
🔄 App Updates Available (${APP_COUNT})
- - - - - - - - ${APP_ROWS} -
AppNew Version
-
" - else - APP_TABLE="
- ✓ All apps are up to date -
" - fi - - BODY_HTML="${MAINT_BLOCK} - - -
- Nextcloud Version - ${NC_VERSION} - Maintenance Mode - ${MAINTENANCE:-false} -
- - ${APP_TABLE} - ${CMD_BLOCK}" -fi - -# --- Determine email subject --------------------------------- -if [[ "$NEEDS_ACTION" == "true" ]]; then - SUBJECT="[NAS08] Nextcloud — ${STATUS_LABEL} — Action Required" -else - SUBJECT="[NAS08] Nextcloud — All Up To Date" -fi - -log "Composing email: $SUBJECT" - -# --- Build full HTML email ----------------------------------- -HTML_EMAIL=$(cat < - - - - - -
- - - - - - - - - - - - - - -
- - - -
-
Nextcloud · Daily Update Report
-
☁ ${HOSTNAME_LABEL}
-
${TIMESTAMP} | cloud.kingdezigns.com
-
-
${STATUS_LABEL}
-
-
- ${STATUS_MSG} -
- ${BODY_HTML} -
- Nextcloud Update Check  |  ${HOSTNAME_LABEL}  |  KingDezigns Infrastructure -
-
- - -HTML -) - -# --- Send email via curl/SMTP -------------------------------- -log "Sending email to ${TO_EMAIL}..." - -SEND_RESULT=$(curl --silent --show-error \ - --url "smtps://${SMTP_SERVER}:${SMTP_PORT}" \ - --ssl-reqd \ - --mail-from "${FROM_EMAIL}" \ - --mail-rcpt "${TO_EMAIL}" \ - --user "${SMTP_USER}:${SMTP_PASS}" \ - --upload-file - <&1 -From: ${FROM_NAME} <${FROM_EMAIL}> -To: ${TO_EMAIL} -Subject: ${SUBJECT} -MIME-Version: 1.0 -Content-Type: text/html; charset=UTF-8 - -${HTML_EMAIL} -EOF -) - -if [[ $? -eq 0 ]]; then - log "Email sent successfully." -else - log "ERROR sending email: ${SEND_RESULT}" -fi - -# --- Rotate old logs ----------------------------------------- -find "$LOG_DIR" -name "update-check-*.log" -mtime +${LOG_RETENTION_DAYS} -delete -log "Done." diff --git a/zfs/NAS08-zfs_pools.html~ b/zfs/NAS08-zfs_pools.html~ deleted file mode 100644 index f067f01..0000000 --- a/zfs/NAS08-zfs_pools.html~ +++ /dev/null @@ -1,506 +0,0 @@ - - - - - -KingDezigns — NAS08 ZFS Pool Maintenance Scripts - - - - - -
- -
-

KingDezigns — NAS08 ZFS Pool Maintenance Scripts

-
- 📅 2026-05-14 - 🖥 NAS08 / Raspberry Pi 5 - ⚙️ OpenMediaVault (OMV) - 🐳 Docker Compose - 💾 ZFS RAIDZ2 — Penta SATA HAT -
-
- - - -
-
The goal
- -

NAS08 serves as a Pi-Hole DNS server, nginx proxy server, Vaultwarden Server, Plex server, as well as an OpenMediaVault Nas Server

- -
- ZFS Scrubbing - ZFS Reporting -
- -

The goal is to create regular maintenance to the ZFS pools for this NAS. This is done to protect and keep the pools healthy.

- -
-
⚠ Critical — store this document off NAS16
-

Keep a copy of this document on your workstation, a USB drive, or in print. If NAS08 is down you cannot read files stored on it.

-
-
- - -
-
Script to Create ZFS report
- -
-
1 Create the folders on NAS08
-

This script gets the current ZFS pool status and reports it back to the administrator via email. Use these commands to setup the scripts on the NAS.

-
ssh rufusking@192.168.150.35
-sudo mkdir /usr/scripts/zfs
-sudo nano nas08_zfs_report.sh
-
- -
-
2 Copy the content of the entire nas08_zfs_report.sh script and paste it in the terminal window. Save (CTRL + X) and commit (Y) the changes
-
nas08_zfs_report.sh
-
- -
-
3 Make the script executable — NAS08
-
chmod +x /usr/scripts/zfs/nas08_zfs_report.sh
-
- -
-
4 Run a test report — NAS08
-
sudo /usr/scripts/zfs/nas08_zfs_report.sh
-

Watch for the report in your inbox

-
-
- - -
-
Script to perform a ZFS pool scrub
- -
-
1 Create the folders on NAS08
-

This script initiates a ZFS pool scrub and reports back the status to the administrator via email. Use these commands to setup this script on the NAS.

-
ssh rufusking@192.168.150.35
-sudo mkdir /usr/scripts/zfs
-sudo nano nas08_zfs_scrub.sh
-
- -
-
2 Copy the content of the entire nas08_zfs_scrub.sh script and paste it in the terminal window. Save (CTRL + X) and commit (Y) the changes
-
nas08_zfs_scrub.sh
-
- -
-
3 Make the script executable — NAS08
-
chmod +x /usr/scripts/zfs/nas08_zfs_scrub.sh
-
- -
-
4 Run a test report — NAS08
-
sudo /usr/scripts/zfs/nas08_zfs_report.sh
-

Watch for the report in your inbox

-
-
- - - -
-
Key differences from NAS16
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
AreaNAS08NAS16
Primary roleDocker containers (Pi-hole, Plex, Vaultwarden, Nextcloud, NPM)Web server (Apache/PHP), databases (MariaDB), Webmin admin
What's backed up beyond /etc + OMVDocker Compose files, Pi-hole data, Plex config/metadataApache vhosts, PHP config, MariaDB dumps, Webmin config
Website filesN/AIntentionally excluded — stored on NAS drives, covered by redundancy
Database backupNone (no databases)Full mysqldump of all user databases, .sql.gz per database
Backup destination/export/kingdezigns-all/Backups/NAS08//export/kingdezignsnas-16/Backups/NAS16/
ZFS pool namekingdezignsnaskingdezignsnas-16 (assumed — verify with sudo zpool list)
Recovery phases9 phases, 34 steps10 phases, 40 steps
-
- - - -
-
Notes & assumptions to verify
- -
-
⚠ Verify these before first run
-

The script was written based on information provided and follows Raspberry Pi OS / Debian conventions. Verify the following on NAS16 before treating any backup as production-ready.

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ItemAssumed valueHow to verify
ZFS pool namekingdezignsnassudo zpool list
Backup destination path/export/kingdezigns08/Backups/NAS08/ls /export/ — confirm share name matches
MariaDB auth methodUses /etc/mysql/debian.cnf (maintenance account, no password when run as root)Run: sudo mysql --defaults-file=/etc/mysql/debian.cnf -e "SHOW DATABASES;" — should work without password
PHP version(s)Auto-detected from /etc/php/*/php -v and ls /etc/php/
Apache config location/etc/apache2/ (standard Debian)apache2 -V — confirms config path
Webmin config location/etc/webmin/ls /etc/webmin/
-
- - - -
-
Network map — items to update
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ItemOld valueNew value
NAS16 — Backup statusNoneAutomated — every 3 days — 30-day retention
NAS16 — Backup destinationNone/export/kingdezignsnas-16/Backups/NAS16/
NAS16 — Backup scriptNone/usr/scripts/omv/nas16-backup.sh
NAS16 — PCIe requirementnot documenteddtparam=pciex1 + dtparam=pciex1_gen=3 required in /boot/firmware/config.txt on fresh OS
NAS16 — Web stacknot documentedApache · PHP · MariaDB · Webmin · Adminer
NAS16 — Recovery planNoneNAS16-Backup-Summary.html — 40 steps across 10 phases + ZFS troubleshooting
-
- - - - -
- - diff --git a/zfs/nas08_zfs_scrub-1.sh b/zfs/nas08_zfs_scrub-1.sh deleted file mode 100644 index 54811dd..0000000 --- a/zfs/nas08_zfs_scrub-1.sh +++ /dev/null @@ -1,322 +0,0 @@ -#!/bin/bash -# ============================================================================= -# ZFS Pool Scrub & Email Report — NAS08 -# Location: /usr/scripts/zfs/nas08_zfs_scrub.sh -# Schedule via OMV Scheduler (cron) -# ============================================================================= - -# ── Configuration ───────────────────────────────────────────────────────────── -HOSTNAME="NAS08" -REPORT_TO="rufus.king@kingdezigns.com" -REPORT_FROM="zfs-monitor@nas08.local" # Display only — Postfix uses your Gmail relay -SCRUB_WAIT_SECONDS=28800 # Max wait per pool scrub (8 hrs for large pools) -CAPACITY_WARN_THRESHOLD=80 # % capacity to flag as WARNING -SCRUB_AGE_WARN_DAYS=8 # Days since last scrub before flagging stale -LOG_FILE="/var/log/zfs_scrub_nas08.log" -# Note: Email is sent via OMV-configured Postfix (Gmail relay). No SMTP config needed here. -# ────────────────────────────────────────────────────────────────────────────── - -REPORT_TIME=$(date "+%Y-%m-%d %H:%M:%S") -POOLS=$(zpool list -H -o name) -POOL_COUNT=$(echo "$POOLS" | wc -l) - -log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; } - -log "===== ZFS Scrub started on $HOSTNAME =====" - -# ── Run scrubs sequentially ─────────────────────────────────────────────────── -for pool in $POOLS; do - log "Starting scrub on pool: $pool" - zpool scrub "$pool" - - ELAPSED=0 - while true; do - SCRUB_STATE=$(zpool status "$pool" | grep -E "scan:" | awk '{print $2}') - [[ "$SCRUB_STATE" == "scrub" ]] || break - sleep 30 - ELAPSED=$((ELAPSED + 30)) - if [[ $ELAPSED -ge $SCRUB_WAIT_SECONDS ]]; then - log "WARNING: Scrub on $pool exceeded wait time. Moving on." - break - fi - done - log "Scrub complete (or timed out) for pool: $pool" -done - -# ── Gather data & build pool cards ─────────────────────────────────────────── - -OVERALL_STATUS="HEALTHY" # HEALTHY | WARNING | CRITICAL -POOLS_WITH_ISSUES=0 -POOL_CARDS_HTML="" - -for pool in $POOLS; do - STATUS_RAW=$(zpool list -H -o health "$pool") - SIZE=$(zpool list -H -o size "$pool") - USED=$(zpool list -H -o alloc "$pool") - FREE=$(zpool list -H -o free "$pool") - CAP=$(zpool list -H -o cap "$pool" | tr -d '%') - FRAG=$(zpool list -H -o frag "$pool") - SCRUB_LINE=$(zpool status "$pool" | grep -E "scan:") - ERRORS_LINE=$(zpool status "$pool" | grep "errors:") - CONFIG_BLOCK=$(zpool status "$pool" | awk '/config:/,/errors:/' | head -n -1) - FAULTED_LINES=$(echo "$CONFIG_BLOCK" | grep -E "FAULTED|DEGRADED|UNAVAIL|REMOVED" | grep -v "^$") - - # Per-pool severity - POOL_SEVERITY="HEALTHY" - ISSUE_DETAIL="" - - if [[ "$STATUS_RAW" == "DEGRADED" || "$STATUS_RAW" == "FAULTED" || "$STATUS_RAW" == "UNAVAIL" ]]; then - POOL_SEVERITY="CRITICAL" - OVERALL_STATUS="CRITICAL" - ISSUE_DETAIL="$FAULTED_LINES" - elif [[ "$CAP" -ge "$CAPACITY_WARN_THRESHOLD" ]]; then - POOL_SEVERITY="WARNING" - [[ "$OVERALL_STATUS" != "CRITICAL" ]] && OVERALL_STATUS="WARNING" - fi - - # Scrub error count - SCRUB_ERRORS=$(echo "$SCRUB_LINE" | grep -oP '\d+ errors' | head -1) - [[ -z "$SCRUB_ERRORS" ]] && SCRUB_ERRORS="0 errors" - - # Scrub date - SCRUB_DATE=$(echo "$SCRUB_LINE" | grep -oP '[A-Z][a-z]{2} \d+ \d+:\d+:\d+ \d{4}' | head -1) - [[ -z "$SCRUB_DATE" ]] && SCRUB_DATE="No scrub data" - - # Scrub age warning - SCRUB_AGE_NOTE="" - if [[ -n "$SCRUB_DATE" && "$SCRUB_DATE" != "No scrub data" ]]; then - SCRUB_EPOCH=$(date -d "$SCRUB_DATE" +%s 2>/dev/null) - NOW_EPOCH=$(date +%s) - DIFF_DAYS=$(( (NOW_EPOCH - SCRUB_EPOCH) / 86400 )) - if [[ $DIFF_DAYS -ge $SCRUB_AGE_WARN_DAYS ]]; then - SCRUB_AGE_NOTE=" — ⚠️ Last scrub was ${DIFF_DAYS} days ago" - [[ "$POOL_SEVERITY" == "HEALTHY" ]] && POOL_SEVERITY="WARNING" - [[ "$OVERALL_STATUS" != "CRITICAL" ]] && OVERALL_STATUS="WARNING" - fi - fi - - [[ "$POOL_SEVERITY" != "HEALTHY" ]] && POOLS_WITH_ISSUES=$((POOLS_WITH_ISSUES + 1)) - - # Colors - case "$STATUS_RAW" in - ONLINE) HEALTH_COLOR="#1a7f4b" ;; - DEGRADED) HEALTH_COLOR="#b91c1c" ;; - FAULTED) HEALTH_COLOR="#b91c1c" ;; - *) HEALTH_COLOR="#b45309" ;; - esac - - case "$POOL_SEVERITY" in - CRITICAL) CARD_BORDER="#fca5a5"; CARD_HEADER_BG="#fff1f2"; POOL_BADGE_BG="#b91c1c"; POOL_BADGE_LABEL="$STATUS_RAW" ;; - WARNING) CARD_BORDER="#fcd34d"; CARD_HEADER_BG="#fffbeb"; POOL_BADGE_BG="#b45309"; POOL_BADGE_LABEL="WARNING" ;; - *) CARD_BORDER="#86efac"; CARD_HEADER_BG="#f0fdf4"; POOL_BADGE_BG="#1a7f4b"; POOL_BADGE_LABEL="HEALTHY" ;; - esac - - CAP_COLOR="#111827" - [[ "$CAP" -ge "$CAPACITY_WARN_THRESHOLD" ]] && CAP_COLOR="#b45309" - [[ "$CAP" -ge 90 ]] && CAP_COLOR="#b91c1c" - - # Issue block - ISSUE_BLOCK="" - if [[ -n "$ISSUE_DETAIL" ]]; then - ESCAPED_DETAIL=$(echo "$ISSUE_DETAIL" | sed 's//\>/g') - ISSUE_BLOCK=" - -
$ESCAPED_DETAIL
- " - fi - - # Config block - ESCAPED_CONFIG=$(echo "$CONFIG_BLOCK" | sed 's//\>/g') - - POOL_CARDS_HTML+=" -
- - -
- - - - - -
- 🗄️ $pool - pool - - $POOL_BADGE_LABEL -
-
- - -
- - - - - - - - - - - - - - - $ISSUE_BLOCK -
-
Total
-
$SIZE
-
-
Used
-
$USED
-
-
Free
-
$FREE
-
-
Capacity
-
${CAP}%
-
-
Frag
-
$FRAG
-
-
Health
-
$STATUS_RAW
-
- - -
- 🔍 Last Scrub - Completed  |  $SCRUB_DATE  |  $SCRUB_ERRORS${SCRUB_AGE_NOTE} -
- - -
- ▶ vdev / drive configuration -
$ESCAPED_CONFIG
-
-
-
" -done - -# ── Overall banner & badge ──────────────────────────────────────────────────── -case "$OVERALL_STATUS" in - CRITICAL) - BADGE_HTML="
🚨 CRITICAL
" - BANNER_HTML="🚨  CRITICAL issue detected — immediate action required!" - ISSUES_COLOR="#b91c1c" - ;; - WARNING) - BADGE_HTML="
⚠️ WARNING
" - BANNER_HTML="⚠️  Warning condition detected — review recommended." - ISSUES_COLOR="#b45309" - ;; - *) - BADGE_HTML="
✅ ALL HEALTHY
" - BANNER_HTML="✅  All pools are healthy — no action required." - ISSUES_COLOR="#1a7f4b" - ;; -esac - -# ── Compose full HTML email ─────────────────────────────────────────────────── -HTML_BODY=$(cat < - - - - - -
- - - - - - - $BANNER_HTML - - - - - - - - - - -
- - - - - -
-
ZFS Pool Health Monitor
-
🔌 $HOSTNAME
-
$REPORT_TIME  |  $POOL_COUNT pool(s) monitored
-
$BADGE_HTML
-
- $POOL_CARDS_HTML -
-
-
📊 Report Summary
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Host$HOSTNAME
Report Time$REPORT_TIME
Pools Checked$POOL_COUNT
Pools with Issues$POOLS_WITH_ISSUES
Scrub Age Warning Threshold$SCRUB_AGE_WARN_DAYS days
Capacity Warning Threshold${CAPACITY_WARN_THRESHOLD}%
Overall Status$OVERALL_STATUS
-
-
- Automated ZFS Monitor  |  $HOSTNAME  |  KingDezigns Infrastructure -
-
- - -EOF -) - -# ── Send email via OMV Postfix (Gmail relay) ────────────────────────────────── -SUBJECT="[ZFS] $HOSTNAME — $OVERALL_STATUS | $REPORT_TIME" - -{ - echo "To: $REPORT_TO" - echo "From: $REPORT_FROM" - echo "Subject: $SUBJECT" - echo "MIME-Version: 1.0" - echo "Content-Type: text/html; charset=UTF-8" - echo "" - echo "$HTML_BODY" -} | sendmail -t - -log "Email sent to $REPORT_TO — Overall status: $OVERALL_STATUS" -log "===== ZFS Scrub complete on $HOSTNAME =====" diff --git a/zfs/nas08_zfs_scrub.sh b/zfs/nas08_zfs_scrub.sh index 4da08cf..54811dd 100644 --- a/zfs/nas08_zfs_scrub.sh +++ b/zfs/nas08_zfs_scrub.sh @@ -29,7 +29,6 @@ for pool in $POOLS; do log "Starting scrub on pool: $pool" zpool scrub "$pool" - # Wait for scrub to finish ELAPSED=0 while true; do SCRUB_STATE=$(zpool status "$pool" | grep -E "scan:" | awk '{print $2}') @@ -44,7 +43,7 @@ for pool in $POOLS; do log "Scrub complete (or timed out) for pool: $pool" done -# ── Gather data & build HTML report ────────────────────────────────────────── +# ── Gather data & build pool cards ─────────────────────────────────────────── OVERALL_STATUS="HEALTHY" # HEALTHY | WARNING | CRITICAL POOLS_WITH_ISSUES=0 @@ -62,7 +61,7 @@ for pool in $POOLS; do CONFIG_BLOCK=$(zpool status "$pool" | awk '/config:/,/errors:/' | head -n -1) FAULTED_LINES=$(echo "$CONFIG_BLOCK" | grep -E "FAULTED|DEGRADED|UNAVAIL|REMOVED" | grep -v "^$") - # Determine per-pool severity + # Per-pool severity POOL_SEVERITY="HEALTHY" ISSUE_DETAIL="" @@ -75,7 +74,7 @@ for pool in $POOLS; do [[ "$OVERALL_STATUS" != "CRITICAL" ]] && OVERALL_STATUS="WARNING" fi - # Scrub error count from status + # Scrub error count SCRUB_ERRORS=$(echo "$SCRUB_LINE" | grep -oP '\d+ errors' | head -1) [[ -z "$SCRUB_ERRORS" ]] && SCRUB_ERRORS="0 errors" @@ -98,7 +97,7 @@ for pool in $POOLS; do [[ "$POOL_SEVERITY" != "HEALTHY" ]] && POOLS_WITH_ISSUES=$((POOLS_WITH_ISSUES + 1)) - # Health value color + # Colors case "$STATUS_RAW" in ONLINE) HEALTH_COLOR="#1a7f4b" ;; DEGRADED) HEALTH_COLOR="#b91c1c" ;; @@ -106,97 +105,115 @@ for pool in $POOLS; do *) HEALTH_COLOR="#b45309" ;; esac - # Pool card header color case "$POOL_SEVERITY" in - CRITICAL) CARD_BG="#fff1f2"; BADGE_BG="#b91c1c"; BADGE_LABEL="$STATUS_RAW" ;; - WARNING) CARD_BG="#fffbeb"; BADGE_BG="#b45309"; BADGE_LABEL="WARNING" ;; - *) CARD_BG="#f0fdf4"; BADGE_BG="#1a7f4b"; BADGE_LABEL="HEALTHY" ;; + CRITICAL) CARD_BORDER="#fca5a5"; CARD_HEADER_BG="#fff1f2"; POOL_BADGE_BG="#b91c1c"; POOL_BADGE_LABEL="$STATUS_RAW" ;; + WARNING) CARD_BORDER="#fcd34d"; CARD_HEADER_BG="#fffbeb"; POOL_BADGE_BG="#b45309"; POOL_BADGE_LABEL="WARNING" ;; + *) CARD_BORDER="#86efac"; CARD_HEADER_BG="#f0fdf4"; POOL_BADGE_BG="#1a7f4b"; POOL_BADGE_LABEL="HEALTHY" ;; esac - # Capacity color CAP_COLOR="#111827" [[ "$CAP" -ge "$CAPACITY_WARN_THRESHOLD" ]] && CAP_COLOR="#b45309" - [[ "$CAP" -ge 90 ]] && CAP_COLOR="#b91c1c" + [[ "$CAP" -ge 90 ]] && CAP_COLOR="#b91c1c" - # Issue block (only shown when there's a problem) + # Issue block ISSUE_BLOCK="" if [[ -n "$ISSUE_DETAIL" ]]; then ESCAPED_DETAIL=$(echo "$ISSUE_DETAIL" | sed 's//\>/g') - ISSUE_BLOCK="
$ESCAPED_DETAIL
" + ISSUE_BLOCK=" + +
$ESCAPED_DETAIL
+ " fi # Config block ESCAPED_CONFIG=$(echo "$CONFIG_BLOCK" | sed 's//\>/g') POOL_CARDS_HTML+=" -
-
-
- 🗄️ $pool - pool -
- $BADGE_LABEL -
-
+
+ + +
- - - - - - - - - - -
-
Total Size
-
$SIZE
+
+ 🗄️ $pool + pool -
Used
-
$USED
-
-
Free
-
$FREE
-
-
Capacity
-
${CAP}%
-
-
Fragmentation
-
$FRAG
-
-
Health
-
$STATUS_RAW
+
+ $POOL_BADGE_LABEL
-
- 🔍 Last Scrub - Completed  |  $SCRUB_DATE  |  $SCRUB_ERRORS$SCRUB_AGE_NOTE +
+ + +
+ + + + + + + + + + + + + + + $ISSUE_BLOCK +
+
Total
+
$SIZE
+
+
Used
+
$USED
+
+
Free
+
$FREE
+
+
Capacity
+
${CAP}%
+
+
Frag
+
$FRAG
+
+
Health
+
$STATUS_RAW
+
+ + +
+ 🔍 Last Scrub + Completed  |  $SCRUB_DATE  |  $SCRUB_ERRORS${SCRUB_AGE_NOTE}
- $ISSUE_BLOCK -
- ▶ vdev / drive configuration + + +
+ ▶ vdev / drive configuration
$ESCAPED_CONFIG
" done -# ── Overall banner ──────────────────────────────────────────────────────────── +# ── Overall banner & badge ──────────────────────────────────────────────────── case "$OVERALL_STATUS" in CRITICAL) BADGE_HTML="
🚨 CRITICAL
" BANNER_HTML="🚨  CRITICAL issue detected — immediate action required!" + ISSUES_COLOR="#b91c1c" ;; WARNING) BADGE_HTML="
⚠️ WARNING
" BANNER_HTML="⚠️  Warning condition detected — review recommended." + ISSUES_COLOR="#b45309" ;; *) BADGE_HTML="
✅ ALL HEALTHY
" BANNER_HTML="✅  All pools are healthy — no action required." + ISSUES_COLOR="#1a7f4b" ;; esac @@ -210,6 +227,7 @@ HTML_BODY=$(cat < +
@@ -223,15 +241,18 @@ HTML_BODY=$(cat < + $BANNER_HTML + + +
$POOL_CARDS_HTML
-
📋 Report Summary
+
📊 Report Summary
@@ -250,7 +271,7 @@ HTML_BODY=$(cat < - + @@ -262,10 +283,16 @@ HTML_BODY=$(cat <Capacity Warning Threshold + + + + +
Host
Pools with Issues$POOLS_WITH_ISSUES$POOLS_WITH_ISSUES
${CAPACITY_WARN_THRESHOLD}%
Overall Status$OVERALL_STATUS
Automated ZFS Monitor  |  $HOSTNAME  |  KingDezigns Infrastructure