# 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)** | `ssh://git@192.168.100.100:2222/shukivaknin/gniza.git` | | **Git (HTTP)** | `http://192.168.100.100:3001/shukivaknin/gniza.git` | | **Web UI** | http://192.168.100.100:3001/shukivaknin/gniza | ## Installation ```bash curl -sSL http://192.168.100.100:3001/shukivaknin/gniza/raw/branch/main/scripts/install.sh | sudo bash ``` Or from a local clone: ```bash git clone ssh://git@192.168.100.100:2222/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 # Interactive setup (creates config + first remote + optional schedule) sudo gniza init # Add additional remote destinations sudo gniza init remote offsite # 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 init gniza init remote 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 # Interactive setup (recommended) sudo gniza init remote nas sudo gniza init remote offsite # 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