# 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. ## Quick Start ```bash # Install sudo bash scripts/install.sh # Interactive setup (creates config + first remote) 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 # Install cron schedules for all remotes sudo gniza schedule install ``` ## Commands ``` gniza backup [--account=NAME] [--remote=NAME] [--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 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 {install|show|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` | Target a specific remote from `/etc/gniza/remotes.d/` | | `--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 schedules, 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 requires `--remote` to specify the source. ```bash # Back up to all remotes sudo gniza backup # Back up to a specific remote sudo gniza backup --remote=nas # List snapshots on a specific remote sudo gniza list --remote=offsite # Restore requires explicit remote sudo gniza restore account johndoe --remote=nas ``` ## Schedule Management Manage cron entries based on each remote's `SCHEDULE` settings. Each remote gets a tagged cron entry for clean install/remove. ```bash sudo gniza schedule install # Install cron entries for all remotes sudo gniza schedule show # Show current gniza cron entries sudo gniza schedule remove # Remove all gniza cron entries ``` Example crontab entries: ``` # gniza:nas 0 2 * * * /usr/local/bin/gniza backup --remote=nas >> /var/log/gniza/cron-nas.log 2>&1 # gniza:offsite 0 3 * * 0 /usr/local/bin/gniza backup --remote=offsite >> /var/log/gniza/cron-offsite.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` 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 | Description | |----------|-------------| | **Full account** | Downloads pkgacct data, decompresses SQL, runs `/scripts/restorepkg`, rsyncs homedir, fixes ownership | | **Selective files** | Rsyncs specific path from remote homedir backup | | **Single database** | Downloads SQL dump + grants, imports via `mysql` | | **Full server rebuild** | 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 │ ├── 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, server restore │ ├── remotes.sh # Remote discovery and context switching │ └── schedule.sh # Cron management for multi-remote schedules └── etc/ ├── gniza.conf.example # Main config template └── remote.conf.example # Remote destination template /etc/gniza/ # Runtime configuration ├── gniza.conf # Main config └── remotes.d/ # Remote destination configs ├── nas.conf └── offsite.conf /var/log/gniza/ # Log files ├── gniza-20260303-020000.log # Per-run logs ├── cron-nas.log # Per-remote cron output └── cron-offsite.log ``` ## WHM Plugin gniza includes a WHM plugin for managing backups through the cPanel/WHM web interface. ### 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, host, port, user, SSH key (pre-filled from step 1), base path, bandwidth limit, and retention count. 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 | | Settings | `settings.cgi` | Edit main config (`/etc/gniza/gniza.conf`) | | Remotes | `remotes.cgi` | Add/edit/delete remote destinations | | Schedules | `schedules.cgi` | View and manage cron schedules | | Setup Wizard | `setup.cgi` | Guided initial configuration | ### SSH Key Guidance When adding a new remote (via Remotes > Add Remote), an SSH key guidance block is displayed above the form showing: - Detected existing keys on the server - Commands to generate a new key and copy it to the remote ### 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 ├── schedules.cgi # Cron management ├── assets/ │ └── gniza-whm.css # Shared styles └── lib/GnizaWHM/ ├── Config.pm # Config parser/writer (pure Perl) ├── Validator.pm # Input validation ├── Cron.pm # Cron read + allowlisted gniza commands └── UI.pm # Navigation, flash, CSRF, HTML helpers ``` ## Production Server gniza is deployed on the following server: | Server | Host | SSH Port | |--------|------|----------| | Production (cPanel) | `192.168.100.13` | `2223` | Hostname: `181-79-81-251.cprapid.com` ## Running Tests ```bash bash tests/test_utils.sh ``` ## License MIT