main was 16 days and 259 commits behind the line the product is actually built on, at contract 0.30.0 against 0.35.0, with no per-PC credential code at all. It was neither current nor curated - just stale. This brings it forward and, at the same time, makes it mean something: main is what a receiving site installs, and nothing else. WHAT IT CARRIES. Twelve plugins - backups, computers, endpoints, guides, knowledgebase, machines, network, notifications, printers, slides, tools, warranty - plus applications, which is manifest-less core and always ships. The only manifest dependency inside that set is endpoints on computers, and both are in it, so the closure is self-contained. WHAT IT DOES NOT. employees, geenforce, measuringtools, printedparts and usb stay on the site line. So do the Windows installer tree, the VM rehearsal helpers, the agent tooling config, one site's data loader, the internal audit and findings records, and the planner spreadsheet. 1,135 tracked files against 1,438. A REMOVAL THAT BREAKS THE APP IS NOT A REMOVAL, and two nearly shipped: deploy/site-profile-universal.json still named all seventeen plugins. It is the file docs/DEPLOY.md tells a site to provision from, so a fresh install would have failed at `flask plugin apply-profile` with PluginError - the first thing a receiving site does, broken by the branch meant to serve them. It now names the twelve that exist, and its comment says the two must agree. agent/ was never refreshed. Whoever populated this branch copied shopdb/, plugins/, frontend/ and tests/ from the site line and left the agent tree at its September state: 22 C# and PowerShell sources missing, including AgentCredentials, EnrollmentService, AgentHttpFactory and the fleet-alert client, plus one file the site line had deleted still present. The old tree is internally consistent, so it compiles, and nothing in CI builds it - every gate was green over a server that speaks enrollment and per-PC credentials to a bundled agent source that cannot. Resynced whole. DECISIONS THIS ENCODES, rather than defers: - The plugin-authoring lab builds printedparts, whose manifest depends on employees, so it could not stay. The lab remains on the site line and main ships no build-along tutorial. That is a real loss for an adopting developer and is recorded here rather than discovered later. - Recognition, Training and Recertification notifications are no longer seeded. Each splits one notification into a card per employee and renders a photo, which needs a plugin this branch does not bundle. Seeding them would offer an author a type that cannot resolve anybody. A site that installs employees gets them from that plugin's own seed. - Windows/IIS is documented as the legacy path throughout, and the README leads with Docker, because that is what a receiving site gets. The API description is regenerated for THIS branch - 347 paths and 483 operations, down from 415 and 573 - so the published contract describes what is here rather than what the site line serves. docs/CONFIG.md lost the two measuring-tool label keys the code no longer declares, which two lanes had decided opposite ways. Suite: 3124 passed, 6 skipped. Naming and style gates pass. The docs gates run rather than skip, and every index line count is derived from the file it names.
172 lines
6.3 KiB
Bash
Executable File
172 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ShopDB backup one-shot.
|
|
#
|
|
# Produces one recoverable set per run and stores it two ways: a plain
|
|
# db.sql.gz + manifest.json on the local /backups volume for a fast, repo-free
|
|
# restore, and a restic snapshot (db dump + instance tree + manifest) at
|
|
# RESTIC_REPOSITORY for a deduped, encrypted, offsite copy.
|
|
#
|
|
# Sequence:
|
|
# 1. mysqldump --single-transaction (InnoDB-consistent, non-blocking)
|
|
# 2. manifest.py stamps core+plugin Alembic revisions + image tag
|
|
# 3. restic backup the dump + /app/instance + manifest as ONE snapshot
|
|
# 4. restic forget --keep-daily 5 --keep-weekly 4 --keep-monthly 6 --prune
|
|
# 5. restic check - fail loudly if the repo is unhealthy, so a bad repo is
|
|
# caught on backup night, not restore night.
|
|
#
|
|
# ASCII only, no emojis - matches the repo style gate.
|
|
set -euo pipefail
|
|
|
|
BACKUPS_ROOT="${SHOPDB_BACKUPS_ROOT:-/backups}"
|
|
SITE="${SHOPDB_SITE:-shopdb}"
|
|
STAMP="$(date -u +%Y-%m-%dT%H%M%SZ)"
|
|
SETNAME="${SITE}-${STAMP}"
|
|
SETDIR="${BACKUPS_ROOT}/${SETNAME}"
|
|
|
|
# shellcheck source=scripts/backup/status.sh
|
|
. /opt/backup/status.sh
|
|
|
|
log() { echo "[backup] $*"; }
|
|
|
|
# Record the run outcome on any exit so a failed backup is visible in the UI
|
|
# history, not just a silent gap.
|
|
BACKUP_DONE=false
|
|
_on_exit() {
|
|
code=$?
|
|
if [ "${BACKUP_DONE}" = true ]; then
|
|
status_write backup done "${SETNAME}" "Backup complete"
|
|
history_append backup success "${SETNAME}" "Backup complete"
|
|
else
|
|
status_write backup failed "${SETNAME}" "Backup failed (exit ${code})"
|
|
history_append backup failed "${SETNAME}" "Backup failed (exit ${code})"
|
|
fi
|
|
}
|
|
trap _on_exit EXIT
|
|
|
|
mkdir -p "${SETDIR}"
|
|
status_write backup database "${SETNAME}" "Dumping database"
|
|
|
|
# 1. Database: whole-DB logical dump, consistent and non-blocking on InnoDB.
|
|
log "dumping database ${MYSQL_DATABASE} from ${MYSQL_HOST}"
|
|
mysqldump \
|
|
--host="${MYSQL_HOST}" \
|
|
--user="${MYSQL_USER}" \
|
|
--password="${MYSQL_PASSWORD}" \
|
|
--single-transaction \
|
|
--routines \
|
|
--triggers \
|
|
--databases "${MYSQL_DATABASE}" \
|
|
| gzip -c > "${SETDIR}/db.sql.gz"
|
|
log "database dump written ($(du -h "${SETDIR}/db.sql.gz" | cut -f1))"
|
|
|
|
# 2. Manifest: the schema stamp that makes a restore a checked operation.
|
|
# Capture the revision actually applied to the DATABASE (its alembic_version),
|
|
# not just the code head. A restore must gate on the DATA revision; stamping
|
|
# only the code head hides an image that has outrun its own database and lets a
|
|
# behind-schema dump restore under ahead-schema code (1054 Unknown column on
|
|
# every affected table). Query it straight from the dumped DB.
|
|
log "reading applied database revision (alembic_version)"
|
|
DATAREV="$(mysql \
|
|
--host="${MYSQL_HOST}" \
|
|
--user="${MYSQL_USER}" \
|
|
--password="${MYSQL_PASSWORD}" \
|
|
--skip-column-names --batch \
|
|
-e "SELECT version_num FROM alembic_version LIMIT 1;" \
|
|
"${MYSQL_DATABASE}" 2>/dev/null | tr -d '\r')"
|
|
[ -n "${DATAREV}" ] || log "WARN could not read alembic_version - datarev will be empty"
|
|
|
|
log "writing manifest"
|
|
# Manifest generation requires Python. If not available (offline build), create
|
|
# a minimal manifest with just the essentials for restore identification.
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
python3 /opt/backup/manifest.py \
|
|
--out "${SETDIR}/manifest.json" \
|
|
--takenat "${STAMP}" \
|
|
--site "${SITE}" \
|
|
--imagetag "${SHOPDB_IMAGE_TAG:-source}" \
|
|
--datarev "${DATAREV}" \
|
|
--migrations /app/migrations \
|
|
--plugins /app/plugins
|
|
|
|
# Consistency guard: if the DATA revision does not match the CODE head, this
|
|
# backup captures a database the running image has outrun. It is still stored
|
|
# (losing it would be worse) but flagged loudly so a restore of it is a
|
|
# deliberate choice, not a surprise.
|
|
CODEREV="$(python3 -c \
|
|
'import json,sys;print(",".join(sorted(json.load(open(sys.argv[1]))["schemarev"]["core"])))' \
|
|
"${SETDIR}/manifest.json" 2>/dev/null || echo "")"
|
|
if [ -n "${DATAREV}" ] && [ "${DATAREV}" != "${CODEREV}" ]; then
|
|
log "WARN schema drift: database is at '${DATAREV}' but code head is '${CODEREV}'"
|
|
log "WARN this DB has not run 'flask db upgrade' to the running image; restore will upgrade it"
|
|
history_append backup warning "${SETNAME}" "Schema drift: data ${DATAREV} vs code ${CODEREV}"
|
|
fi
|
|
else
|
|
# No Python available - create minimal manifest with bash
|
|
log "Python not available - creating minimal manifest"
|
|
cat > "${SETDIR}/manifest.json" <<-EOF
|
|
{
|
|
"takenat": "${STAMP}",
|
|
"site": "${SITE}",
|
|
"imagetag": "${SHOPDB_IMAGE_TAG:-source}",
|
|
"datarev": "${DATAREV}",
|
|
"note": "Minimal manifest - Python not available in offline build"
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
# 3. restic snapshot: db dump + instance tree + manifest, one point in time.
|
|
# Skips gracefully when restic/repo are not configured (local-only sites).
|
|
IS_CHECK_PASSED=false
|
|
status_write backup snapshot "${SETNAME}" "Snapshotting to backup store"
|
|
if command -v restic >/dev/null 2>&1 && [ -n "${RESTIC_REPOSITORY:-}" ]; then
|
|
if ! restic snapshots >/dev/null 2>&1; then
|
|
log "initializing restic repository at ${RESTIC_REPOSITORY}"
|
|
restic init
|
|
fi
|
|
log "restic backup (db dump + instance tree + manifest)"
|
|
restic backup \
|
|
--tag "${SITE}" \
|
|
--tag "${SETNAME}" \
|
|
"${SETDIR}/db.sql.gz" \
|
|
"${SETDIR}/manifest.json" \
|
|
/app/instance
|
|
|
|
# 4. Retention: any day in the last 5, plus weekly/monthly tail.
|
|
log "pruning per retention policy"
|
|
restic forget \
|
|
--tag "${SITE}" \
|
|
--keep-daily 5 \
|
|
--keep-weekly 4 \
|
|
--keep-monthly 6 \
|
|
--prune
|
|
|
|
# 5. Integrity: learn about corruption now, not at restore time.
|
|
log "verifying repository integrity"
|
|
if restic check; then
|
|
IS_CHECK_PASSED=true
|
|
log "restic check passed"
|
|
else
|
|
log "restic check FAILED - repository may be corrupt"
|
|
fi
|
|
else
|
|
log "restic not configured - local db.sql.gz + manifest only"
|
|
fi
|
|
|
|
# Record the verification result into the manifest so the UI can badge the set.
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
python3 /opt/backup/manifest.py \
|
|
--amend "${SETDIR}/manifest.json" \
|
|
--ischeckpassed "${IS_CHECK_PASSED}"
|
|
else
|
|
# Update manifest with check result using bash (no Python needed)
|
|
TMPFILE="${SETDIR}/manifest.json.tmp"
|
|
cat "${SETDIR}/manifest.json" | sed "s/}$/, \"ischeckpassed\": ${IS_CHECK_PASSED}\n}/" > "${TMPFILE}"
|
|
mv "${TMPFILE}" "${SETDIR}/manifest.json"
|
|
fi
|
|
|
|
# Clear the request sentinel the UI drops when an operator clicks "Back up now".
|
|
rm -f "${BACKUPS_ROOT}/.backup-requested" 2>/dev/null || true
|
|
|
|
BACKUP_DONE=true
|
|
log "backup set ${SETNAME} complete"
|