od-db-backup: clarify full backup = DB + images (app is reinstalled), capture config + versions
Per Open Dental docs a complete backup is the database + A-to-Z images; the program itself is not backed up but reinstalled at the matching version on restore. Extend the script to capture two recovery aids best-effort (outside the downtime window): a copy of FreeDentalConfig.xml and the exact Open Dental + MySQL/MariaDB versions, recorded in MANIFEST.txt. Runbook now spells out what is/isn't backed up and notes the config file holds the obfuscated DB password on direct-connect setups. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+16
-7
@@ -3,14 +3,19 @@
|
|||||||
**Applies to:** The Windows machine hosting the Open Dental MySQL/MariaDB database (the "server").
|
**Applies to:** The Windows machine hosting the Open Dental MySQL/MariaDB database (the "server").
|
||||||
**Goal:** A fully consistent, restorable backup of both halves of an Open Dental practice — the **database** and the **A-to-Z images folder** — following Open Dental and MySQL/MariaDB best practice, including cleanly **stopping the database during the copy and restarting it after**.
|
**Goal:** A fully consistent, restorable backup of both halves of an Open Dental practice — the **database** and the **A-to-Z images folder** — following Open Dental and MySQL/MariaDB best practice, including cleanly **stopping the database during the copy and restarting it after**.
|
||||||
|
|
||||||
A complete Open Dental backup is **two things**, and both must be captured together:
|
### What a complete Open Dental backup is (and isn't)
|
||||||
|
|
||||||
| Component | Default location | Holds |
|
Per Open Dental's own docs, a full backup is **two things — the database and the A-to-Z images folder** — and both must be captured together. A database backup **without** the images folder (or vice-versa) is not a usable restore.
|
||||||
|---|---|---|
|
|
||||||
| MySQL/MariaDB **data directory** | `C:\mysql\data\` (contains the `opendental` DB) | All clinical/financial data |
|
|
||||||
| **A-to-Z / OpenDentImages** folder | `C:\OpenDentImages\` | Scanned docs, images, attachments |
|
|
||||||
|
|
||||||
A database backup **without** the images folder (or vice-versa) is not a usable restore.
|
| Component | Default location | Back up? | Holds |
|
||||||
|
|---|---|---|---|
|
||||||
|
| MySQL/MariaDB **data directory** | `C:\mysql\data\` (contains the `opendental` DB) | **Yes — required** | All clinical/financial data |
|
||||||
|
| **A-to-Z / OpenDentImages** folder | `C:\OpenDentImages\` | **Yes — required** | Scanned docs, images, attachments |
|
||||||
|
| **`FreeDentalConfig.xml`** | Open Dental install dir | Nice-to-have | Connection / Choose-Database / AtoZ path config |
|
||||||
|
| Open Dental **program files** | `…\Program Files\Open Dental\` | **No — reinstall** | The application itself |
|
||||||
|
| Other office documents | (varies) | Per your policy | Non-Open-Dental files |
|
||||||
|
|
||||||
|
**There is no separate "application backup."** Open Dental does **not** back up the program itself — on restore you **reinstall the matching Open Dental version** (and matching MySQL/MariaDB) and point it at the restored data. So the two "recovery aids" worth grabbing are just: a copy of **`FreeDentalConfig.xml`** and a record of the **exact versions** to reinstall. The script captures both automatically into the backup folder (`FreeDentalConfig.xml` + `MANIFEST.txt`) — nothing extra to run.
|
||||||
|
|
||||||
**Placeholders:**
|
**Placeholders:**
|
||||||
|
|
||||||
@@ -61,7 +66,8 @@ It will:
|
|||||||
3. **Stop `<DB_SERVICE>` and verify it reached *Stopped*** — aborting the copy if it doesn't.
|
3. **Stop `<DB_SERVICE>` and verify it reached *Stopped*** — aborting the copy if it doesn't.
|
||||||
4. `robocopy` the **whole** data directory to `<DEST>\od-backup-<timestamp>\data`, then the images to `…\OpenDentImages`.
|
4. `robocopy` the **whole** data directory to `<DEST>\od-backup-<timestamp>\data`, then the images to `…\OpenDentImages`.
|
||||||
5. **Restart `<DB_SERVICE>`** in a `finally` block — this runs even if the copy fails or is interrupted.
|
5. **Restart `<DB_SERVICE>`** in a `finally` block — this runs even if the copy fails or is interrupted.
|
||||||
6. Write `MANIFEST.txt` and `backup.log` into the backup folder.
|
6. Capture recovery aids (best-effort, outside the downtime window): a copy of **`FreeDentalConfig.xml`** and the exact **Open Dental + MySQL/MariaDB versions**.
|
||||||
|
7. Write `MANIFEST.txt` and `backup.log` into the backup folder.
|
||||||
|
|
||||||
### Manual equivalent (offline / if you can't fetch the script)
|
### Manual equivalent (offline / if you can't fetch the script)
|
||||||
|
|
||||||
@@ -118,11 +124,14 @@ schtasks /Create /TN "OD Nightly Cold Backup" /TR "powershell -NoProfile -Execut
|
|||||||
|
|
||||||
Backup media and dumps hold the **entire practice's PHI**. Treat them as the crown jewels: encrypt the destination, lock down share permissions, and keep at least one copy **off-line/immutable** so ransomware can't encrypt your backups along with production. Don't store `<DB_PASSWORD>` in the scheduled command line — use a MySQL option file / limited account.
|
Backup media and dumps hold the **entire practice's PHI**. Treat them as the crown jewels: encrypt the destination, lock down share permissions, and keep at least one copy **off-line/immutable** so ransomware can't encrypt your backups along with production. Don't store `<DB_PASSWORD>` in the scheduled command line — use a MySQL option file / limited account.
|
||||||
|
|
||||||
|
Note that the captured `FreeDentalConfig.xml` stores the MySQL password **obfuscated but reversible** on **direct-connection** setups. The backup already contains all PHI so this doesn't change the requirement — but it's one more reason the destination must be encrypted and access-controlled. (Middle Tier setups don't put DB credentials in that file — see `od-cfg-persist.md`.)
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- Open Dental manual — Backups (overview): <https://opendental.com/manual/backups.html>
|
- Open Dental manual — Backups (overview): <https://opendental.com/manual/backups.html>
|
||||||
- Open Dental manual — Manual Backups: <https://www.opendental.com/manual/backupsmanual.html>
|
- Open Dental manual — Manual Backups: <https://www.opendental.com/manual/backupsmanual.html>
|
||||||
- Open Dental manual — Backup Tool: <https://www.opendental.com/manual/backuptool.html>
|
- Open Dental manual — Backup Tool: <https://www.opendental.com/manual/backuptool.html>
|
||||||
|
- Open Dental manual — FreeDentalConfig.xml: <https://www.opendental.com/manual/freedentalconfig.html>
|
||||||
- Open Dental — InnoDB (backup implications): <https://www.opendental.com/site/mysqlinnodb.html>
|
- Open Dental — InnoDB (backup implications): <https://www.opendental.com/site/mysqlinnodb.html>
|
||||||
- Open Dental manual — MySQL Data Directory Management: <https://opendental.com/manual/mysqlmanage.html>
|
- Open Dental manual — MySQL Data Directory Management: <https://opendental.com/manual/mysqlmanage.html>
|
||||||
- MySQL — `mysqldump`, `--single-transaction`: <https://dev.mysql.com/doc/refman/en/mysqldump.html>
|
- MySQL — `mysqldump`, `--single-transaction`: <https://dev.mysql.com/doc/refman/en/mysqldump.html>
|
||||||
|
|||||||
@@ -206,21 +206,53 @@ finally {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 9. Write a manifest and report ---
|
# --- 9. Capture recovery aids: FreeDentalConfig.xml + version stamps (best-effort) ---
|
||||||
|
# The Open Dental program is NOT backed up (reinstall the matching version on
|
||||||
|
# restore). But grabbing the connection config and recording the exact versions
|
||||||
|
# makes a restore fast and version-correct. Best-effort: never fails the backup.
|
||||||
|
if ($ok) {
|
||||||
|
$odInstall = @(${env:ProgramFiles(x86)}, $env:ProgramFiles) | Where-Object { $_ } |
|
||||||
|
ForEach-Object { Join-Path $_ 'Open Dental' } |
|
||||||
|
Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||||
|
$odVersion = 'unknown'
|
||||||
|
if ($odInstall) {
|
||||||
|
$cfg = Join-Path $odInstall 'FreeDentalConfig.xml'
|
||||||
|
if (Test-Path $cfg) {
|
||||||
|
try { Copy-Item -LiteralPath $cfg -Destination (Join-Path $backupDir 'FreeDentalConfig.xml') -Force
|
||||||
|
Log 'Captured FreeDentalConfig.xml (connection/AtoZ config).' }
|
||||||
|
catch { Log "Could not copy FreeDentalConfig.xml: $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
$exe = Join-Path $odInstall 'OpenDental.exe'
|
||||||
|
if (Test-Path $exe) { try { $odVersion = (Get-Item $exe).VersionInfo.FileVersion } catch {} }
|
||||||
|
}
|
||||||
|
# DB engine version from the service's backing executable
|
||||||
|
$dbVersion = 'unknown'
|
||||||
|
try {
|
||||||
|
$imgPath = (Get-CimInstance Win32_Service -Filter "Name='$svcName'" -ErrorAction Stop).PathName
|
||||||
|
$exePath = ([regex]::Match($imgPath, '([A-Za-z]:\\[^"]*?\.exe)')).Groups[1].Value
|
||||||
|
if ($exePath -and (Test-Path $exePath)) { $dbVersion = (Get-Item $exePath).VersionInfo.ProductVersion }
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 10. Write a manifest and report ---
|
||||||
if ($ok) {
|
if ($ok) {
|
||||||
$manifest = @"
|
$manifest = @"
|
||||||
Open Dental cold backup
|
Open Dental cold backup
|
||||||
Created : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
|
Created : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
|
||||||
Host : $env:COMPUTERNAME
|
Host : $env:COMPUTERNAME
|
||||||
Service : $svcName
|
Service : $svcName
|
||||||
Data directory : $dataDir (~$dataGB GB)
|
Open Dental ver : $odVersion (reinstall THIS version to restore)
|
||||||
Images folder : $(if($imgDir){"$imgDir (~$imgGB GB)"}else{'(skipped)'})
|
DB engine ver : $dbVersion (restore onto a matching MySQL/MariaDB)
|
||||||
Backup folder : $backupDir
|
Data directory : $dataDir (~$dataGB GB)
|
||||||
Method : cold copy (service stopped + verified), whole data dir incl InnoDB shared tablespace/logs
|
Images folder : $(if($imgDir){"$imgDir (~$imgGB GB)"}else{'(skipped)'})
|
||||||
|
FreeDentalConfig : $(if($odInstall -and (Test-Path (Join-Path $backupDir 'FreeDentalConfig.xml'))){'captured'}else{'(not found)'})
|
||||||
|
Backup folder : $backupDir
|
||||||
|
Method : cold copy (service stopped + verified), whole data dir incl InnoDB shared tablespace/logs
|
||||||
"@
|
"@
|
||||||
Set-Content -LiteralPath (Join-Path $backupDir 'MANIFEST.txt') -Value $manifest
|
Set-Content -LiteralPath (Join-Path $backupDir 'MANIFEST.txt') -Value $manifest
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Host "Backup complete: $backupDir" -ForegroundColor Green
|
Write-Host "Backup complete: $backupDir" -ForegroundColor Green
|
||||||
|
Write-Host "Open Dental $odVersion / DB $dbVersion — reinstall matching versions to restore." -ForegroundColor Green
|
||||||
Write-Host 'Next: copy this folder OFF-SITE, and TEST-RESTORE it periodically on an isolated machine.' -ForegroundColor Green
|
Write-Host 'Next: copy this folder OFF-SITE, and TEST-RESTORE it periodically on an isolated machine.' -ForegroundColor Green
|
||||||
Write-Host 'Never restore over a live production database.' -ForegroundColor Yellow
|
Write-Host 'Never restore over a live production database.' -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user