# gniza cPanel Backup, Restore & Disaster Recovery tool. Uses `pkgacct --nocompress --skiphomedir` for account backups, gzips SQL files individually, and transfers everything (including homedirs) to remote destinations with incremental snapshots. Supports three remote types: **SSH** (rsync with hardlink-based `--link-dest`), **Amazon S3** / S3-compatible (via rclone), and **Google Drive** (via rclone). Supports multiple remote destinations with independent schedules and retention policies. ## Repository | | URL | |---|-----| | **Git (SSH)** | `gitea:shukivaknin/gniza.git` (uses `Host gitea` from `~/.ssh/config`) | | **Git (HTTPS)** | `https://git.linux-hosting.co.il/shukivaknin/gniza.git` | | **Web UI** | https://git.linux-hosting.co.il/shukivaknin/gniza/ | ## Installation From a clone: ```bash git clone https://git.linux-hosting.co.il/shukivaknin/gniza.git cd gniza sudo bash scripts/install.sh ``` To uninstall: ```bash sudo bash /usr/local/gniza/uninstall.sh # from installed copy # or sudo bash scripts/uninstall.sh # from repo clone ``` The uninstall script removes the CLI, symlink, cron entries, and WHM plugin. Config (`/etc/gniza/`) and logs (`/var/log/gniza/`) are preserved — remove manually if desired. ## Quick Start ```bash # Configure via WHM → GNIZA Backup Manager (setup wizard) # Or copy example configs manually: sudo cp /etc/gniza/gniza.conf.example /etc/gniza/gniza.conf sudo cp /etc/gniza/remote.conf.example /etc/gniza/remotes.d/nas.conf # Test backup (dry run) sudo gniza backup --dry-run # Run backup sudo gniza backup # Back up to specific remotes sudo gniza backup --remote=nas,offsite ``` ## Commands ``` gniza backup [--account=NAME] [--remote=NAME[,NAME2]] [--skip-suspended] [--dry-run] gniza restore account --remote=NAME [--timestamp=TS] [--force] gniza restore files --remote=NAME [--path=subpath] [--timestamp=TS] gniza restore database --remote=NAME [--timestamp=TS] gniza restore mailbox --remote=NAME [--timestamp=TS] gniza restore server --remote=NAME [--timestamp=TS] gniza list [--account=NAME] [--remote=NAME] gniza verify [--account=NAME] [--remote=NAME] gniza status gniza remote list gniza remote delete gniza schedule add gniza schedule delete gniza schedule list gniza schedule install gniza schedule show gniza schedule remove gniza version gniza help ``` ### Global Options | Option | Description | |--------|-------------| | `--config=PATH` | Alternate config file (default: `/etc/gniza/gniza.conf`) | | `--remote=NAME[,NAME2]` | Target specific remote(s) from `/etc/gniza/remotes.d/` (comma-separated) | | `--debug` | Enable debug logging | ## Configuration ### Main Config **File:** `/etc/gniza/gniza.conf` Controls local settings (accounts, logging, notifications). Remote destinations are configured in `/etc/gniza/remotes.d/`. ```bash # Local Settings TEMP_DIR="/usr/local/gniza/workdir" # Working dir for pkgacct output INCLUDE_ACCOUNTS="" # Comma-separated, empty = all EXCLUDE_ACCOUNTS="nobody" # Comma-separated exclusions # Logging LOG_DIR="/var/log/gniza" LOG_LEVEL="info" # debug, info, warn, error LOG_RETAIN=90 # Days to keep log files # Notifications NOTIFY_EMAIL="" # Email for notifications NOTIFY_ON="failure" # always, failure, never # Advanced LOCK_FILE="/var/run/gniza.lock" SSH_TIMEOUT=30 SSH_RETRIES=3 RSYNC_EXTRA_OPTS="" ``` See `etc/gniza.conf.example` for the full template. ### Remote Destinations Back up to one or more destinations with independent retention policies and bandwidth limits. Supports SSH, Amazon S3 (and S3-compatible services like MinIO, Wasabi, Backblaze B2), and Google Drive. Remote destinations are configured as individual files in `/etc/gniza/remotes.d/`. #### Setup ```bash # Configure via WHM → Remotes, or copy the template manually sudo cp /etc/gniza/remote.conf.example /etc/gniza/remotes.d/nas.conf sudo vi /etc/gniza/remotes.d/nas.conf # List configured remotes sudo gniza remote list # Delete a remote sudo gniza remote delete nas ``` #### Remote Config Format Each file in `/etc/gniza/remotes.d/.conf`: ```bash # Remote type: "ssh" (default), "s3", or "gdrive" REMOTE_TYPE="ssh" # ── SSH Remote ────────────────────────────────── REMOTE_HOST="192.168.1.100" # Required (SSH only) REMOTE_PORT=22 REMOTE_USER="root" REMOTE_AUTH_METHOD="key" # "key" or "password" REMOTE_KEY="/root/.ssh/id_rsa" # Required for key auth REMOTE_PASSWORD="" # Required for password auth (needs sshpass) # ── S3 Remote ─────────────────────────────────── S3_ACCESS_KEY_ID="" # Required (S3 only) S3_SECRET_ACCESS_KEY="" # Required (S3 only) S3_REGION="us-east-1" S3_ENDPOINT="" # For S3-compatible services S3_BUCKET="" # Required (S3 only) # ── Google Drive Remote ───────────────────────── GDRIVE_SERVICE_ACCOUNT_FILE="" # Required (GDrive only) GDRIVE_ROOT_FOLDER_ID="" # Optional # ── Common ────────────────────────────────────── REMOTE_BASE="/backups" BWLIMIT=0 RETENTION_COUNT=30 RSYNC_EXTRA_OPTS="" # SSH only ``` S3 and Google Drive remotes require `rclone` installed on the server. See `etc/remote.conf.example` for the full template. #### Usage Without `--remote`, backup/list/verify operate on **all** configured remotes. Restore always requires `--remote` to specify the source. ```bash # Back up to all remotes sudo gniza backup # Back up to specific remote(s) sudo gniza backup --remote=nas sudo gniza backup --remote=nas,offsite # List snapshots on a specific remote sudo gniza list --remote=offsite # Restore requires explicit remote sudo gniza restore account johndoe --remote=nas ``` ### Schedules Schedules are **decoupled from remotes**. Each schedule lives in `/etc/gniza/schedules.d/.conf` and defines when backups run and which remotes to target. This allows multiple schedules targeting different sets of remotes. #### Schedule Config Format ```bash SCHEDULE="daily" # hourly, daily, weekly, monthly, custom SCHEDULE_TIME="02:00" # HH:MM (24-hour) SCHEDULE_DAY="" # hours between backups (1-23) for hourly # day-of-week (0=Sun..6=Sat) for weekly # day-of-month (1-28) for monthly SCHEDULE_CRON="" # Full cron expression for SCHEDULE=custom REMOTES="" # Comma-separated remote names (empty = all) SYSBACKUP="" # "yes" to include system backup SKIP_SUSPENDED="" # "yes" to skip cPanel suspended accounts ``` #### Managing Schedules ```bash # Interactive schedule creation sudo gniza schedule add nightly # List configured schedules sudo gniza schedule list # Delete a schedule sudo gniza schedule delete nightly # Install all schedules to crontab sudo gniza schedule install # Show current gniza cron entries sudo gniza schedule show # Remove all gniza cron entries sudo gniza schedule remove ``` #### Schedule Types | Type | SCHEDULE_DAY | Cron Pattern | Example | |------|-------------|--------------|---------| | `hourly` | Hours interval (1-23) | ` */ * * *` | Every 2 hours at :15 → `15 */2 * * *` | | `daily` | — | ` * * *` | Daily at 02:00 → `0 2 * * *` | | `weekly` | Day-of-week (0-6) | ` * * ` | Sundays at 03:00 → `0 3 * * 0` | | `monthly` | Day-of-month (1-28) | ` * *` | 1st at 02:00 → `0 2 1 * *` | | `custom` | — | `SCHEDULE_CRON` (5-field) | `*/30 * * * *` | Each schedule gets a tagged cron entry for clean install/remove: ``` # gniza:nightly 0 2 * * * /usr/local/bin/gniza backup --remote=nas,offsite >> /var/log/gniza/cron-nightly.log 2>&1 ``` ## Remote Directory Structure ### SSH Remotes ``` $REMOTE_BASE//accounts// ├── snapshots/ │ ├── 2026-03-03T020000/ # Completed snapshot │ │ ├── mysql/ # SQL dumps (*.sql.gz) │ │ ├── mysql.sql # Database grants │ │ ├── cp/ # cPanel metadata │ │ ├── ... # Other pkgacct files │ │ └── homedir/ # Full home directory │ ├── 2026-03-02T020000/ # Previous (hardlinked unchanged files) │ └── 2026-03-01T020000.partial/ # Incomplete (failed/in-progress) └── latest -> snapshots/2026-03-03T020000 ``` ### Cloud Remotes (S3/GDrive) ``` $REMOTE_BASE//accounts//snapshots/ ├── 2026-03-03T020000/ # Completed snapshot │ ├── .complete # Completion marker (empty file) │ ├── mysql/ # SQL dumps (*.sql.gz) │ ├── ... # Other pkgacct files │ └── homedir/ # Full home directory ├── 2026-03-02T020000/ # Previous snapshot │ └── .complete ├── 2026-03-01T020000/ # Partial (no .complete → purged on next run) └── latest.txt # Contains timestamp of newest snapshot ``` Cloud storage has no atomic rename or symlinks, so `.complete` markers and `latest.txt` replace the `.partial` suffix and `latest` symlink used by SSH remotes. pkgacct output is stored directly in the snapshot root (no wrapper subdirectory). The `homedir/` sits alongside it. ## Ownership & Permissions All rsync operations use `--rsync-path="rsync --fake-super"` to preserve file ownership and permissions even when the remote SSH user is not root. The real uid/gid/permissions are stored as extended attributes (`user.rsync.%stat`) on the remote filesystem. On restore, the same flag reads the xattrs back, allowing the local root process to set the correct ownership. ## Backup Flow 1. Load main config (TEMP_DIR, LOG_DIR, accounts, notifications) 2. Resolve target remotes (`--remote=NAME[,NAME2]` or all from `remotes.d/`) 3. Test connectivity to all targets upfront (SSH or rclone by type) 4. For each account: - `pkgacct` ONCE - Gzip SQL ONCE - For each remote: - `load_remote(name)` — swaps REMOTE_*/S3_*/GDRIVE_* globals - Clean partials, get previous snapshot - Transfer pkgacct content (rsync for SSH, rclone for cloud) - Transfer homedir (rsync for SSH, rclone for cloud) - Finalize snapshot (SSH: rename `.partial` + symlink; cloud: `.complete` marker + `latest.txt`) - Enforce retention - Cleanup local temp 5. Summary + notification If one remote fails for an account, other remotes still receive the backup. ## Restore Workflows | Workflow | Command | Description | |----------|---------|-------------| | **Full account** | `restore account ` | Downloads pkgacct data, decompresses SQL, runs `/scripts/restorepkg`, rsyncs homedir, fixes ownership | | **Selective files** | `restore files --path=...` | Rsyncs specific path from remote homedir backup | | **Single database** | `restore database ` | Downloads SQL dump + grants, imports via `mysql` | | **Mailbox** | `restore mailbox ` | Restores individual email account from `mail///` | | **Full server** | `restore server` | Restores all accounts found on the remote | All restore commands require `--remote=NAME` to specify the source. ## Error Handling - Single account failures don't abort the run - Exit codes: `0` success, `1` fatal, `2` locked, `5` partial failure - `.partial` directories mark incomplete snapshots - rsync retries with exponential backoff (configurable via `SSH_RETRIES`) - `flock`-based concurrency control prevents parallel runs - In multi-remote mode, failure on one remote doesn't block others ## File Layout ``` /usr/local/gniza/ # Install directory ├── bin/gniza # CLI entrypoint ├── lib/ # Shell libraries │ ├── constants.sh # Version, exit codes, colors, defaults │ ├── utils.sh # die(), require_root(), timestamp, human_* │ ├── logging.sh # Per-run log files, log_info/warn/error/debug │ ├── config.sh # Config loading and validation │ ├── locking.sh # flock-based concurrency control │ ├── ssh.sh # SSH connectivity, remote_exec, rsync SSH cmd │ ├── rclone.sh # Rclone transport layer for S3/GDrive │ ├── accounts.sh # cPanel account discovery and filtering │ ├── pkgacct.sh # pkgacct execution, SQL gzip, temp cleanup │ ├── snapshot.sh # Timestamp naming, list/resolve snapshots │ ├── transfer.sh # rsync --link-dest transfers, finalize │ ├── retention.sh # Prune old snapshots beyond RETENTION_COUNT │ ├── verify.sh # Remote backup integrity checks │ ├── notify.sh # Email notifications │ ├── restore.sh # Full account, files, database, mailbox, server restore │ ├── remotes.sh # Remote discovery and context switching │ └── schedule.sh # Cron management for decoupled schedules └── etc/ ├── gniza.conf.example # Main config template ├── remote.conf.example # Remote destination template └── schedule.conf.example # Schedule template /etc/gniza/ # Runtime configuration ├── gniza.conf # Main config ├── remotes.d/ # Remote destination configs │ ├── nas.conf │ └── offsite.conf └── schedules.d/ # Schedule configs ├── nightly.conf └── weekly-offsite.conf /var/log/gniza/ # Log files ├── gniza-20260303-020000.log # Per-run logs ├── cron-nightly.log # Per-schedule cron output └── cron-weekly-offsite.log ``` ## WHM Plugin gniza includes a WHM plugin for managing backups through the cPanel/WHM web interface. All pages use **Tailwind CSS v4** with **DaisyUI v5** for styling. ### Installation The plugin is installed automatically by `scripts/install.sh`. It registers with WHM at **Plugins > gniza Backup Manager**. Plugin files are deployed to `/usr/local/cpanel/whostmgr/docroot/cgi/gniza-whm/`. ### Setup Wizard When gniza is not yet configured (no remotes in `/etc/gniza/remotes.d/`), the dashboard automatically redirects to a **3-step setup wizard**: 1. **SSH Key** — Detects existing keys in `/root/.ssh/` (`id_ed25519`, `id_rsa`, `id_ecdsa`, `id_dsa`). Lets you select one or enter a custom path. Shows `ssh-keygen` and `ssh-copy-id` commands for creating new keys. 2. **Remote Destination** — Configure the first remote: name, type (SSH/S3/GDrive), connection details, base path, bandwidth limit, and retention count. Tests the connection before saving. Creates a config file in `/etc/gniza/remotes.d/`. 3. **Schedule** — Optionally set a backup schedule (hourly/daily/weekly/monthly/custom) for the new remote. Installs the cron entry automatically. Can be skipped. The wizard is also accessible anytime from the dashboard quick links ("Run Setup Wizard"). ### Pages | Page | URL | Description | |------|-----|-------------| | Dashboard | `index.cgi` | Overview, remote listing, cron status, quick links | | Remotes | `remotes.cgi` | Add/edit/delete remote destinations (SSH/S3/GDrive) with connection testing | | Schedules | `schedules.cgi` | Add/edit/delete schedules, per-schedule cron toggle | | Restore | `restore.cgi` | Restore workflow: select account, remote, snapshot, then restore type (full/files/database/mailbox) | | Settings | `settings.cgi` | Edit main config (`/etc/gniza/gniza.conf`) | | Setup Wizard | `setup.cgi` | Guided initial configuration (3 steps) | ### Plugin File Layout ``` whm/ ├── gniza-whm.conf # WHM AppConfig registration └── gniza-whm/ ├── index.cgi # Dashboard ├── setup.cgi # Setup wizard (3 steps) ├── settings.cgi # Main config editor ├── remotes.cgi # Remote CRUD (SSH/S3/GDrive) ├── schedules.cgi # Schedule CRUD + cron toggles ├── restore.cgi # Restore workflow (account → remote → snapshot → type) ├── assets/ │ ├── gniza-whm.css # Built Tailwind/DaisyUI CSS (committed) │ └── src/ │ ├── input.css # Tailwind v4 entry point │ ├── safelist.html # Class safelist for Tailwind scanner │ └── package.json # Build toolchain └── lib/GnizaWHM/ ├── Config.pm # Config parser/writer (pure Perl) ├── Validator.pm # Input validation ├── Cron.pm # Cron read + per-schedule install/remove ├── Runner.pm # Pattern-based safe CLI command runner └── UI.pm # Navigation, flash, CSRF, HTML helpers, CSS delivery ``` ## Running Tests ```bash bash tests/test_utils.sh ``` ## License MIT