Homelab Academy / Modules
Guided practice + break/fix + lab gate.
Module: Segmentation, Backup & Power
Activity type: Lab (Practice)
Lab risk: medium
Objective: Distinguish a file copy from a transactionally consistent database backup.
Every command in this lab creates or modifies content only beneath /opt/lab-classroom/class74/. The exercise does not stop services, change firewall rules, alter operating-system configuration, or overwrite a database outside the classroom directory.
1
Create the classroom directories and initial database
python3 - <<'PY'
from pathlib import Path
import os
import sqlite3
root = Path('/opt/lab-classroom/class74')
if root.exists() and any(root.iterdir()):
raise SystemExit('Safety stop: /opt/lab-classroom/class74 is not empty. Use the rollback procedure or inspect the existing work.')
for directory in (root, root / 'live', root / 'backups', root / 'restore'):
directory.mkdir(parents=True, exist_ok=True)
os.chmod(directory, 0o700)
db_path = root / 'live' / 'inventory.db'
with sqlite3.connect(db_path) as conn:
conn.execute('PRAGMA foreign_keys = ON')
conn.execute('CREATE TABLE devices (id INTEGER PRIMARY KEY, hostname TEXT NOT NULL UNIQUE, role TEXT NOT NULL, state TEXT NOT NULL, updated_at TEXT NOT NULL)')
conn.executemany(
'INSERT INTO devices(id, hostname, role, state, updated_at) VALUES (?, ?, ?, ?, ?)',
[
(1, 'router01', 'router', 'active', '2025-01-15T10:00:00Z'),
(2, 'nas01', 'storage', 'active', '2025-01-15T10:01:00Z'),
(3, 'db01', 'database', 'maintenance', '2025-01-15T10:02:00Z')
]
)
conn.commit()
os.chmod(db_path, 0o600)
print(db_path)
PY
2
Inspect the live database before backup
python3 - <<'PY'
import sqlite3
from pathlib import Path
path = Path('/opt/lab-classroom/class74/live/inventory.db')
with sqlite3.connect(path) as conn:
rows = conn.execute('SELECT id, hostname, role, state, updated_at FROM devices ORDER BY id').fetchall()
for row in rows:
print(row)
print('row_count=', len(rows))
PY
3
Create a consistent backup and its metadata
python3 - <<'PY'
from pathlib import Path
from datetime import datetime, timezone
import hashlib
import json
import os
import sqlite3
root = Path('/opt/lab-classroom/class74')
source = root / 'live' / 'inventory.db'
backup = root / 'backups' / 'inventory-backup.db'
metadata = root / 'backups' / 'inventory-backup.json'
if backup.exists() or metadata.exists():
raise SystemExit('Safety stop: backup output already exists.')
with sqlite3.connect(source) as src, sqlite3.connect(backup) as dst:
src.backup(dst)
with backup.open('rb') as handle:
digest = hashlib.sha256(handle.read()).hexdigest()
record = {
'format': 'SQLite database produced with sqlite3.Connection.backup',
'source': str(source),
'backup': str(backup),
'created_utc': datetime.now(timezone.utc).isoformat(),
'sha256': digest
}
metadata.write_text(json.dumps(record, indent=2) + '\n', encoding='utf-8')
os.chmod(backup, 0o600)
os.chmod(metadata, 0o600)
print(json.dumps(record, indent=2))
PY
4
Make post-backup changes to the live database
python3 - <<'PY'
from pathlib import Path
import sqlite3
path = Path('/opt/lab-classroom/class74/live/inventory.db')
with sqlite3.connect(path) as conn:
conn.execute("UPDATE devices SET state = 'degraded', updated_at = '2025-01-15T11:00:00Z' WHERE hostname = 'router01'")
conn.execute("INSERT INTO devices(id, hostname, role, state, updated_at) VALUES (4, 'app01', 'application', 'active', '2025-01-15T11:01:00Z')")
conn.commit()
rows = conn.execute('SELECT id, hostname, state FROM devices ORDER BY id').fetchall()
for row in rows:
print(row)
print('live_row_count=', len(rows))
PY
5
Verify the stored backup digest before restoration
python3 - <<'PY'
from pathlib import Path
import hashlib
import json
root = Path('/opt/lab-classroom/class74')
backup = root / 'backups' / 'inventory-backup.db'
metadata = json.loads((root / 'backups' / 'inventory-backup.json').read_text(encoding='utf-8'))
actual = hashlib.sha256(backup.read_bytes()).hexdigest()
print('recorded_sha256=', metadata['sha256'])
print('actual_sha256=', actual)
print('digest_match=', actual == metadata['sha256'])
if actual != metadata['sha256']:
raise SystemExit('Backup digest mismatch; restoration is blocked.')
PY
6
Restore into an isolated database
python3 - <<'PY'
from pathlib import Path
import os
import shutil
root = Path('/opt/lab-classroom/class74')
source = root / 'backups' / 'inventory-backup.db'
destination = root / 'restore' / 'inventory-restored.db'
if destination.exists():
raise SystemExit('Safety stop: restore destination already exists.')
shutil.copy2(source, destination)
os.chmod(destination, 0o600)
print(destination)
PY
7
Validate structure and application-level data
python3 - <<'PY'
from pathlib import Path
import sqlite3
path = Path('/opt/lab-classroom/class74/restore/inventory-restored.db')
expected = [
(1, 'router01', 'router', 'active', '2025-01-15T10:00:00Z'),
(2, 'nas01', 'storage', 'active', '2025-01-15T10:01:00Z'),
(3, 'db01', 'database', 'maintenance', '2025-01-15T10:02:00Z')
]
with sqlite3.connect(path) as conn:
integrity = conn.execute('PRAGMA integrity_check').fetchone()[0]
rows = conn.execute('SELECT id, hostname, role, state, updated_at FROM devices ORDER BY id').fetchall()
print('integrity_check=', integrity)
print('restored_rows=', rows)
print('expected_rows_match=', rows == expected)
if integrity != 'ok':
raise SystemExit('Restored database failed SQLite integrity checking.')
if rows != expected:
raise SystemExit('Restored application data does not match the expected recovery point.')
print('RESTORE_VERIFIED')
PY
8
Compare live and restored recovery points
python3 - <<'PY'
from pathlib import Path
import sqlite3
root = Path('/opt/lab-classroom/class74')
def read_rows(path):
with sqlite3.connect(path) as conn:
return conn.execute('SELECT id, hostname, state, updated_at FROM devices ORDER BY id').fetchall()
live = read_rows(root / 'live' / 'inventory.db')
restored = read_rows(root / 'restore' / 'inventory-restored.db')
print('live=', live)
print('restored=', restored)
print('live_count=', len(live))
print('restored_count=', len(restored))
PY
| Symptom | Likely cause | Fix |
|---|---|---|
| Python reports that the sqlite3 module is unavailable. | The installed Python build lacks SQLite support or Python 3 is incomplete. | Use a Python 3 installation that includes the standard-library sqlite3 module. Verify with python3 -c "import sqlite3; print(sqlite3.sqlite_version)" before rerunning the lab. |
| The setup step stops because the classroom directory is not empty. | A previous attempt or unrelated file already exists within the lab directory. | Inspect /opt/lab-classroom/class74/, preserve any required evidence, and then use the explicit rollback procedure. Do not bypass the safety check without understanding the existing files. |
| Permission denied is reported when creating the classroom directory. | The current account cannot write beneath /opt/lab-classroom/. | Ask the lab administrator to create and assign the class74 directory to the learner. Do not redirect the exercise to an unapproved system path. |
| The digest verification reports digest_match=False. | The backup artifact or metadata changed after creation, or the wrong files are being compared. | Do not continue with the restore as though the artifact were trusted. Confirm the exact paths, inspect timestamps and access history, and recreate the backup from a known-good source if this is a classroom rerun. |
| The restore step says the destination already exists. | An earlier restore is still present. | Preserve the existing restored database if it is needed for investigation. Otherwise use the rollback procedure and restart the lab from step 1. |
| PRAGMA integrity_check does not return ok. | The backup or restored copy is damaged, incomplete, or not a valid SQLite database. | Reject the restored artifact, compare its digest with the expected backup digest, verify available storage, and create a fresh database-aware backup. |
| The restored database contains app01 or shows router01 as degraded. | The live database was copied after the simulated changes, or the verification script opened the live path instead of the restore path. | Check every source and destination path. The expected backup must be created before step 4, and validation must target /opt/lab-classroom/class74/restore/inventory-restored.db. |
| The backup operation reports that the database is locked. | Another process holds a conflicting transaction or the source is being used in an unexpected locking mode. | Identify the process using only the classroom database, allow its transaction to complete, and retry. Do not terminate unrelated production processes for this exercise. |
Database backups frequently contain the same secrets, personal data, and operational records as the live database; protect them to the same or a stronger standard.
Use a dedicated backup identity with narrowly scoped database and storage permissions.
Encrypt backup data in transit and at rest in production. Store encryption keys separately from the backup artifacts and test key recovery.
Protect metadata and digests from unauthorized modification. A digest stored beside a backup is useful for accidental corruption detection but is not strong evidence against a malicious actor who can alter both files.
Keep at least one production backup copy in a failure domain separate from the database host.
Consider immutable or offline retention so compromised production credentials cannot silently destroy every recovery copy.
Audit backup creation, retrieval, expiration, restore attempts, and changes to retention policies.
Restore sensitive production data only into environments with equivalent access controls, logging, and data-handling rules.
Sanitize or synthesize data before using restored production databases in development or training environments.
Do not log database credentials in shell history, scripts, job output, or backup metadata.
The lab applies restrictive owner-only modes to its directories and files, but permissions alone do not provide encryption or host isolation.
Remove only the known files and empty directories created by this lesson. The script refuses to operate unless the resolved root is exactly /opt/lab-classroom/class74.
python3 - <<'PY'
from pathlib import Path
root = Path('/opt/lab-classroom/class74')
if root.resolve() != Path('/opt/lab-classroom/class74'):
raise SystemExit('Safety stop: unexpected rollback path.')
files = [
root / 'restore' / 'inventory-restored.db',
root / 'backups' / 'inventory-backup.json',
root / 'backups' / 'inventory-backup.db',
root / 'live' / 'inventory.db'
]
for path in files:
if path.exists():
if not path.is_file():
raise SystemExit(f'Safety stop: expected a regular file at {path}')
path.unlink()
for directory in (root / 'restore', root / 'backups', root / 'live', root):
if directory.exists():
directory.rmdir()
print('Class 74 lab artifacts removed.')
PY
Confirm that /opt/lab-classroom/class74/ no longer exists. If removal stops because an unexpected file or non-empty directory is present, inspect it manually rather than broadening the deletion procedure.