Portable, Windows-friendly toolkit to back up and restore Digital Loggers (DLI) / OpenWrt-based Web Power Switch configs.
- Backup (CLI): Creates an OpenWrt config archive via
sysupgrade -band saves it underbackups\<IP>\<YYYY-MM-DD>\. - Restore (GUI): Uploads a saved archive to a replacement unit (defaults to
192.168.0.100), triggers restore/reboot, waits for it to come back online, and reports the final reachable IP.
- Install Python 3.11+ (64-bit).
From an elevated PowerShell:python -m pip install -U pip
2. **Clone and enter the repo**
```powershell
cd C:\Scripts
git clone https://github.com/scoggeshall/webpowerswitch-backup-restore.git WebPowerSwitch
cd C:\Scripts\WebPowerSwitch
```
3. **Install the package (adds `wps-backup` & `wps-restore` commands)**
```powershell
pip install -e .
```
> If `pip install -e .` fails, try `pip install .` (non-editable).
4. **Bootstrap safe config templates** (creates sample files under `data\` and updates `.gitignore`)
```powershell
# If you don't see this script in your repo, skip to step 5 and create files manually.
.\init-data-templates.ps1
```
5. **Edit your data files**
* `data\ip_list.csv` – which devices to back up:
```csv
ip,method,ssh_path,https_url,username
192.168.1.3,ssh,@auto,,admin
192.168.0.100,ssh,@auto,,admin
```
*Use `method=ssh` and `ssh_path=@auto` to generate/pull an OpenWrt archive automatically.*
* `data\ssh.json` – **create this from** `ssh.sample.json` (this real file is ignored by git):
```json
{
"key_path": "C:/Users/USERNAME/.ssh/id_ed25519",
"known_hosts": "C:/Users/USERNAME/.ssh/known_hosts",
"timeout": 10
}
```
**Generate a key if needed:**
```powershell
ssh-keygen -t ed25519 -f $env:USERPROFILE\.ssh\id_ed25519 -C "wps-backups"
```
6. **Trust host keys (strict host-key checking)**
For each device in your CSV, record its host key once:
```powershell
ssh -i $env:USERPROFILE\.ssh\id_ed25519 [email protected]
# Verify fingerprint carefully; type 'yes' to add to known_hosts, then exit.
```
7. **Run a backup**
```powershell
wps-backup
```
Check results & logs:
```powershell
Get-ChildItem .\backups\192.168.1.3 -Recurse
Get-Content .\logs\backup.log -Tail 100
```
8. **Restore to a replacement (GUI)**
```powershell
wps-restore
```
In the GUI:
* **Browse** to a saved archive, e.g. `backups\192.168.1.3\2025-08-10\config-backup-192.168.1.3.tar.gz`
* Target IP defaults to **`192.168.0.100`** (factory).
* Click **Restore** → the app uploads `/tmp/config-restore.tar.gz`, runs `sysupgrade -r`, predicts the post-restore IP from the archive’s `/etc/config/network`, waits for SSH to return, then shows **“Device back online at X — restore complete.”**
---
## What gets backed up (SSH mode)
* We run on-device: `sysupgrade -b /tmp/config-backup.tar.gz`
* Then SFTP pull the archive locally:
`backups\<IP>\<YYYY-MM-DD>\config-backup-<IP>.tar.gz`
* If `sysupgrade` is missing, we fall back to a minimal `tar` of key config paths:
* `/etc/config`, `/etc/dropbear`, `uhttpd cert/key`, `/etc/rc.local`, `/etc/sysupgrade.conf`
---
## Directory layout (what you’ll actually see)
```
C:\Scripts\WebPowerSwitch
├─ backups\ # backup archives (ignored by git)
├─ data\ # config for both tools
│ ├─ ip_list.csv
│ ├─ ssh.sample.json # tracked sample (safe)
│ ├─ https.sample.json # tracked sample (safe)
│ ├─ ssh.json # REAL secrets (ignored)
│ └─ https.json # REAL secrets (ignored, optional)
├─ logs\ # backup.log, restore.log (ignored by git)
├─ src\
│ ├─ wpsbackup\ # backup CLI package
│ └─ wpsrestore\ # restore GUI package
├─ init-data-templates.ps1 # creates safe samples in data\
├─ backup.ps1 / restore.ps1 # wrappers (optional)
├─ pyproject.toml # packaging / console scripts
├─ requirements.txt # convenience (pip install -r)
├─ README.md
└─ LICENSE
```
> **Note:** `backups\`, `logs\`, and real `data\*.json` are **excluded** from version control.
---
## Commands (cheat sheet)
**Install / update**
```powershell
python -m pip install -U pip
pip install -e . # or: pip install .
```
**Back up everything in `data\ip_list.csv`**
```powershell
wps-backup
```
**Open the restore GUI**
```powershell
wps-restore
```
**Run modules directly (alternative)**
```powershell
python -m wpsbackup --help
python -m wpsrestore
```
**Inspect logs**
```powershell
Get-Content .\logs\backup.log -Tail 100
Get-Content .\logs\restore.log -Tail 100
```
---
## Security defaults
* **SSH + ED25519** only (passwords discouraged).
* **Strict host-key checking**: backup CLI uses `RejectPolicy` and your `known_hosts`.
* GUI uses strict mode if `known_hosts` exists; otherwise first-run convenience onboarding for brand-new replacements.
* Real secrets (`data\ssh.json`, optional `data\https.json`) are **ignored by git**. Samples are tracked.
---
## HTTPS mode (optional)
If your model exposes an “Export” endpoint:
* Add a **pinned fingerprint** to `data\https.json` (use `https.sample.json` as a template).
* **Do not** store HTTP basic-auth passwords in files; supply at runtime or via a secure store.
Example `https.sample.json`:
```json
{
"timeout": 10,
"fingerprints": {
"192.168.1.10": "sha256/REPLACE_WITH_REAL_FINGERPRINT"
}
}
```
---
## Troubleshooting
* **“No such file”** during backup: ensure `ssh_path=@auto` (we’ll create/pull the standard OpenWrt archive).
* **Host key errors**: enroll host keys first:
```powershell
ssh -i $env:USERPROFILE\.ssh\id_ed25519 admin@<device-ip>
```
* **Device never comes back after restore**:
* Give it more time (some units take a while to reboot).
* Try the **predicted** IP shown in the GUI (from the restored config).
* Check cabling / PoE / VLAN / firewall.
---
## Why OpenWrt’s `sysupgrade -b`?
It’s the supported way to export configs on OpenWrt-class devices, capturing UCI configs (`/etc/config/*`) plus any files listed for backup. Restores are fast, reliable, and model-agnostic (no firmware flashing).
---
## Contributing (Phase-2+ roadmap)
* One-command EXEs for field techs via GitHub Actions + PyInstaller.
* CLI subcommands (`backup`, `restore`, `inspect`) with JSON logs & log rotation.
* Stronger host-key onboarding and “trust on first use” tooling.
* Tests on Windows; archive parsing & IP prediction fixtures.
```
::contentReference[oaicite:0]{index=0}
```