commit b155475c8cfad31d8a580143fcb001596f2ac21f Author: James Coleman Date: Fri Aug 21 09:34:09 2026 -0500 first commit Claude-Session: https://claude.ai/code/session_014PM3SXLZ3PUivkMQ8QXTxT diff --git a/kvm-zvol-restore/SKILL.md b/kvm-zvol-restore/SKILL.md new file mode 100644 index 0000000..cebad3c --- /dev/null +++ b/kvm-zvol-restore/SKILL.md @@ -0,0 +1,169 @@ +--- +name: kvm-zvol-restore +description: Restore KVM/libvirt VM disks from borg backups onto ZFS zvols exported over iSCSI. Use when asked to restore, roll back, or recover a VM's drive to a previous date, or to inspect what VM disk backups are available. Covers the tama/kiki zvol+iSCSI setup backed up by kvm-backup-zvol-iscsi.sh. +--- + +# Restoring KVM VM disks from borg to ZFS zvols + +## Topology + +Two hosts, and it matters which one you run each command on: + +| Host | Name | Role | +|---|---|---| +| `10.0.0.5` | `kiki` | libvirt host (runs the VMs) **and** holds the borg repo at `/media/Storage/Backup/kvm` | +| `10.0.0.6` | `tama` | ZFS server: owns the zvols (`tank/kvm/*`), exports them via iSCSI (LIO/targetcli), runs the backup script | + +The backup script is `/usr/local/bin/kvm-backup-zvol-iscsi.sh` **on tama (.6)**, not on kiki. +It discovers domains via `virsh` locally and on `REMOTE_HOSTS` (`kiki`, `gaming-pc`), maps each +iSCSI-backed disk to a local zvol, snapshots it, and `dd`s the snapshot device into borg. + +VMs reach their disks over the **storage network** `10.0.100.6:3260`, not `10.0.0.6`. + +## What the backups actually are + +Raw whole-device images (`dd` of a ZFS snapshot block device) piped into borg as **stdin**, so each +archive holds exactly one item named `stdin`. There is no filesystem structure inside — restoring +means writing the stream back over the whole block device. + +Archive naming: + +``` +-- disk image, e.g. centos7-vda-2026-08-14T07:38:41 +-xml- virsh dumpxml output +``` + +Retention is `--keep-daily 7 --keep-weekly 4 --keep-monthly 6`, so anything older than a week +only exists at weekly/monthly granularity. Check before promising a specific date. + +## Gotchas that have actually bitten + +1. **Archive names contain colons.** `centos7-vda-2026-08-14T07:38:41`. Never use `:` as a field + separator when building job lists in a shell script — it silently truncates the name to + `centos7-vda-2026-08-14T07` and borg reports "Archive does not exist". Use `|`. +2. **`borg list --format "{archive}"` fails on an archive.** `{archive}` is a repo-level key; using + it while listing archive *contents* errors out and looks exactly like a missing archive. + To test existence use `borg info "::$ARCHIVE"`. +3. **The device name is not always `vda`.** `centos6` is `sdb`; `centos7`/`rocky9` are `vda`. + Always read the device from the archive name or `virsh domblklist`. +4. **`virsh domblklist` shows CDROMs with source `-`.** Skip those; only `-iscsi-` paths are backed up. +5. **Domain name vs zvol name is case-insensitive.** IQN target names are lowercased + (`iqn...tama:centos7-2`) while the zvol may be `tank/kvm/CentOS7-2`. There are also stale archives + from an older capitalized `Centos7` domain — don't confuse them with current `centos7`. +6. **CentOS 6 ignores `virsh shutdown`** (no/old acpid). It may need several minutes or a + `virsh destroy`. Never start writing until `virsh domstate` says `shut off`. +7. **Don't run during the backup window** (~07:24–07:55 daily). The backup holds + `/tmp/backup-zvol-iscsi.pid` on tama and would be snapshotting the same zvols. + +## Procedure + +### 1. Shut down the VMs (on kiki, .5) + +```bash +ssh root@10.0.0.5 'for d in vm1 vm2; do virsh shutdown "$d"; done' +# then poll until every one reports "shut off" — do not proceed otherwise +ssh root@10.0.0.5 'for i in $(seq 1 30); do s=$(virsh domstate vm1); [ "$s" = "shut off" ] && break; sleep 5; done; virsh domstate vm1' +``` + +If a guest won't go down gracefully, confirm with the user before `virsh destroy`. + +### 2. Find the archives (from tama, .6) + +```bash +ssh root@10.0.0.6 'export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes + borg list --format "{archive}{TAB}{time}{NL}" root@10.0.0.5:/media/Storage/Backup/kvm' \ + | grep -Ei "^(vm1|vm2)-" +``` + +Then record each archive's exact byte size — it must equal the zvol's size: + +```bash +borg info "::$ARCHIVE" # "Original size" column, or borg list shows the stdin item size +blockdev --getsize64 /dev/zvol/tank/kvm/ +``` + +### 3. Map disks to zvols + +```bash +ssh root@10.0.0.5 'virsh domblklist ' # dev -> /dev/disk/by-path/...-iscsi-iqn...:-lun-0 +ssh root@10.0.0.6 'zfs list -o name,volsize,volmode -r tank/kvm -d 1' +``` +The zvol is the IQN target name after the last colon, matched case-insensitively under `tank/kvm/`. + +### 4. Optional rollback point + +A pre-restore `zfs snapshot tank/kvm/@pre-restore-` is cheap insurance, **but ask first** — +it is not always wanted, and the daily backup snapshots (`SNAPSHOTS_KEEP=2`) may already cover it. + +### 5. Restore (on tama, .6) + +Run detached — 500 GiB takes ~20 min at ~475 MB/s, and an SSH drop must not kill it. +Guard every device write behind a size check and an existence check. + +```bash +#!/bin/bash +export BORG_REPO="root@10.0.0.5:/media/Storage/Backup/kvm" +export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes +export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes +set -o pipefail + +# zvol|archive|expected_bytes (pipe-delimited: archive names contain colons) +JOBS=( + "centos7|centos7-vda-2026-08-14T07:38:41|536870912000" +) + +for J in "${JOBS[@]}"; do + IFS="|" read -r ZVOL ARCHIVE EXPECT <<<"$J" + DEV="/dev/zvol/tank/kvm/$ZVOL" + echo "=== $(date "+%F %T") restoring $ARCHIVE -> $DEV ($EXPECT bytes)" + + [[ -b "$DEV" ]] || { echo "FAIL: $DEV not a block device"; exit 1; } + + ACTUAL=$(blockdev --getsize64 "$DEV") + [[ "$ACTUAL" == "$EXPECT" ]] || { echo "FAIL: $DEV is $ACTUAL, archive is $EXPECT"; exit 1; } + + borg info "::$ARCHIVE" >/dev/null 2>&1 || { echo "FAIL: archive $ARCHIVE not found"; exit 1; } + + borg extract --stdout "::$ARCHIVE" \ + | dd of="$DEV" bs=4M iflag=fullblock conv=fsync status=progress \ + || { echo "FAIL: restore of $ARCHIVE failed"; exit 1; } + + echo "=== $(date "+%F %T") completed $ZVOL" +done +echo "=== $(date "+%F %T") ALL RESTORES COMPLETE" +``` + +Launch and watch: + +```bash +ssh root@10.0.0.6 'nohup setsid /root/restore.sh > /root/restore.log 2>&1 -lun-0' +``` + +A `iscsiadm -m node -T --rescan`, or logout/login of the session, is the heavier alternative. + +### 7. Boot and verify + +```bash +ssh root@10.0.0.5 'virsh start ; virsh domstate ' +``` + +Restoring the domain XML is a **separate** decision — the `-xml-` archives exist, but rolling back a +disk rarely needs the definition rolled back too, and doing so can undo unrelated config. Ask first: + +```bash +borg extract --stdout "::-xml-" > /tmp/.xml # then virsh define +``` diff --git a/unit-testing/SKILL.md b/unit-testing/SKILL.md new file mode 100644 index 0000000..03a09be --- /dev/null +++ b/unit-testing/SKILL.md @@ -0,0 +1,165 @@ +--- +name: unit-testing +description: My unit-testing style. Use whenever writing, reviewing, or pruning unit tests in any project, before writing the first test function. Covers what deserves a unit test, what must not be tested, and the required mechanics (testify, table-driven, real dependencies first, mocks only as a last resort). +--- + +# Unit testing style + +I want few, high-value tests, in two tiers. + +1. **Unit tests.** Small, fast, and mainly covering serialization boundaries: + marshal/unmarshal, parse/encode, round-trips, and on-disk or wire-format + invariants. +2. **Integration tests.** The primary safety net. They exercise the project as + a whole against real systems (real backends, real servers, real databases) + and usually live in a separate suite behind build tags, run with + `make test-integration`. + +More tests are not better. I have had redundant tests removed by the dozens +("Slim down the tests", "remove simple logic tests"). Write fewer tests than +feels natural, and never pad coverage. + +## What deserves a unit test + +- **Serialization and parsing.** JSON/YAML marshal and unmarshal round-trips, + config parsing, enum-to-string stability (a backup must stay readable even + if an iota constant gets reordered). For codecs, encode, decode, and + compare, with the invalid-input case paired in the same test. +- **External-format invariants.** Scan/rewrite behavior against fixtures + shaped exactly like what the external tool or system really emits, + especially the shapes an integration run can't easily seed. +- **Contract tests against an external system's declared interface.** Assert + the encoder's output against what the external system's spec or + introspection declares; the declared signature or schema string can itself + be the assertion. Include tolerance tests: truncated or old-version input + must not panic or misalign. +- **Facts about the outside world** the code depends on: a real tool's exact + output spelling, a protocol quirk, a version-capability boundary pinned to + the real deployment targets. These stay even when the test is four lines. +- **Real algorithmic logic** with a non-obvious result: coverage and overlap + relations, canonicalization that guards against churn, index math, scoring. + One representative test per axis, never an exhaustive fan-out. +- **API-boundary behavior** of a service. Drive the real handler with + `httptest` and assert observable effects (status codes, files written or + removed), not internals. + +## What must NOT be unit tested + +- **Simple logic.** Trivial predicates, single-field identity assertions + ("Equal ignores field X"), tests that restate a small helper's own body. + These are useless. They don't test anything worthwhile. +- **Anything the integration suite already proves live.** Lifecycle + operations (add, remove, sync, backup, restore) that a named integration + subtest pushes through a real backend don't get duplicated in miniature + with fakes. +- **Pure reject-only guards**, meaning a lone `require.Error` on one + validation. The real system fails loudly anyway. A rejection test earns its + place only when it also pins a positive boundary case next to the + rejection. +- **Mock-based tests of internal collaborators.** Mock-manager style tests + get deleted. Mocks aren't banned outright, see "Real dependencies" below, + but mocking your own internals never qualifies. + +For borderline cases, one question decides: does the test encode an external +fact? If it pins a real spelling, format, or quirk of a system outside this +codebase, keep it. If it only restates this codebase's own code, delete it. A +bug there fails loudly downstream. + +These rules govern what gets committed, not how to work. A throwaway test to +confirm behavior while developing or debugging is fine, even for simple +logic. Write it, run it, learn from it, then delete it before the work is +done. It must not land in the suite. + +## Mechanics + +- Go tests use `testing` plus `github.com/stretchr/testify`, never + hand-rolled `if got != want`. Some older projects predate this, but every + migration has been toward testify. Split the roles: `require` for setup and + anything that invalidates the rest of the test, `assert` for the actual + claims, so one run reports every wrong field. +- Assertion messages are sentences that carry the reason, not labels: + `assert.Equal(t, 1, *calls, "a ready service must not be polled twice")`. + In tables, the message identifies the case: `"signature mismatch for %s"`. +- Go table-driven wherever more than one input shape exists: anonymous struct + slice with `name`, inputs, and `want` fields. Rows may carry a `why` column + so the table holds the rationale ("leading zeros do not change the value"). + `t.Run(tc.name, ...)` is optional when the assert message already names the + case. Sub-test names outside tables read as full English claims: + `t.Run("metric zero falls back to default", ...)`. +- Every test gets a doc comment stating what invariant it locks and why, + ideally naming the real-world source: the distro or tool version, the + kernel flag, or the regression it pins. Spell out arithmetic inline so an + expected number isn't magic; the comment shows the terms that add up to it. +- Test helpers are small, local to the file, `t.Helper()`-marked, use + `t.Cleanup` to restore anything they change, and build realistic fixtures + laid out exactly as the real system writes them. +- Re-read from the source of truth before asserting. Re-construct the backend + from disk after a write, so the test exercises the written config rather + than in-memory state. Re-query the kernel rather than trusting a cached + handle. + +## Real dependencies, never substitutes + +- HTTP: `httptest` servers, or a real TLS listener with self-signed test + certs when the TLS path itself matters. Never live external services. Live + URLs get replaced with a local server plus fixture repos built with docker + dummy packages. +- Protocol peers: a real in-process server speaking the real wire protocol at + the boundary, fed fixture files authored in the protocol's native format, + or a fake that decodes real requests and streams captured responses. Drive + error paths by mutating the fake mid-test, walking happy, data-missing, and + unreachable phases with one golden file per phase. +- External commands: fake them as executable scripts on disk, selected via + config paths or a prepended `$PATH` entry. The exec boundary stays real. +- Databases: the real database engine in a container (testcontainers), never + a lighter engine standing in for the one production runs. +- Filesystem: real files in `t.TempDir()`, byte-for-byte like production. + Functions that walk absolute paths take a root-prefix parameter so a + tempdir can stand in for `/`. +- System and network APIs: a network namespace (with `runtime.LockOSThread()`, + dummy links, and `t.Skip` when not root) lets tests drive the real kernel + safely. A fresh namespace per test is the isolation. +- OS backends beyond that belong to the integration suite, which may be a + cross-compiled test binary shipped to a target host. Destructive + integration tests operate only on throwaway PID-named objects, never a + system default or live resource whose loss could cut the SSH session. + Cleanup uses `context.Background()` rather than the + possibly-cancelled test context, and pre-deletes idempotently. + +Fixtures: prefer real captured output, checked into `testdata/` with +provenance noted (which host, which tool version). Assert on specific parsed +fields with a comment per quirk, rather than golden-diffing everything. +Synthetic samples are a fallback for conditions no real capture shows, and +get labeled synthetic. Whole-output golden files (metrics exposition, +rendered configs) are compared with a diff that prints the golden path on +mismatch. When two tools report the same underlying state, cross-check +their fixtures against each other. + +Mocks are okay only when there is no safe way to exercise the real thing, +when no isolation mechanism (namespace, container, tempdir, httptest, +in-process protocol server) makes the real dependency safe or practical. The +order is real thing, then real thing isolated, then mock as last resort. +Never mock to dodge the setup cost of an isolation mechanism that exists. +When a test double is warranted, hand-write it at the external boundary: a +call-recording fake of the external interface, a plain function field, a +closure with a call counter. No gomock, no mockery, no testify-mock, and no +interfaces introduced solely for testing. Small documented seams in +production code are fine instead: a sentinel test value, a package-level +poll-interval knob swapped and restored via `t.Cleanup`, an override +timestamp field for deterministic output. + +## Workflow + +- Wire testing through the Makefile (`make test`, `make test-integration`) + and document that in the README. Verify tooling (schema and config + validation) gets its own command and runs under `make test`. +- Integration suites sit behind build tags (`//go:build _integration`) + so plain `go test ./...` stays dependency-free. Capability differences are + handled with `t.Skip` in both directions: an old server skips the new + feature, a new server skips the legacy guard. +- After adding or removing tests, `gofmt`, `go vet` (including other-GOOS + vets for platform-gated files), and the make target must pass. Confirm + removed tests left no dead helpers or unused imports. +- When asked to add tests, propose the small set that fits the rules above. + When touching an existing suite, look for tests that violate these rules + and offer to prune them. diff --git a/unslop/SKILL.md b/unslop/SKILL.md new file mode 100644 index 0000000..e8505d3 --- /dev/null +++ b/unslop/SKILL.md @@ -0,0 +1,154 @@ +--- +name: unslop +description: Cut AI tells from anything written in my projects. Apply to all prose I ship, including code comments, godoc, error strings, log lines, Svelte UI copy, docs, READMEs, and commit messages. Always apply. +--- + +# Unslop + +Edit text so it reads like a person who works on this codebase wrote it. This +covers every writing surface in a Go/Svelte project: godoc and code comments, +error and log messages, UI copy, markdown docs, commit messages, and PR +descriptions. + +## Process + +1. Scan for the patterns below. +2. Rewrite. Preserve meaning, match the surrounding voice. +3. Self-audit: "What makes this obviously AI generated?" Fix remaining tells. + +## Voice + +- Have opinions. State what happened and what it means, don't neutrally list. +- Vary rhythm. Short sentences. Then longer ones that take their time. +- Be specific. Not "this can cause issues" but "the client stays disconnected + and the request times out after 2m". +- If a sentence could appear unchanged in another project's docs, it says + nothing about this one. Cut it. +- Match the existing voice. When two docs cover sibling features, they should + read like the same person wrote both: same structure, same phrasing for the + same concepts, same level of detail. + +## Go code comments + +- Every commented element (func, struct, var) gets a comment whose first + word is the element's name, written as a complete sentence ending in a + period. `// ParseSize converts a human-readable size ("2.5G") to bytes.` +- A comment states a constraint the code can't show, a real-world fact, or + the regression it guards. Never a narration of the next line, where the + code came from, or why the change is correct. That last kind is + review-speak and turns into noise the moment the change merges. +- The exception to the no-narration rule: a function with multiple logical + steps uses short section comments to label each phase. "Validate input.", + "Persist to database.", "Build response." Those segment the function; + they don't narrate individual lines. +- Operational tone, written for someone maintaining or debugging the + system. No conversational language, jokes, or speculative notes. +- Name the external source when there is one: the distro, the tool version, + the kernel behavior. Naming the exact distro quirk the branch exists for + beats "handle legacy services". +- Spell out arithmetic so numbers aren't magic: + `// reallocated 8 -> 40+40 capped 80; total 89 -> REPLACE_SOON.` +- Match the file's comment density. A file with sparse comments doesn't get + a narrated paragraph bolted onto one function. + +## Errors and logs + +- Error strings: lowercase, no trailing punctuation, no "Error:" prefix. + Chain context with colons or `%w`: `syncing repo: connecting to source: + connection refused`. Say what was being attempted, not "failed to failed + to". +- An error the operator will read should point at the fix when one is known: + "set license.key in the config, or pass --license-key". +- Log messages: terse, lowercase, structured fields for the variables, no + exclamation marks, no emoji. `task executed` with fields beats "Task was + executed successfully!". +- Never log or print chatbot phrases: "Successfully completed!", "Oops!", + "Something went wrong :(". + +## Svelte and UI copy + +- Sentence case everywhere: buttons, labels, headings, menu items. "Import + test", not "Import Test". +- Error states name what failed and what to do next. Not "Oops! Something + went wrong" but "the server is unreachable; check that the service is + running". +- Empty states say what will appear there or how to add the first item. No + cutesy filler, no illustrations-with-quips tone. +- No exclamation marks in UI text. No emoji in buttons, headings, toasts, or + placeholder text. +- Labels are literal. A field for a slug says "Slug", not "Your unique + identifier". Tooltips explain the consequence of the setting, not restate + its name. +- No marketing adjectives in product UI: "powerful", "seamless", "blazing + fast". The interface should describe, not sell. + +## Docs and READMEs + +- Say the thing directly. If a sentence can be replaced by a plainer one + that says the same thing, replace it. +- A doc stands alone for its task. Don't make the reader open a second doc + to use the first one; inline the few lines they need. +- Don't restate. If an example already appeared in an earlier section, later + sections reference it, they don't duplicate it. +- Don't explain external systems. A doc about configuring a third-party + integration covers how to configure it here and why, not how the third + party's API works. +- Structure follows the sibling doc. Same heading shapes, same table format, + same key: value conventions, so the docs read as one set. +- README covers how to build and test via the Makefile, actual usage, and + real configuration. No badge walls, no "Features" bullet lists of + adjectives, no roadmap promises. +- Headings in sentence case. No decorative emoji. No bold on every proper + noun. + +## Commits and PRs + +- Imperative, plain, specific: "Switch unit tests to testify", "Replace + polling with retry backoff". No "This PR introduces...", no "Enhanced", + no scope theater. +- The body states what changed and why, names the bug or behavior pinned, + and stops. No summary tables of the diff, no "Testing" section that just + says tests pass. + +## Language tells + +- AI vocabulary: additionally, crucial, delve, enhance, fostering, garner, + intricate, landscape (abstract), pivotal, robust, seamless, showcase, + streamline, tapestry, testament, underscore, utilize, leverage. Use the + plain word. +- Fancy ways to say "is": "serves as", "stands as", "boasts", "features". + Say "is" or "has". +- "Not just X, but Y." State the point directly. +- Rule of three. Don't force ideas into groups of three; use the natural + number. +- Filler: "in order to" becomes "to", "due to the fact that" becomes + "because", "it is important to note that" gets deleted. +- Hedging stacks: "could potentially possibly" becomes "may". +- Abstract metaphor nouns: substrate, wedge, vector, nexus, primitive (as + noun), harness (as metaphor), surface (as in "API surface"), scaffolding + (as metaphor), paradigm, north star, flywheel. Pick the concrete word. +- Say what it does, not how it feels. "SQL you can read" names a feeling; + "`.toSQL()` returns the exact string sent to the database" names a + mechanism. If a sentence can't be restated as a fact, instruction, or + number, cut it. +- Active voice. "The compiler validates queries", not "queries are + validated". Passive only when the actor is unknown or doesn't matter. + +## Punctuation and style tells + +- No em dashes, anywhere. Use periods or commas. Don't swap in parentheses + or en dashes as a substitute; end the sentence instead. +- Colons before a list or example only, not as mid-sentence connectors. +- No arrow chains in prose ("A → B → fails"). Write the sentence. Arrows + are fine inside code and code comments where they're the local idiom. +- Straight quotes, not curly. +- Bold sparingly. A bold lead-in that names an item and ends in a period is + fine; a bold label with a colon that restates the line is a tell. +- No decorative emoji in headings, bullets, commits, or code. + +## Chat artifacts + +Never let these into committed text: "I hope this helps!", "Let me know +if...", "Great question!", "You're absolutely right!", "Found the smoking +gun!", cutoff disclaimers like "while specific details are limited". Respond +directly, write directly.