braid replace
Replace a disk with a new one using btrfs replace. Works for both live (still-online) and dead/missing disks.
When to use it
- A disk has failed and you need to rebuild data onto a replacement
- Proactively swapping a healthy disk for a larger or newer one
Basic example
The same invocation replaces a disk whether it is still live or already
dead/missing. braid resolves --old against pool.json to find the member and
detects its state automatically, so there is no mode to choose and --missing-id
is never required:
sudo braid replace --old toshiba1 --new toshiba4=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_NEW1
Common variations
Note: braid replace operates only on btrfs-authoritative MISSING devids. A
drive that is hot-unplugged while the pool is mounted contributes to
missing_count and appears in missing_devids in braid status before btrfs
promotes its devid to MISSING; both an explicit --missing-id cross-check
and the no-flag auto-resolve path refuse the devid with a specific hot-unplug
diagnostic until that promotion happens. See
Hot-unplug while pool is mounted.
Optionally assert which missing devid you expect (braid refuses if it disagrees with --old’s devid):
sudo braid replace \
--old toshiba1 \
--new toshiba4=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_NEW1 \
--missing-id 3
Preview what would happen:
sudo braid replace --old toshiba1 --new toshiba4=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_NEW1 --dry-run
Enroll a keyfile from a mounted USB drive on the new disk:
sudo braid replace \
--old toshiba1 \
--new toshiba4=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_NEW1 \
--enroll /mnt/usb
Mount the USB first so the --enroll directory refers to removable media,
not persistent host storage.
Pass passphrase non-interactively:
sudo braid replace --old toshiba1 --new toshiba4=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_NEW1 --passphrase-file /tmp/pass.txt
Important flags
| Flag | Purpose |
|---|---|
--old <name> | Name of the disk to replace |
--new <name>=<path> | Name and by-id path of the replacement disk, a whole disk (not a partition) |
--missing-id <devid> | Optional cross-check for a dead-disk replace: assert the missing btrfs devid. braid refuses if it disagrees with –old’s devid: the one its still-open mapper holds if its disk was hot-unplugged, else the one pool.json records. Never required. |
--enroll <dir> | Enroll braid.key from this directory into LUKS slot 1 on the new disk |
--dry-run | Show what would happen without executing |
--yes | Skip interactive confirmation |
--passphrase-stdin | Read passphrase from stdin |
--passphrase-file <path> | Read passphrase from a file (conflicts with --passphrase-stdin) |
--luks-format-arg=<ARG> | Advanced: pass one raw argument to cryptsetup luksFormat, repeated once per argument; always use the equals form (e.g. --luks-format-arg=--pbkdf --luks-format-arg=argon2id). braid refuses flags it manages itself – identity, key-material, integrity, hardware-encryption (OPAL), and on-disk-layout options such as --uuid, --label, --type, --key-file, and offset/sizing flags. It also refuses a bare --, which would make cryptsetup ignore every argument after it, and options that make cryptsetup exit without formatting (--help, --usage, --version, --test-args, -?, -V). braid passes the disk to cryptsetup ahead of every --luks-format-arg, so no argument can change which disk is formatted; cryptsetup ignores a stray value that no option takes. Before it writes pending-op.json, braid runs the full luksFormat command with --test-args, which checks the options and writes nothing, and refuses with cryptsetup’s message if cryptsetup rejects them. |
--progress auto|always|never | Control progress display (default: auto) |
What happens under the hood
For a fresh replacement disk (no LUKS):
- Pre-generates the replacement member’s LUKS UUID and LUKS-formats the new disk with the pool passphrase and a
braid-<name>label - Optionally enrolls a keyfile in slot 1
- Creates a LUKS header backup
- Opens the LUKS mapper
Then, for all replacements:
- Runs
btrfs replace startto copy data from the old device (or its mirrors) to the new device - Writes committed UUID-keyed membership to
pool.json, advances the journal to post-replace maintenance, and forgets the old disk’s alert state on the devid the new disk inherits: its acknowledged baseline and the evidence in any latched alert (the error counts, and the record that the old disk was missing). A latched alert for the old disk stays visible untilbraid ack, but ack will not apply the old disk’s counts to the new one or mark the new one as acknowledged-missing, so the new disk dropping out still alerts. If this cleanup fails, replace stops with the journal kept, andbraid recoverretries it - For live replacements: closes the old disk’s LUKS mapper
- Resizes the new device to use its full capacity (important when the new disk is larger)
- For missing-disk replacements that clear the last missing device: runs a soft RAID1 balance to restore redundancy on any single-profile chunks
- Clears the journal
The step-7 mapper close is best-effort, exactly as in braid remove: the btrfs replace is already committed, so a failed close prints a [warn] row, and a close-time ownership probe that cannot prove the old mapper is the expected disk – inactive, a probe failure, or a foreign LUKS UUID – skips the close with a Warning: line. Neither fails the replace, and a clean exit does not guarantee the old mapper is closed. The probe reads the LUKS UUID the kernel recorded when the old mapper was opened, not the disk’s header, so the old mapper still closes when the old disk is already gone.
The fresh-disk path always produces a local LUKS header backup in step 3; the existing-LUKS path produces one only when --enroll actually adds slot 1, so an already-enrolled disk is a no-op with no new backup. See Pending LUKS header backups – copy each .luksheader off-system and delete the local copy.
A sleep inhibitor is held throughout the replace to prevent the system from suspending. Suspending mid-replace can corrupt the btrfs topology.
If a btrfs exclusive operation (a running balance, device add/remove/replace, resize, or swap activate) is already in flight on the pool, braid does not fail – its btrfs commands queue behind the in-flight operation (via --enqueue) and the kernel runs them when the pool is free. A paused balance is the exception and is refused (see Safety checks below).
Safety checks / refusal cases
- Refuses if the pool is not mounted
- Refuses if
--oldand--neware the same disk: the same name, or a--newdisk whose LUKS UUID is--old’s. Such a disk is--olditself under another by-id path (a re-seated cable, a USB dock) or a clone of it, and recover could not tell a finished replace from an unfinished one. Use a different disk, or wipe that one so braid formats it fresh. - Refuses if the new disk’s LUKS UUID is already in use by the pool (registered membership or live btrfs devices) – detach the conflicting disk before retrying
- Refuses if the new disk is absent (not plugged in)
- Refuses a new disk that is not a whole disk. braid asks the kernel (sysfs), not the link’s name, so it refuses a partition even when its link has no
-partNsuffix, and refuses a device sysfs cannot answer for. Pass the disk’s own by-id link. This check runs when planning, so--dry-runrefuses too. - Refuses a fresh new disk that is not ready to format, both when planning (so
--dry-runrefuses too) and again just before the journal: if its/dev/mapper/braid-<new>mapper is already open, close it withsudo cryptsetup close braid-<new>; if a mount, an LVM volume group, an md array, or a device-mapper device holds the disk, free it (lsblk <by-id path>shows the holder). Then re-run. - Refuses if the new disk’s mapper capacity is smaller than the source disk’s btrfs
total_bytes(read viaBTRFS_IOC_DEV_INFO, the same valuebtrfs replace startcompares against). For existing LUKS targets, mapper capacity is derived from the LUKS2 segmentoffsetandsize(dynamicmeansraw - offset, fixed means the segment size). For fresh-LUKS targets, braid formats with a fixed data offset (luksFormat --offset, the same 16 MiB cryptsetup picks on an ordinary disk), so mapper capacity israw - offsetwhatever optimal I/O size or alignment offset the disk reports. braid refuses--offset/-oas a--luks-format-arg, since it would replace the pinned offset, and--hw-opal/--hw-opal-only, since OPAL would shrink the data segment to the drive’s alignment. It also refuses--align-payloadand--sector-size. Neither can change the layout, and both conflict with the pinned offset: cryptsetup refuses--align-payloadnext to--offset, and refuses a sector size that does not divideraw - offset. cryptsetup finds the sector-size conflict only when the format runs, after braid has journaled the replace, so the replace would stop andbraid recoverwould only undo it. - For live replacements: refuses if the pool has missing devices (resolve those first)
- For missing replacements: refuses if
--missing-idpoints to a live device - For missing replacements: refuses if
--missing-iddisagrees with--old’s devid (--oldalready identifies which member to rebuild). If--old’s disk was hot-unplugged and btrfs still lists it through its open mapper (null-underlying, before btrfs promotes it to MISSING), braid finds that mapper by the LUKS UUID it records and uses the devid btrfs holds it at; otherwise it uses the devid pool.json records - For missing replacements with no open mapper for
--old: refuses if pool.json has no recorded devid for--old–--missing-idcannot substitute, it must match the recorded devid - For missing replacements: refuses if the devid pool.json records for
--oldis held by another disk’s open mapper – pool.json is out of date - Verifies the passphrase against an existing pool member before formatting
- Refuses when cryptsetup rejects the
--luks-format-argoptions for a fresh new disk. braid checks them withcryptsetup luksFormat --test-args, which writes nothing, when planning, so--dry-runrefuses too. - Warns before confirmation and in
--dry-runif the live source device has I/O errors (informational, does not block) - Warns if existing pool drives have a keyfile but
--enrollwas not passed - Refuses if a pending operation journal (
pending-op.json) exists – runbraid recoverto reconcile. - Refuses if another braid operation is in progress (pool lock
/run/braid-pool.lockis held) – retry once it finishes. - Refuses if a btrfs balance is paused on the pool – resume or cancel it first. A paused balance holds the exclusive-operation lock indefinitely, so braid cannot wait it out.
- Refuses if the pool is mounted read-only. If an operator or a tool remounted it,
mount -o remount,rw <mount-point>fixes that; if btrfs forced it read-only after an error, only a fresh mount does (braid lock, thenbraid unlock). See Pool is mounted read-only. - Refuses when UPS support is enabled and
braid ups statuscannot verify a trustedOL(utility-power) state.
Related commands
- braid status – find devids and see which disks are missing
- braid remove-missing – forget a dead device without replacing it
- braid add – add a new disk (without replacing an existing one)