diff --git a/.gitignore b/.gitignore index ec3adea..ea1a8f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,17 @@ -# Editor temporary and backup files -*~ +# Local/editor junk +.DS_Store +Thumbs.db *.swp *.swo -*.bak -.#* +.vscode/* +!.vscode/extensions.json +.idea/ -# Temporary files and generated logs -*.tmp +# Script-local temp/output — scripts should write here if they need +# scratch space, never into the repo tree itself +tmp/ *.log -# Operating system files -.DS_Store -Thumbs.db \ No newline at end of file +# Local env overrides, if any script ends up needing one +.env +.env.local diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..b807867 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,8 @@ +# shellcheck config — applies to every script in this repo via `shellcheck` +# run from the repo root. See AGENTS.md for the mandatory-shellcheck rule. + +# Follow sourced files (needed so shellcheck resolves lib/common.sh +# correctly instead of flagging unknown functions as undefined). +external-sources=true + +shell=bash diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..91a458f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,85 @@ +# AGENTS.md — StrixScripts + +Инструкции за AI coding агенти (Claude Code, GitHub Copilot, Cursor и др.), +работещи в този repo. Ако инструментът ти чете `CLAUDE.md` вместо този +файл — `CLAUDE.md` е кратък pointer към този документ; съдържанието не се +дублира умишлено, за да не се разсинхронизира. + +## Какво е проектът + +Колекция от самостоятелни bash скриптове, организирани по функционални +папки. Всеки скрипт решава една конкретна задача и има собствена `.md` +документация до себе си. + +## Задължителна структура при всеки нов скрипт + +Когато създаваш нов скрипт, следните стъпки са ЗАДЪЛЖИТЕЛНИ, не опционални +— пропускането на която и да е от тях е непълна задача, не "готово с TODO": + +1. **Скриптът и неговият `.md` файл имат еднакво basename**, в една и + съща папка: `deploy/restart-nginx.sh` → `deploy/restart-nginx.md`. + Виж `templates/script-template.md` за структурата, която `.md` файлът + трябва да следва. + +2. **Скриптът source-ва `lib/common.sh`** и извиква + `common::show_help_if_requested "$@"` веднага след дефиниране на + `SCRIPT_HELP` heredoc-а, преди какъвто и да е друг argument parsing. + Виж `templates/script-template.sh` за точния pattern и + `examples/hello-world.sh` за работещ пример. + +3. **`--help` (и `-h`) работи и показва пълна информация** — какво прави + скриптът, какви опции приема, поне един пример за извикване. Това е + съдържанието на `SCRIPT_HELP`, не отделна имплементация — не пиши + ръчен `if [[ "$1" == "--help" ]]` в скрипта, common.sh вече го прави. + +4. **В `README.md` (корена на проекта) се добавя ред в секцията на + съответната категория папка** — кратко описание (едно изречение) + + relative link към `.md` файла на скрипта. Пълното описание живее само + в скрипт-специфичния `.md`, README не го дублира. + +5. **Скриптът минава `shellcheck` без warnings** (виж `.shellcheckrc` в + корена). Ако warning-ът е false positive, добави `# shellcheck disable=SCxxxx` + с коментар защо точно на реда, не глобално в файла. + +6. **`set -euo pipefail`** в началото на всеки скрипт, веднага след shebang + реда и коментарния блок. Без изключения — ако скрипт умишлено трябва да + продължи след грешка в конкретна команда, обгради само тази команда с + `|| true` или явна проверка, не маха global `set -e`. + +## Конвенции за именуване и стил + +- **Файлови имена:** kebab-case, описателни глагол+обект (`backup-nextcloud.sh`, + не `backup.sh` или `script1.sh`). +- **Категория папки:** функционална група (напр. `backup/`, `network/`, + `deploy/`) — създавай нова папка само когато скриптовете вътре наистина + споделят функционален домейн, не по проект или по дата. +- **Всички коментари в кода — само на английски**, независимо от езика на + разговора с агента. `.md` документацията и `README.md` могат да остават + на български — те са за човека, не за компилатора/shellcheck. +- **Грешки:** винаги през `common::die "message"`, никога суров + `echo ... >&2; exit 1` — за консистентен формат на грешките между + скриптове. +- **Статус/progress съобщения:** през `common::log`, който пише на stderr, + за да остане stdout чист за реален output, който може да бъде pipe-нат + или captured от друг скрипт/потребител. + +## `lib/common.sh` — НЕ дублирай логика от него + +Ако скрипт се нуждае от поведение, което `common.sh` не предоставя (напр. +retry logic, temp file cleanup trap, конкретен validation helper) — +разшири `common.sh`, не пиши локална версия вътре в отделния скрипт. Целта +е всеки скрипт да остава кратък и всички споделят едно и също поведение +за грешки/help/logging. + +## `examples/` и `templates/` — само референция + +`examples/hello-world.sh` е работещ демонстрационен скрипт, показващ +пълната конвенция накратко. `templates/` съдържа празни шаблони за копиране. +Нито едната папка не съдържа реални production скриптове — не ги викай от +други скриптове или automation, и не добавяй реални utility скриптове там. + +## Статус + +Проектът е в начален етап — само structural скелет (`lib/common.sh`, +`templates/`, `examples/hello-world.sh`) съществува. Реални функционални +категории папки и скриптове предстои да бъдат създадени. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..30efeed --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,7 @@ +# CLAUDE.md + +Пълните инструкции за работа в този repo са в [`AGENTS.md`](./AGENTS.md). +Прочети него първо — този файл е само pointer, за да не поддържаме два +дублиращи се документа, които се разсинхронизират. + +Claude-специфично: няма допълнителни инструкции извън `AGENTS.md` в момента. diff --git a/HDD/erase-storage.md b/HDD/erase-storage.md new file mode 100644 index 0000000..41c1d02 --- /dev/null +++ b/HDD/erase-storage.md @@ -0,0 +1,58 @@ +# erase-storage.sh + +Изтрива един дял с еднократен запис на нули или псевдослучайни данни, или +използва поддържания от устройството ATA Secure Erase/NVMe Sanitize за цял диск. + +## Какво прави + +При подаване на дял, скриптът записва точно размера на целия дял един път. По подразбиране +използва нули; може да се избере и `/dev/urandom`. Изисква root права, +проверява че устройството не е монтирано, не е активен swap и не участва в +root filesystem, след което иска потвърждение чрез въвеждане на точния път. + +При подаване на цял диск скриптът проверява поддръжката преди да продължи. +За NVMe избира наличния Sanitize метод в следния ред: Crypto Erase, Block +Erase, Overwrite. За ATA избира Enhanced Security Erase, ако устройството го +обявява, иначе стандартен Security Erase. Ако методът не може да бъде +потвърден, устройството е замразено/вече е с активирана ATA защита или +информацията за capability е неясна, скриптът отказва да продължи. Преди +изтриването на цял диск показва отделно предупреждение и изисква две +потвърждения, като второто е точното име на устройството. + +**Изтриването е необратимо и при режим за цял диск засяга всички дялове.** +Не прекъсвай захранването или връзката с диска. При прекъсване на ATA +Security Erase устройството може да остане заключено с временната парола, +посочена в съобщението за грешка. Secure Erase/Sanitize зависи от коректната +реализация във firmware на устройството; скриптът не може да гарантира +възстановимост или физическо състояние на носителя. + +## Изисквания + +- Linux, Bash и root права (`sudo`) +- `lsblk`, `findmnt`, `swapon`, `dd` и GNU/Linux стандартни помощни команди +- За NVMe: `nvme-cli` и `jq` +- За ATA: `hdparm` + +## Употреба + +```bash +sudo ./HDD/erase-storage.sh --help +sudo ./HDD/erase-storage.sh /dev/sda1 +sudo ./HDD/erase-storage.sh /dev/nvme0n1p2 random +sudo ./HDD/erase-storage.sh /dev/sda +sudo ./HDD/erase-storage.sh /dev/nvme0n1 +``` + +`dev/sda1` без начална наклонена черта също се приема като `/dev/sda1`. + +| Аргумент | Описание | +|----------|----------| +| `DEVICE` | Block device — дял или цял диск | +| `zero` | Един проход с нули върху дял; режим по подразбиране | +| `random` | Един проход с псевдослучайни данни върху дял | +| `-h`, `--help` | Показва помощта и приключва | + +Режимът `zero`/`random` не е Secure Erase и не гарантира изтриване на +пренасочени или недостъпни физически сектори. При SSD не използвай обикновен +запис към дял като гаранция за заличаване; използвай само успешно завършен +Sanitize/Secure Erase, поддържан от устройството. \ No newline at end of file diff --git a/HDD/erase-storage.sh b/HDD/erase-storage.sh new file mode 100755 index 0000000..c2a564b --- /dev/null +++ b/HDD/erase-storage.sh @@ -0,0 +1,240 @@ +#!/usr/bin/env bash +# +# erase-storage.sh — overwrite a partition or securely erase a whole disk. +# Full documentation: see erase-storage.md in this same folder. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/common.sh +source "${SCRIPT_DIR}/../lib/common.sh" + +SCRIPT_HELP=$(cat <<'HELP' +Usage: sudo ./HDD/erase-storage.sh DEVICE [zero|random] + +For a partition, writes one full pass of zeros (default) or pseudorandom data. +For a whole disk, detects and runs a supported ATA Secure Erase or NVMe Sanitize +operation. Whole-disk erasure requires two separate confirmations. + +Arguments: + DEVICE Partition (for example /dev/sda1) or whole disk + zero Write zeros to a partition (default) + random Write pseudorandom data to a partition + +Options: + -h, --help Show this help message and exit + +Examples: + sudo ./HDD/erase-storage.sh /dev/sda1 + sudo ./HDD/erase-storage.sh /dev/nvme0n1p2 random + sudo ./HDD/erase-storage.sh /dev/sda + sudo ./HDD/erase-storage.sh /dev/nvme0n1 +HELP +) + +common::show_help_if_requested "$@" + +[[ $# -ge 1 && $# -le 2 ]] || common::die "expected DEVICE [zero|random]; run with --help for usage" + +DEVICE_INPUT="$1" +if [[ "$DEVICE_INPUT" == dev/* ]]; then + DEVICE_INPUT="/$DEVICE_INPUT" +fi +[[ "$DEVICE_INPUT" == /dev/* ]] || common::die "DEVICE must be a path under /dev" + +FILL_MODE="${2:-zero}" +case "$FILL_MODE" in + zero|random) + ;; + *) + common::die "partition fill mode must be 'zero' or 'random'" + ;; +esac + +for required_command in findmnt lsblk readlink swapon date dd sleep; do + common::require_cmd "$required_command" +done + +[[ "$EUID" -eq 0 ]] || common::die "this script must be run as root (for example, with sudo)" +[[ -b "$DEVICE_INPUT" ]] || common::die "$DEVICE_INPUT does not exist or is not a block device" + +DEVICE="$(readlink -f -- "$DEVICE_INPUT")" || common::die "cannot resolve device path: $DEVICE_INPUT" +DEVICE_TYPE="$(lsblk -dnro TYPE -- "$DEVICE")" || common::die "cannot determine device type" +[[ "$DEVICE_TYPE" == "part" || "$DEVICE_TYPE" == "disk" ]] \ + || common::die "only a partition or a whole disk is accepted (detected: $DEVICE_TYPE)" + +ROOT_SOURCE="$(findmnt -n -o SOURCE /)" || common::die "cannot determine the root filesystem device" +ROOT_SOURCE="${ROOT_SOURCE%%\[*}" +[[ -b "$ROOT_SOURCE" ]] || common::die "cannot safely resolve the root filesystem device ($ROOT_SOURCE)" +ROOT_DEVICE="$(readlink -f -- "$ROOT_SOURCE")" || common::die "cannot resolve the root filesystem device" +ROOT_TREE="$(lsblk --inverse --list --noheadings --paths --output PATH -- "$ROOT_DEVICE")" \ + || common::die "cannot determine the devices backing the root filesystem" + +while IFS= read -r root_device; do + [[ -n "$root_device" ]] || continue + if [[ "$(readlink -f -- "$root_device")" == "$DEVICE" ]]; then + common::die "$DEVICE backs the root filesystem; refusing to erase it" + fi +done <<< "$ROOT_TREE" + +DEVICE_TREE="$(lsblk --list --noheadings --paths --output PATH -- "$DEVICE")" \ + || common::die "cannot inspect devices below $DEVICE" +ACTIVE_SWAP_DEVICES="$(swapon --show --noheadings --raw --output NAME)" \ + || common::die "cannot inspect active swap devices" + +while IFS= read -r child_device; do + [[ -n "$child_device" ]] || continue + if findmnt -rn -S "$child_device" >/dev/null 2>&1; then + common::die "$child_device is mounted; unmount it before running this script" + fi + + while IFS= read -r swap_device; do + [[ -n "$swap_device" ]] || continue + if [[ -b "$swap_device" ]]; then + swap_source="$swap_device" + else + swap_mount_source="$(findmnt -n -o SOURCE -T "$swap_device")" \ + || common::die "cannot determine the filesystem backing active swap file $swap_device" + swap_mount_source="${swap_mount_source%%\[*}" + [[ -b "$swap_mount_source" ]] \ + || common::die "cannot safely resolve the block device backing active swap file $swap_device" + swap_source="$swap_mount_source" + fi + + swap_source_tree="$(lsblk --inverse --list --noheadings --paths --output PATH -- "$swap_source")" \ + || common::die "cannot determine the devices backing active swap $swap_device" + while IFS= read -r swap_backing_device; do + [[ -n "$swap_backing_device" ]] || continue + if [[ "$(readlink -f -- "$swap_backing_device")" == "$(readlink -f -- "$child_device")" ]]; then + common::die "$child_device backs active swap ($swap_device); disable it before running this script" + fi + done <<< "$swap_source_tree" + done <<< "$ACTIVE_SWAP_DEVICES" +done <<< "$DEVICE_TREE" + +if [[ "$DEVICE_TYPE" == "part" ]]; then + [[ $# -eq 1 || "$FILL_MODE" == "zero" || "$FILL_MODE" == "random" ]] \ + || common::die "invalid fill mode; use 'zero' or 'random'" + DEVICE_SIZE="$(lsblk -dnro SIZE --bytes -- "$DEVICE")" || common::die "cannot determine partition size" + + common::log "WARNING: this will irreversibly overwrite all data on partition $DEVICE ($DEVICE_SIZE bytes)." + read -r -p "Type the exact device path '$DEVICE' to continue: " CONFIRM + [[ "$CONFIRM" == "$DEVICE" ]] || common::die "operation cancelled; no data was changed" + + common::log "Writing one full $FILL_MODE pass to $DEVICE." + if [[ "$FILL_MODE" == "zero" ]]; then + dd if=/dev/zero of="$DEVICE" bs=4M count="$DEVICE_SIZE" iflag=count_bytes conv=fsync status=progress + else + dd if=/dev/urandom of="$DEVICE" bs=4M count="$DEVICE_SIZE" iflag=count_bytes conv=fsync status=progress + fi + common::log "Partition overwrite completed successfully." + exit 0 +fi + +[[ $# -eq 1 ]] || common::die "a fill mode is only valid for a partition" +DEVICE_SIZE="$(lsblk -dnro SIZE --bytes -- "$DEVICE")" || common::die "cannot determine disk size" +DEVICE_NAME="${DEVICE##*/}" +ERASE_METHOD="" +ERASE_TOOL="" +NVME_ACTION="" +NVME_ACTION_NAME="" +ATA_ERASE_OPTION="" + +if [[ "$DEVICE_NAME" =~ ^(nvme[0-9]+)n[0-9]+$ ]]; then + common::require_cmd nvme + common::require_cmd jq + NVME_CONTROLLER="/dev/${BASH_REMATCH[1]}" + NVME_IDENTIFY="$(nvme id-ctrl "$NVME_CONTROLLER" -o json)" \ + || common::die "could not read NVMe controller capabilities" + SANICAP="$(jq -er '.sanicap' <<< "$NVME_IDENTIFY")" \ + || common::die "NVMe controller did not report SANICAP" + [[ "$SANICAP" =~ ^[0-9]+$ ]] || common::die "invalid NVMe SANICAP value: $SANICAP" + SANICAP_VALUE=$((SANICAP)) + + if (( SANICAP_VALUE & 1 )); then + NVME_ACTION=4 + NVME_ACTION_NAME="Crypto Erase" + elif (( SANICAP_VALUE & 2 )); then + NVME_ACTION=2 + NVME_ACTION_NAME="Block Erase" + elif (( SANICAP_VALUE & 4 )); then + NVME_ACTION=3 + NVME_ACTION_NAME="Overwrite" + else + common::die "NVMe controller does not advertise a supported Sanitize action" + fi + ERASE_METHOD="NVMe Sanitize: $NVME_ACTION_NAME" + ERASE_TOOL="nvme" +else + common::require_cmd hdparm + common::require_cmd awk + common::require_cmd grep + ATA_IDENTIFY="$(hdparm -I "$DEVICE" 2>&1)" || common::die "could not read ATA security capabilities" + ATA_SECURITY="$(awk ' + /^[[:space:]]*Security:/ { in_security = 1; print; next } + in_security && /^[^[:space:]]/ { exit } + in_security { print } + ' <<< "$ATA_IDENTIFY")" + [[ -n "$ATA_SECURITY" ]] || common::die "device did not report an ATA Security section" + grep -Eiq '^[[:space:]]+supported[[:space:]]*$' <<< "$ATA_SECURITY" \ + || common::die "device does not report ATA Security Erase support" + grep -Eiq '^[[:space:]]+not frozen[[:space:]]*$' <<< "$ATA_SECURITY" \ + || common::die "ATA security is frozen or its state could not be determined; refusing to continue" + grep -Eiq '^[[:space:]]+not enabled[[:space:]]*$' <<< "$ATA_SECURITY" \ + || common::die "ATA security is already enabled; refusing to risk leaving the disk locked" + + if grep -Eiq 'supported:[[:space:]]+enhanced erase' <<< "$ATA_SECURITY"; then + ATA_ERASE_OPTION="--security-erase-enhanced" + ERASE_METHOD="ATA Enhanced Security Erase" + else + ATA_ERASE_OPTION="--security-erase" + ERASE_METHOD="ATA Security Erase" + fi + ERASE_TOOL="hdparm" +fi + +common::log "WARNING: whole-disk operation on $DEVICE ($DEVICE_SIZE bytes)." +common::log "Detected supported method: $ERASE_METHOD. ALL DATA on this disk will be destroyed." +read -r -p "First confirmation: type ERASE to continue: " FIRST_CONFIRM +[[ "$FIRST_CONFIRM" == "ERASE" ]] || common::die "operation cancelled; no data was changed" +common::log "Final warning: $ERASE_METHOD will erase the entire disk $DEVICE, including every partition." +read -r -p "Final confirmation: type the exact device name '$DEVICE_NAME': " FINAL_CONFIRM +[[ "$FINAL_CONFIRM" == "$DEVICE_NAME" ]] || common::die "operation cancelled; no data was changed" + +if [[ "$ERASE_TOOL" == "nvme" ]]; then + common::log "Starting $ERASE_METHOD on $DEVICE. The device may continue sanitizing after the command returns." + nvme sanitize "$DEVICE" --sanact="$NVME_ACTION" + + while true; do + SANITIZE_LOG="$(nvme sanitize-log "$DEVICE" -o json)" \ + || common::die "could not read NVMe Sanitize status; the operation may still be running" + SANITIZE_STATUS="$(jq -er '.sstat' <<< "$SANITIZE_LOG")" \ + || common::die "NVMe Sanitize status was missing or invalid" + [[ "$SANITIZE_STATUS" =~ ^[0-9]+$ ]] || common::die "invalid NVMe Sanitize status: $SANITIZE_STATUS" + + case $((SANITIZE_STATUS & 7)) in + 1|4) + common::log "NVMe Sanitize completed successfully." + break + ;; + 2) + common::log "NVMe Sanitize is still in progress; checking again in 30 seconds." + sleep 30 + ;; + 3) + common::die "NVMe Sanitize reported failure; consult the device sanitize log" + ;; + *) + common::die "NVMe Sanitize is not running or reported an unknown status ($SANITIZE_STATUS)" + ;; + esac + done +else + common::log "Starting $ERASE_METHOD on $DEVICE. Do not interrupt power or disconnect the drive." + ATA_PASSWORD="strix-erase" + hdparm --user-master u --security-set-pass "$ATA_PASSWORD" "$DEVICE" + if ! hdparm --user-master u "$ATA_ERASE_OPTION" "$ATA_PASSWORD" "$DEVICE"; then + common::die "ATA erase failed; the disk may remain security-locked with the temporary password '$ATA_PASSWORD'" + fi + common::log "$ERASE_METHOD completed successfully." +fi \ No newline at end of file diff --git a/HDD/repair-hdd.md b/HDD/repair-hdd.md new file mode 100644 index 0000000..a27d0d7 --- /dev/null +++ b/HDD/repair-hdd.md @@ -0,0 +1,53 @@ +# repair-hdd.sh + +Презаписва избран диск, за да задейства remap на проблемни сектори, след +което изпълнява SMART extended self-test и показва финален отчет. + +## Какво прави + +Скриптът записва целия block device с нули или псевдослучайни данни. След +това стартира продължителен SMART self-test и изчаква очакваното му време, +преди да покаже резултата и SMART атрибутите. Изходът се записва в +`/var/log/hdd-repair/`. + +**Операцията унищожава всички данни на избрания диск.** Преди запис скриптът +проверява дали дискът участва в root filesystem, проверява за монтирани +дъщерни устройства и активен swap, и изисква изрично потвърждение `YES`. +Ако дъщерно устройство не може да се демонтира, скриптът прекратява +операцията. + +Използвай само с правилно идентифициран диск, който можеш безопасно да +изтриеш. Изисква пряка SATA връзка; `smartctl` не използва `-d sat`, затова +USB адаптер може да изисква ръчна промяна на извикванията в скрипта. + +## Изисквания + +- Linux и Bash +- Root права (`sudo`) +- `smartctl` от пакета smartmontools +- `findmnt`, `lsblk`, `readlink`, `swapon`, `umount` и стандартните GNU/Linux + помощни команди, използвани от скрипта +- OpenSSL е незадължителен; при липса режимът `random` използва `/dev/urandom` + +## Употреба + +```bash +sudo ./HDD/repair-hdd.sh --help +sudo ./HDD/repair-hdd.sh /dev/sdX +sudo ./HDD/repair-hdd.sh /dev/sdX zero +sudo ./HDD/repair-hdd.sh /dev/sdX null +sudo ./HDD/repair-hdd.sh /dev/sdX random +``` + +| Аргумент | Описание | +|----------|----------| +| `DEVICE` | Целият диск, например `/dev/sdX`, не дял от него | +| `zero` | Записва нули; режим по подразбиране | +| `null` | Синоним на `zero` | +| `random` | Записва псевдослучайни данни | +| `-h`, `--help` | Показва помощта и приключва | + +Изпълнението може да продължи часове. Не прекъсвай захранването или връзката +с диска по време на записа или SMART теста. SMART remap не възстановява +данни и не гарантира, че дискът ще стане надежден; запази резервно копие и +подмени диск с повтарящи се грешки. \ No newline at end of file diff --git a/HDD/repair-hdd.sh b/HDD/repair-hdd.sh new file mode 100755 index 0000000..b467f07 --- /dev/null +++ b/HDD/repair-hdd.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env bash +# +# repair-hdd.sh — overwrite a disk to trigger remapping of pending/bad sectors, +# then run a SMART extended self-test and print a final report. +# Full documentation: see repair-hdd.md in this same folder. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/common.sh +source "${SCRIPT_DIR}/../lib/common.sh" + +SCRIPT_HELP=$(cat <<'HELP' +Usage: sudo ./HDD/repair-hdd.sh DEVICE [zero|null|random] + +Overwrites the selected disk, then runs a SMART extended self-test. This +destroys all data on DEVICE. Use only with a directly connected SATA disk; +USB adapters may require adding -d sat to the smartctl calls. + +Arguments: + DEVICE Whole-disk block device, for example /dev/sdX + zero Write zeros (default; also accepted as "null") + random Write pseudorandom data + +Options: + -h, --help Show this help message and exit + +Examples: + sudo ./HDD/repair-hdd.sh /dev/sdX + sudo ./HDD/repair-hdd.sh /dev/sdX random +HELP +) + +common::show_help_if_requested "$@" + +[[ $# -le 2 ]] || common::die "too many arguments; run with --help for usage" +LOG_DIR="/var/log/hdd-repair" +DEVICE="${1:-}" +FILL_MODE="${2:-zero}" + +# --- Валидация на входа --------------------------------------------------- + +if [[ -z "$DEVICE" ]]; then + common::die "a whole-disk block device is required; run with --help for usage" +fi + +case "$FILL_MODE" in + zero|null) + FILL_MODE="zero" + ;; + random) + ;; + *) + common::die "fill mode must be 'zero', 'null', or 'random'" + ;; +esac + +for required_command in findmnt lsblk readlink swapon mkdir date tee smartctl grep umount dd sync sleep head base64; do + common::require_cmd "$required_command" +done + +if [[ "$EUID" -ne 0 ]]; then + common::die "this script must be run as root (for example, with sudo)" +fi + +if [[ ! -b "$DEVICE" ]]; then + common::die "$DEVICE does not exist or is not a block device" +fi + +# Refuse the root filesystem device and any block device it depends on. +TARGET_DEVICE="$(readlink -f -- "$DEVICE")" || common::die "cannot resolve device path: $DEVICE" +ROOT_SOURCE="$(findmnt -n -o SOURCE /)" || common::die "cannot determine the root filesystem device" +ROOT_SOURCE="${ROOT_SOURCE%%\[*}" +ROOT_DEVICE="$(readlink -f -- "$ROOT_SOURCE")" || common::die "cannot resolve root device path: $ROOT_SOURCE" +ROOT_DEVICE_TREE="$(lsblk --inverse --list --noheadings --paths --output PATH "$ROOT_DEVICE")" \ + || common::die "cannot determine the devices backing the root filesystem" + +while IFS= read -r root_device; do + [[ -n "$root_device" ]] || continue + if [[ "$(readlink -f -- "$root_device")" == "$TARGET_DEVICE" ]]; then + common::die "$DEVICE backs the root filesystem; refusing to overwrite it" + fi +done <<< "$ROOT_DEVICE_TREE" + +DEVICE_TREE="$(lsblk --list --noheadings --paths --output PATH "$TARGET_DEVICE")" \ + || common::die "cannot inspect block devices below $DEVICE" + +while IFS= read -r child_device; do + [[ -n "$child_device" ]] || continue + while IFS= read -r swap_device; do + [[ -n "$swap_device" ]] || continue + if [[ "$(readlink -f -- "$swap_device")" == "$(readlink -f -- "$child_device")" ]]; then + common::die "$child_device is active swap; disable it before running this script" + fi + done < <(swapon --show --noheadings --raw --output NAME) +done <<< "$DEVICE_TREE" + +if [[ "$FILL_MODE" == "random" ]] && ! command -v openssl >/dev/null 2>&1; then + common::log "OpenSSL is unavailable; random mode will use /dev/urandom." +fi + +mkdir -p "$LOG_DIR" +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +MAIN_LOG="$LOG_DIR/hdd-repair-${DEVICE##*/}-${TIMESTAMP}.log" + +# Send all output to both the console and the log file. +exec > >(tee -a "$MAIN_LOG") 2>&1 + +common::log "==============================================================" +common::log "HDD Repair — $DEVICE" +common::log "Write mode: $FILL_MODE" +common::log "Started: $(date '+%Y-%m-%d %H:%M:%S')" +common::log "Log: $MAIN_LOG" +common::log "==============================================================" + +# --- Disk information before the operation -------------------------------- + +common::log "--- Device identification ---" +smartctl -i "$DEVICE" || true + +common::log "--- SMART values before overwrite ---" +smartctl -A "$DEVICE" | grep -E \ + "Reallocated_Sector_Ct|Current_Pending_Sector|Offline_Uncorrectable|UDMA_CRC_Error_Count|Power_On_Hours" \ + || common::log "SMART read failed; check the connection." + +common::log "WARNING: ALL DATA ON $DEVICE WILL BE DESTROYED." +read -r -p "Type uppercase YES to confirm: " CONFIRM +if [[ "$CONFIRM" != "YES" ]]; then + common::die "operation cancelled; no changes were made" +fi + +while IFS= read -r child_device; do + [[ -n "$child_device" ]] || continue + if findmnt -n -S "$child_device" >/dev/null 2>&1; then + common::log "Unmounting $child_device..." + umount "$child_device" || common::die "could not unmount $child_device; refusing to overwrite $DEVICE" + fi +done <<< "$DEVICE_TREE" + +# --- Phase 1: overwrite the disk to trigger sector remapping --------------- + +common::log "--- Phase 1: overwriting $DEVICE ---" +common::log "Started: $(date '+%Y-%m-%d %H:%M:%S')" +common::log "This may take hours, depending on disk size and speed." + +DD_START=$(date +%s) +DD_RESULT=0 + +if [[ "$FILL_MODE" == "zero" ]]; then + RANDOM_SOURCE_DESC="/dev/zero" + dd if=/dev/zero of="$DEVICE" bs=4M conv=fsync,noerror status=progress || DD_RESULT=$? +else + # Use OpenSSL for faster pseudorandom generation when available. + if command -v openssl >/dev/null 2>&1; then + RANDOM_SOURCE_DESC="openssl enc (AES-256-CTR PRNG)" + openssl enc -aes-256-ctr -pass pass:"$(head -c 32 /dev/urandom | base64)" \ + -nosalt < /dev/zero 2>/dev/null | \ + dd of="$DEVICE" bs=4M conv=fsync,noerror status=progress || DD_RESULT=$? + else + RANDOM_SOURCE_DESC="/dev/urandom (по-бавно, openssl не е намерен)" + dd if=/dev/urandom of="$DEVICE" bs=4M conv=fsync,noerror status=progress || DD_RESULT=$? + fi +fi + +common::log "Data source: $RANDOM_SOURCE_DESC" +if [[ "$DD_RESULT" -eq 0 ]]; then + common::log "dd completed successfully (no write I/O errors)." +else + common::log "dd exited with status $DD_RESULT; the disk may have reached its end." +fi +DD_END=$(date +%s) +common::log "Overwrite finished: $(date '+%Y-%m-%d %H:%M:%S')" +common::log "Duration: $(( (DD_END - DD_START) / 3600 ))h $(( ((DD_END - DD_START) % 3600) / 60 ))m" + +sync +common::log "Waiting 10 seconds before SMART verification." +sleep 10 + +common::log "--- SMART values after overwrite ---" +smartctl -A "$DEVICE" | grep -E \ + "Reallocated_Sector_Ct|Current_Pending_Sector|Offline_Uncorrectable|UDMA_CRC_Error_Count" \ + || common::log "SMART read failed." + +# --- Phase 2: SMART extended self-test ------------------------------------- + +common::log "--- Phase 2: starting SMART extended self-test ---" +smartctl -t long "$DEVICE" + +# Read the estimated duration from smartctl, falling back to 360 minutes. +EST_MIN="$(smartctl -c "$DEVICE" 2>/dev/null | grep -oP 'Extended self-test routine\s*\K[0-9]+' || true)" +EST_MIN="${EST_MIN:-360}" +WAIT_SECONDS=$(( (EST_MIN + 10) * 60 )) + +common::log "Expected self-test duration: about ${EST_MIN} minutes." +common::log "Waiting $(( WAIT_SECONDS / 60 )) minutes before reading the result." +common::log "The drive firmware runs the self-test while this script waits." + +sleep "$WAIT_SECONDS" + +common::log "--- Self-test result ---" +smartctl -l selftest "$DEVICE" + +# --- Final report ---------------------------------------------------------- + +common::log "==============================================================" +common::log "Final SMART report — $DEVICE" +common::log "Finished: $(date '+%Y-%m-%d %H:%M:%S')" +common::log "==============================================================" +smartctl -A "$DEVICE" | grep -E \ + "Reallocated_Sector_Ct|Current_Pending_Sector|Offline_Uncorrectable|UDMA_CRC_Error_Count|Reallocated_Event_Count" +smartctl -H "$DEVICE" +common::log "Full log: $MAIN_LOG" +common::log "Compare these values with the initial values in the log:" +common::log "Current_Pending_Sector should decrease (ideally to 0)." +common::log "Reallocated_Sector_Ct may increase as sectors are remapped." +common::log "If pending sectors remain, the drive may be unable to write them." +common::log "==============================================================" diff --git a/README.md b/README.md new file mode 100644 index 0000000..952bdb1 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# StrixScripts + +Колекция от самостоятелни bash скриптове, организирани по функционални +папки. Всеки скрипт има собствена `.md` документация до себе си с пълно +описание, изисквания и примери. + +За AI агенти, работещи в repo-то: виж [`AGENTS.md`](./AGENTS.md) за +задължителните конвенции при създаване на нов скрипт — структура, +`--help` поведение, shellcheck, документация. + +## Структура + +``` +strixscripts/ +├── lib/ +│ └── common.sh споделени helper функции (--help, error handling, logging) +├── templates/ шаблони за нов скрипт + неговия .md файл +├── examples/ +│ └── hello-world.sh работещ пример, демонстриращ пълната конвенция +├── HDD/ +│ ├── repair-hdd.sh презапис на диск и SMART extended self-test +│ └── erase-storage.sh презапис на дял или Secure Erase/Sanitize на диск +└── <категория>/ функционална група скриптове (предстои) +``` + +## Как се използва скрипт + +Всеки скрипт поддържа `--help`: + +```bash +./<категория>/.sh --help +``` + +## Категории + + + +### examples/ (референция, не production скриптове) + +- [`hello-world.sh`](./examples/hello-world.md) — демонстрационен скрипт, + показващ конвенцията за нов скрипт в repo-то. + +### HDD/ + +- [`repair-hdd.sh`](./HDD/repair-hdd.md) — презаписва избран диск и + изпълнява SMART extended self-test. +- [`erase-storage.sh`](./HDD/erase-storage.md) — презаписва дял с един проход + или използва поддържан Secure Erase/Sanitize за цял диск. diff --git a/examples/hello-world.md b/examples/hello-world.md new file mode 100644 index 0000000..4ae8e1a --- /dev/null +++ b/examples/hello-world.md @@ -0,0 +1,45 @@ +# hello-world.sh + +Reference example script — prints a greeting. Demonstrates the required +structure every script in this repo must follow: `common.sh` sourcing, +`SCRIPT_HELP` heredoc, `--help` handling, and argument parsing with +`common::die` on invalid input. + +**This is not a real utility.** Copy its structure when writing an actual +script; don't invoke it from other scripts or automation. + +## What it does + +Prints `Hello, !` to stdout, where `` defaults to `world` or +whatever is passed via `--name`. + +## Requirements + +- Bash 4+ (uses `set -euo pipefail`, no external commands beyond common.sh) + +## Usage + +```bash +./examples/hello-world.sh [OPTIONS] +``` + +### Options + +| Option | Description | +|--------|-------------| +| `-h`, `--help` | Show help message and exit | +| `-n`, `--name NAME` | Name to greet (default: `world`) | + +## Examples + +```bash +# Default greeting +./examples/hello-world.sh +# → Hello, world! +``` + +```bash +# Custom name +./examples/hello-world.sh --name Danail +# → Hello, Danail! +``` diff --git a/examples/hello-world.sh b/examples/hello-world.sh new file mode 100755 index 0000000..004071c --- /dev/null +++ b/examples/hello-world.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# +# hello-world.sh — reference example demonstrating the StrixScripts +# conventions (see AGENTS.md). Not a real utility script — copy this +# pattern when creating an actual script, don't call this one from +# anywhere real. +# +# Full documentation: see hello-world.md in this same folder. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/common.sh +source "${SCRIPT_DIR}/../lib/common.sh" + +SCRIPT_HELP=$(cat <<'HELP' +Usage: hello-world.sh [-n NAME] + +Prints a greeting. Reference example only — demonstrates the SCRIPT_HELP +pattern, argument parsing, and common.sh usage that every real script in +this repo should follow. + +Options: + -h, --help Show this help message and exit + -n, --name NAME Name to greet (default: world) + +Examples: + ./hello-world.sh + ./hello-world.sh --name Danail +HELP +) + +common::show_help_if_requested "$@" + +name="world" +while [[ $# -gt 0 ]]; do + case "$1" in + -n|--name) + [[ $# -ge 2 ]] || common::die "--name requires a value" + name="$2" + shift 2 + ;; + *) + common::die "unknown argument: $1" + ;; + esac +done + +common::log "Greeting ${name}..." +echo "Hello, ${name}!" diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..063500f --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# +# common.sh — shared helper functions for all StrixScripts scripts. +# +# Every script in this repo MUST source this file. It provides: +# - --help handling driven by a script-defined SCRIPT_HELP heredoc +# - consistent error/log output +# - a command-existence guard +# +# Usage from a script: +# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# source "${SCRIPT_DIR}/../lib/common.sh" # adjust ../ depth to match nesting +# +# Do NOT duplicate this logic inside individual scripts — if a script needs +# behavior this file doesn't provide, extend this file instead, so every +# script stays consistent. + +set -euo pipefail + +# common::show_help_if_requested scans argv for -h/--help and, if present, +# prints the calling script's $SCRIPT_HELP and exits 0. +# +# Call this IMMEDIATELY after defining SCRIPT_HELP and BEFORE any other +# argument parsing — --help must win regardless of what else is on the +# command line (including invalid flags). +common::show_help_if_requested() { + local arg + for arg in "$@"; do + if [[ "$arg" == "-h" || "$arg" == "--help" ]]; then + if [[ -z "${SCRIPT_HELP:-}" ]]; then + echo "Error: SCRIPT_HELP is not defined for this script." >&2 + exit 1 + fi + echo "$SCRIPT_HELP" + exit 0 + fi + done +} + +# common::die prints an error to stderr and exits with the given code +# (default 1). Use this for every fatal error instead of a raw `exit 1`, +# so error output is consistent across all scripts. +common::die() { + local message="$1" + local code="${2:-1}" + echo "Error: ${message}" >&2 + exit "$code" +} + +# common::log prints a timestamped status line to stderr. stdout is left +# clean for actual script output that might be piped or captured by a +# caller — never write status/progress messages to stdout. +common::log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2 +} + +# common::require_cmd fails fast with a clear message if a required +# external command is missing from PATH, instead of letting the script +# fail later with a cryptic "command not found". +common::require_cmd() { + local cmd="$1" + if ! command -v "$cmd" &>/dev/null; then + common::die "required command '${cmd}' not found on PATH" + fi +} diff --git a/templates/script-template.md b/templates/script-template.md new file mode 100644 index 0000000..e33ab9e --- /dev/null +++ b/templates/script-template.md @@ -0,0 +1,48 @@ +# .sh + + + +## What it does + + + +## Requirements + +- +- + +## Usage + +```bash +.//.sh [OPTIONS] +``` + +### Options + +| Option | Description | +|--------|-------------| +| `-h`, `--help` | Show help message and exit | +| `` | | + +## Examples + +```bash +# +./.sh +``` + +```bash +# +./.sh --example value +``` + +## Notes / Caveats + + diff --git a/templates/script-template.sh b/templates/script-template.sh new file mode 100755 index 0000000..8853b15 --- /dev/null +++ b/templates/script-template.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# .sh — +# +# Full documentation: see .md in this same folder. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/common.sh +# Adjust the relative depth below to match this script's folder nesting +# relative to lib/ — e.g. "../../lib/common.sh" if the script is two +# folders deep from the repo root. +source "${SCRIPT_DIR}/../lib/common.sh" + +SCRIPT_HELP=$(cat <<'HELP' +Usage: .sh [OPTIONS] + + + +Options: + -h, --help Show this help message and exit + <-x, --example> + +Examples: + ./.sh + ./.sh --example value +HELP +) + +# --help must be checked before any other argument parsing. +common::show_help_if_requested "$@" + +# --- Argument parsing (if the script takes options) --- +# TODO: parse "$@" here if needed. Use common::die for invalid arguments, +# not a raw echo+exit, so error formatting stays consistent. + +# --- Script logic --- + +common::log "Starting ..." + +# TODO: actual script logic goes here. + +common::log "Done."