Commit da718350 authored by Vitaly Lipatov's avatar Vitaly Lipatov

Add ZFS label tool for mirror disk recovery

Tool to read, parse, modify and write ZFS vdev labels. Allows recovering a detached disk back into a mirror by copying labels between devices. Co-Authored-By: 's avatarClaude Opus 4.6 (1M context) <noreply@anthropic.com>
parent ee8ba7ed
# ZFS Label Tool — редактор меток ZFS
Инструмент для чтения, модификации и записи меток (labels) на устройствах ZFS.
Позволяет вернуть отключённый (`zpool detach`) диск обратно в зеркало
без полного resilver.
## Проблема
При `zpool detach` ZFS стирает метки на отключённом диске. Стандартного способа
вернуть диск обратно как «родной» не существует — `zpool attach` всегда запускает
полный resilver, даже если данные на диске почти актуальны.
`zhack label repair -u` может восстановить uberblock, но не может добавить
устройство обратно в конфигурацию пула (vdev_tree).
Этот инструмент решает проблему, напрямую модифицируя nvlist в метках.
## Использование
```bash
# Показать содержимое меток
python3 zfs_label_tool.py show /dev/sdX
# Проверить контрольные суммы и round-trip кодирование
python3 zfs_label_tool.py verify /dev/sdX
# Добавить устройство в зеркало (модифицирует метки на target)
python3 zfs_label_tool.py add-child <target> <child>
# Синхронизировать конфигурацию между дисками
python3 zfs_label_tool.py sync-labels <source> <target>
# Dry-run (показать изменения без записи)
python3 zfs_label_tool.py add-child -n <target> <child>
```
## Пошаговый сценарий восстановления
```bash
# 1. Восстановить uberblock на отключённом диске
zhack label repair -u /dev/disk/by-id/ata-DETACHED_DISK-part1
# 2. Экспортировать пул (или загрузиться с zfs_autoimport_disable=1)
zpool export poolname
# 3. Добавить диск в конфигурацию на всех активных устройствах пула
python3 zfs_label_tool.py add-child /dev/disk/by-id/ata-ACTIVE_DISK1-part1 \
/dev/disk/by-id/ata-DETACHED_DISK-part1
python3 zfs_label_tool.py add-child /dev/disk/by-id/ata-ACTIVE_DISK2-part1 \
/dev/disk/by-id/ata-DETACHED_DISK-part1
# 4. Синхронизировать конфигурацию на отключённый диск
python3 zfs_label_tool.py sync-labels /dev/disk/by-id/ata-ACTIVE_DISK1-part1 \
/dev/disk/by-id/ata-DETACHED_DISK-part1
# 5. Импортировать пул
zpool import poolname
```
## Тестирование
```bash
sudo bash test_zfs_mirror.sh
```
Скрипт создаёт файловое зеркало из 3 дисков, отключает один, восстанавливает
его через инструмент и проверяет целостность данных.
---
# Структура ZFS на диске — подробное описание
## Обзор
ZFS (Zettabyte File System) хранит данные в **пулах** (zpools), состоящих из
**виртуальных устройств** (vdevs). Каждое физическое устройство в пуле содержит:
- 4 копии **меток** (vdev labels) — по 256 КБ каждая
- **Данные** — блоки файловой системы, организованные в дерево Merkle
- **Uberblock** — корневой указатель на всё дерево метаданных
### Ссылки
- [OpenZFS source: vdev_label.c](https://github.com/openzfs/zfs/blob/master/module/zfs/vdev_label.c)
- [OpenZFS source: nvpair.c](https://github.com/openzfs/zfs/blob/master/module/nvpair/nvpair.c)
- [ZFS On-Disk Specification (PDF)](http://www.giis.co.in/Zfs_ondiskformat.pdf)
- [ZFS On-Disk Internals (ahrens)](https://github.com/ahrens/zfsondisk)
- [RFC 4506 — XDR: External Data Representation](https://datatracker.ietf.org/doc/html/rfc4506)
---
## 1. Расположение меток на диске
Каждое устройство содержит **4 копии** метки (label), расположенных так:
```
Начало диска:
[Label 0: 256 KB] offset = 0
[Label 1: 256 KB] offset = 256 KB
[Boot Block: 3.5 MB]
[Данные...]
Конец диска:
[...Данные]
[Label 2: 256 KB] offset = disk_size - 512 KB
[Label 3: 256 KB] offset = disk_size - 256 KB
```
4 копии обеспечивают избыточность: даже при повреждении начала или конца диска
хотя бы одна метка останется читаемой.
---
## 2. Структура метки (vdev_label_t) — 256 КБ
```
Смещение Размер Содержимое
─────────────────────────────────────────────────────────
0x00000 8 КБ Пустое поле (legacy VTOC support)
0x02000 8 КБ Boot Environment Block (vdev_boot_envblock_t)
0x04000 112 КБ NVList (vdev_phys_t) — конфигурация пула
0x20000 128 КБ Uberblock Ring — 128 × 1 КБ uberblocks
─────────────────────────────────────────────────────────
256 КБ ИТОГО
```
### 2.1 Пустое поле (0x00000, 8 КБ)
Зарезервировано для совместимости с VTOC-разметкой дисков (Solaris legacy).
Заполнено нулями.
### 2.2 Boot Environment Block (0x02000, 8 КБ)
Хранит переменные загрузочной среды (bootenv). Используется для загрузки с ZFS.
Последние 40 байт — встроенная контрольная сумма (zio_eck_t).
### 2.3 NVList / vdev_phys_t (0x04000, 112 КБ)
Основная часть метки. Содержит **сериализованный NVList** (Name-Value List) —
полное описание пула и устройства:
```
Поле Тип Описание
─────────────────────────────────────────────────────────
version UINT64 Версия формата пула (обычно 5000)
name STRING Имя пула (например, "ssd2")
state UINT64 Состояние: 0=UNINITIALIZED, 1=ACTIVE, 2=EXPORTED
txg UINT64 Номер группы транзакций (Transaction Group)
pool_guid UINT64 Уникальный идентификатор пула
errata UINT64 Номер известной ошибки (0 = нет)
hostname STRING Имя хоста, создавшего пул
top_guid UINT64 GUID верхнего vdev (зеркала)
guid UINT64 GUID этого устройства
vdev_children UINT64 Количество top-level vdev (обычно 1)
vdev_tree NVLIST Дерево виртуальных устройств (см. ниже)
features_for_read STRING_ARRAY Фичи, нужные для чтения пула
create_txg UINT64 TXG создания пула
```
Последние 40 байт блока vdev_phys_t — контрольная сумма (zio_eck_t).
### 2.4 Uberblock Ring (0x20000, 128 КБ)
Кольцевой буфер из **128 uberblock'ов** по 1 КБ каждый. При каждом коммите
группы транзакций (TXG) записывается новый uberblock в следующую позицию кольца.
```
Позиция в кольце = TXG % 128
```
#### Структура uberblock (1 КБ)
```
Поле Размер Описание
─────────────────────────────────────────────────────────
ub_magic 8 байт Магическое число: 0x00BAB10C (oo-ba-block)
ub_version 8 байт Версия SPA
ub_txg 8 байт Номер TXG
ub_guid_sum 8 байт Сумма GUID всех vdev
ub_timestamp 8 байт UTC timestamp последней синхронизации
ub_rootbp 128 байт Block Pointer на Meta Object Set (MOS)
... ... Дополнительные поля
zio_eck_t 40 байт Встроенная контрольная сумма
```
ZFS при импорте пула находит uberblock с наибольшим TXG и валидной контрольной
суммой — это «текущее» состояние пула.
---
## 3. Дерево виртуальных устройств (vdev_tree)
vdev_tree описывает топологию хранения. Для зеркала:
```
vdev_tree (NVLIST):
type STRING "mirror"
id UINT64 0
guid UINT64 GUID зеркала (= top_guid)
metaslab_array UINT64 Объект MOS с картой аллокаций
metaslab_shift UINT64 Размер метаслэба (log2)
ashift UINT64 Размер сектора (log2, обычно 9=512B или 12=4KB)
asize UINT64 Доступный размер (байт)
is_log UINT64 1 если это SLOG
create_txg UINT64 TXG создания vdev
children NVLIST_ARRAY Массив дочерних устройств:
children[0] (NVLIST):
type STRING "disk"
id UINT64 0
guid UINT64 GUID устройства
path STRING "/dev/disk/by-id/..."
devid STRING "ata-..."
phys_path STRING "pci-0000:18:00.0-ata-2.0"
whole_disk UINT64 1 если ZFS управляет разделами
DTL UINT64 Объект MOS с Dirty Time Log
create_txg UINT64 TXG добавления в пул
children[1] (NVLIST):
... (аналогично)
```
### DTL (Dirty Time Log)
DTL отслеживает диапазоны TXG, для которых устройство может не иметь актуальных
данных. Используется при resilver для определения, какие блоки нужно копировать.
DTL хранится как объект в MOS (Meta Object Set), а поле `DTL` в метке — это
номер этого объекта.
---
## 4. Формат NVList (XDR-кодирование)
NVList — это упорядоченный список пар имя-значение. На диске кодируется в формате
**XDR** (External Data Representation, RFC 4506).
### Ссылки на код
- [nvpair.c — кодирование/декодирование](https://github.com/openzfs/zfs/blob/master/module/nvpair/nvpair.c)
- [nvpair.h — определения типов](https://github.com/openzfs/zfs/blob/master/include/sys/nvpair.h)
### 4.1 Заголовок верхнего уровня (4 байта)
Только для внешнего (top-level) nvlist:
```
Байт 0: encoding — 0x00=native, 0x01=XDR
Байт 1: endianness — 0x00=big-endian host, 0x01=little-endian host
Байт 2-3: reserved (нули)
```
В метках ZFS всегда используется XDR (0x01).
### 4.2 Заголовок nvlist (8 байт)
Присутствует и в top-level, и во вложенных nvlist:
```
nvl_version UINT32 BE Версия формата (0)
nvl_nvflag UINT32 BE Флаги: 1 = NV_UNIQUE_NAME
```
### 4.3 Пара NVPair
```
encoded_size UINT32 BE Полный размер пары в XDR (включая это поле)
decoded_size UINT32 BE Размер в памяти (native формат, 64-bit)
name_length UINT32 BE Длина имени (без null-терминатора)
name N байт Имя, дополненное нулями до 4-байтовой границы
type UINT32 BE Тип данных (см. таблицу)
nelem UINT32 BE Количество элементов (1 для скаляров)
data ... Данные (формат зависит от типа)
```
### 4.4 Терминатор
Конец nvlist обозначается парой нулей:
```
00 00 00 00 encoded_size = 0
00 00 00 00 decoded_size = 0
```
### 4.5 Типы данных
```
Код Имя Размер данных в XDR
─────────────────────────────────────────────────────────
1 BOOLEAN 0 байт (nelem=0)
2 BYTE 4 байта (дополнен до uint32)
3 INT16 4 байта (дополнен до uint32)
4 UINT16 4 байта (дополнен до uint32)
5 INT32 4 байта
6 UINT32 4 байта
7 INT64 8 байт
8 UINT64 8 байт
9 STRING 4 (длина) + строка + padding до 4
10 BYTE_ARRAY N байт + padding
11 INT16_ARRAY N × 4 байта
12 UINT16_ARRAY N × 4 байта
13 INT32_ARRAY N × 4 байта
14 UINT32_ARRAY N × 4 байта
15 INT64_ARRAY N × 8 байт
16 UINT64_ARRAY N × 8 байт
17 STRING_ARRAY N × (4 + строка + padding)
18 HRTIME 8 байт (uint64)
19 NVLIST вложенный nvlist (без top-level заголовка)
20 NVLIST_ARRAY N × вложенных nvlist
21 BOOLEAN_VALUE 4 байта (uint32: 0 или 1)
22 INT8 4 байта (дополнен)
23 UINT8 4 байта (дополнен)
```
### 4.6 Кодирование строк
```
string_length UINT32 BE Длина строки (без null)
string_data N байт Байты строки
padding 0-3 байт Нули до 4-байтовой границы
```
Пример: строка "ssd2"
```
00 00 00 04 длина = 4
73 73 64 32 "ssd2" (уже выровнена)
```
Пример: строка "version"
```
00 00 00 07 длина = 7
76 65 72 73 69 6F 6E 00 "version" + 1 байт padding
```
### 4.7 Кодирование вложенных NVList
Вложенный nvlist (тип 19) кодируется как:
```
nvl_version UINT32 BE
nvl_nvflag UINT32 BE
nvpair_1 ...
nvpair_2 ...
...
terminator UINT32(0) + UINT32(0)
```
Без 4-байтового top-level заголовка (encoding/endian).
### 4.8 Вычисление decoded_size
`decoded_size` — размер nvpair в памяти на 64-битной системе:
```
decoded_size = ALIGN8(sizeof(nvpair_t) + name_len + 1) + ALIGN8(data_size)
```
Где:
- `sizeof(nvpair_t)` = 16 байт на 64-bit
- `name_len + 1` — имя с null-терминатором
- `ALIGN8(x)` = (x + 7) & ~7
- `data_size` зависит от типа:
- Скаляры: размер типа (4 или 8 байт)
- Строки: strlen + 1
- NVLIST: 8 (указатель)
- NVLIST_ARRAY: N × 8 (массив указателей)
### 4.9 Пример: разбор реальной метки
Байты начала nvlist на диске (смещение 0x4000 в метке):
```
01 01 00 00 header: XDR, LE host
00 00 00 00 nvl_version = 0
00 00 00 01 nvl_nvflag = 1 (NV_UNIQUE_NAME)
00 00 00 24 encoded_size = 36
00 00 00 20 decoded_size = 32
00 00 00 07 name_length = 7
76 65 72 73 69 6F 6E 00 "version" + pad
00 00 00 08 type = 8 (UINT64)
00 00 00 01 nelem = 1
00 00 00 00 00 00 13 88 value = 5000
00 00 00 20 encoded_size = 32
00 00 00 20 decoded_size = 32
00 00 00 04 name_length = 4
6E 61 6D 65 "name"
00 00 00 09 type = 9 (STRING)
00 00 00 01 nelem = 1
00 00 00 04 string_length = 4
73 73 64 32 "ssd2"
...
```
---
## 5. Контрольная сумма (Fletcher-4 + zio_eck_t)
### Ссылки на код
- [zfs_fletcher.c](https://github.com/openzfs/zfs/blob/master/module/zcommon/zfs_fletcher.c)
- [zio_checksum.c](https://github.com/openzfs/zfs/blob/master/module/zfs/zio_checksum.c)
- [zio.h — определение zio_eck_t](https://github.com/openzfs/zfs/blob/master/include/sys/zio.h)
### 5.1 Встроенная контрольная сумма (zio_eck_t)
Последние 40 байт каждого блока метки содержат:
```
struct zio_eck {
uint64_t zec_magic; // 8 байт: магическое число
zio_cksum_t zec_cksum; // 32 байта: контрольная сумма (4 × uint64)
};
```
```
zec_magic = 0x0210da7ab10c7a11 (в native byte order)
```
Магическое число используется для определения порядка байтов (endianness):
- Если `zec_magic == 0x0210da7ab10c7a11` → native endian
- Если `zec_magic == BSWAP_64(0x0210da7ab10c7a11)` → byte-swapped
### 5.2 Расположение zio_eck_t
Для vdev_phys_t (112 КБ):
```
zio_eck_t находится на смещении: 112*1024 - 40 = 114648 = 0x1BFD8
(относительно начала vdev_phys_t)
```
Абсолютное смещение в метке: `0x4000 + 0x1BFD8 = 0x1FFD8`
### 5.3 Алгоритм Fletcher-4
Fletcher-4 обрабатывает данные как массив 32-битных слов и вычисляет
4 накопительных суммы:
```python
def fletcher4(data):
a = b = c = d = 0
for каждого 32-битного слова w в data:
a += w # mod 2^64
b += a # mod 2^64
c += b # mod 2^64
d += c # mod 2^64
return (a, b, c, d)
```
Результат — 256-битная контрольная сумма (4 × 64-bit).
### 5.4 Порядок байтов
- **fletcher_4_native**: обрабатывает 32-битные слова в native byte order
(little-endian на x86)
- **fletcher_4_byteswap**: обрабатывает слова в обратном порядке байтов
Для меток, записанных на x86 системе, используется native (LE).
### 5.5 Алгоритм вычисления контрольной суммы метки
**Запись:**
1. Подготовить блок vdev_phys_t (112 КБ) с nvlist данными
2. Установить `zec_magic = 0x0210da7ab10c7a11` (native LE)
3. Обнулить `zec_cksum` (32 байта нулей)
4. Вычислить Fletcher-4 по всему блоку (112 КБ)
5. Записать результат в `zec_cksum`
**Проверка:**
1. Прочитать блок vdev_phys_t (112 КБ)
2. Сохранить `zec_cksum`
3. Установить `zec_magic = 0x0210da7ab10c7a11`
4. Обнулить `zec_cksum`
5. Вычислить Fletcher-4 по всему блоку
6. Сравнить с сохранённой суммой
---
## 6. Операции с метками
### 6.1 zpool detach — что происходит
При `zpool detach` ZFS вызывает `vdev_label_init()` с флагом `VDEV_LABEL_REMOVE`:
1. Обнуляет **nvlist** во всех 4 метках (vdev_phys_t заполняется нулями)
2. Обнуляет **uberblock ring** во всех 4 метках
3. Перезаписывает контрольные суммы
**Но**: данные на диске остаются нетронутыми! Стираются только метки.
### 6.2 zhack label repair -u
Восстанавливает uberblock на отключённом устройстве:
1. Читает nvlist из метки (если он сохранился)
2. Ищет максимальный валидный uberblock в кольце
3. Пересчитывает контрольные суммы
**Ограничение**: если nvlist полностью обнулён (как после detach),
`zhack label repair -u` не сможет восстановить — ему нужен хотя бы nvlist.
**Наблюдение**: на практике `zpool detach` может обнулить не весь nvlist.
В нашем случае nvlist на QVO сохранился полностью, а обнулились только uberblock'и.
Это зависит от версии ZFS и конкретных условий.
### 6.3 zpool offline vs zpool detach
- `zpool offline` — помечает устройство как offline, **сохраняет метки**.
При `zpool online` делает resilver только дельты (по DTL).
- `zpool detach` — полностью удаляет устройство из пула, **стирает метки**.
Возврат возможен только через `zpool attach` (полный resilver).
- `zpool split` — отделяет зеркало в новый пул, **сохраняет метки**
с новыми GUID. Предпочтительнее detach для временного отделения.
### 6.4 Что делает наш инструмент
1. **Читает** nvlist из метки (XDR → Python структура)
2. **Модифицирует** vdev_tree — добавляет child в массив children зеркала
3. **Кодирует** обратно (Python → XDR)
4. **Вычисляет** Fletcher-4 контрольную сумму
5. **Записывает** во все 4 копии метки
---
## 7. Resilver и TXG
### 7.1 Как работает resilver
ZFS resilver — НЕ побитовое копирование диска. Это обход дерева метаданных:
1. **Фаза сканирования** (scan): ZFS обходит всё дерево блоков от uberblock
вниз, определяя какие блоки нужно скопировать
2. **Фаза записи** (issue): копирует найденные блоки с исправного устройства
на новое/восстановленное
ZFS определяет что копировать по **TXG** (Transaction Group Number):
- Каждый блок помечен номером TXG, в котором он был записан
- Если устройство было offline/detached, ZFS знает его последний TXG
- Копируются только блоки с TXG > последнего известного для устройства
### 7.2 DTL (Dirty Time Log)
DTL хранит диапазоны TXG, для которых устройство НЕ имеет актуальных данных.
При resilver ZFS проверяет DTL и копирует только «грязные» блоки.
DTL хранится как объект в MOS (Meta Object Set). Поле `DTL` в метке —
номер этого объекта.
### 7.3 Ключевые параметры ZFS resilver
```
zfs_scan_ignore_errors — пропускать ошибки чтения (не зависать)
zfs_resilver_min_time_ms — минимальное время на группу скана
zfs_scan_vdev_limit — размер буфера I/O для скана
zfs_scan_suspend_progress — приостановить скан (1) / возобновить (0)
```
Установка через sysfs:
```bash
echo 1 > /sys/module/zfs/parameters/zfs_scan_ignore_errors
```
Постоянная установка через modprobe:
```bash
echo "options zfs zfs_scan_ignore_errors=1" >> /etc/modprobe.d/zfs.conf
```
---
## 8. Формат данных на диске: зеркало
В зеркале (mirror) данные на всех дисках **побайтово идентичны** на уровне
блоков ZFS. Различаются только **метки** (labels) — у каждого диска свой
vdev_guid, path, devid.
Это означает:
- `dd` с одного диска зеркала на другой даёт рабочую копию (но с чужими метками)
- Для корректной работы нужно обновить метки на копии
---
## 9. Инструменты ZFS для работы с метками
| Инструмент | Что делает |
|------------|-----------|
| `zdb -l /dev/sdX` | Читает и показывает все 4 метки |
| `zdb -lu /dev/sdX` | То же + uberblock'и |
| `zhack label repair -c /dev/sdX` | Восстанавливает контрольные суммы меток |
| `zhack label repair -u /dev/sdX` | Восстанавливает uberblock на detached диске |
| `zpool labelclear /dev/sdX` | Стирает метки (для повторного использования диска) |
| `zfs_label_tool.py` | Модификация nvlist в метках (этот инструмент) |
---
## 10. Известные проблемы OpenZFS
### Зависание при сбойном диске
При resilver/scrub, если диск не отвечает, ZFS бесконечно ждёт I/O в
`txg_sync`. Нет таймаута. Всё что работает с пулом (export, scrub -s,
zpool status) также зависает.
**Обходные пути:**
- `zfs_scan_ignore_errors=1` — пропускать ошибки
- Сброс SCSI-устройства через sysfs
- Жёсткая перезагрузка (`echo b > /proc/sysrq-trigger`)
### Полный resilver после detach
После `zpool detach` нет способа вернуть диск с частичным resilver.
`zpool attach` всегда запускает полный resilver.
**Feature request:** [openzfs/zfs#16984](https://github.com/openzfs/zfs/issues/16984)
### Нет инструмента для правки vdev_tree
`zhack` не умеет модифицировать vdev_tree в метках (добавлять/удалять устройства).
**Feature request:** [openzfs/zfs#2510](https://github.com/openzfs/zfs/issues/2510)
Именно эту проблему решает `zfs_label_tool.py`.
#!/bin/bash
# Test ZFS mirror label editing tool
# Scenario: 2-disk mirror → detach disk1 → write data → add disk3 →
# restore disk1 → get 3-way mirror with all data intact
#
# Run as root: bash test_zfs_mirror.sh
set -e
TOOL="$(cd "$(dirname "$0")" && pwd)/zfs_label_tool.py"
DIR="/tmp/zfs_label_test"
POOL="testlabeltool"
cleanup() {
echo "=== Cleanup ==="
zpool destroy "$POOL" 2>/dev/null || true
rm -rf "$DIR"
}
die() { echo "FAIL: $1" >&2; exit 1; }
# Clean up before start
cleanup
mkdir -p "$DIR"
echo "============================================================"
echo "Step 1: Create 2 files (256MB each) and a mirror"
echo "============================================================"
truncate -s 256M "$DIR/disk1"
truncate -s 256M "$DIR/disk2"
zpool create -f "$POOL" mirror "$DIR/disk1" "$DIR/disk2"
zpool status "$POOL"
echo ""
echo "============================================================"
echo "Step 2: Write initial data"
echo "============================================================"
echo "Initial data from 2-way mirror" > "/$POOL/testfile1"
dd if=/dev/urandom of="/$POOL/random1" bs=1M count=5 2>/dev/null
md5sum "/$POOL/random1" > "$DIR/md5_random1"
zpool sync "$POOL"
echo "Written: testfile1, random1"
echo ""
echo "============================================================"
echo "Step 3: Verify tool can parse and round-trip labels"
echo "============================================================"
python3 "$TOOL" verify "$DIR/disk1"
python3 "$TOOL" verify "$DIR/disk2"
echo ""
echo "============================================================"
echo "Step 4: Detach disk1"
echo "============================================================"
zpool detach "$POOL" "$DIR/disk1"
echo "disk1 detached"
zpool status "$POOL"
echo ""
echo "============================================================"
echo "Step 5: Write more data (disk1 is now behind)"
echo "============================================================"
echo "Data written after disk1 detach" > "/$POOL/testfile2"
dd if=/dev/urandom of="/$POOL/random2" bs=1M count=5 2>/dev/null
md5sum "/$POOL/random2" > "$DIR/md5_random2"
zpool sync "$POOL"
echo "Written: testfile2, random2"
echo ""
echo "============================================================"
echo "Step 6: Create disk3 and attach to mirror"
echo "============================================================"
truncate -s 256M "$DIR/disk3"
zpool attach "$POOL" "$DIR/disk2" "$DIR/disk3"
echo "disk3 attached, waiting for resilver..."
sleep 3
zpool wait "$POOL" 2>/dev/null || sleep 5
zpool status "$POOL"
echo ""
echo "============================================================"
echo "Step 7: Write even more data (disk1 is further behind)"
echo "============================================================"
echo "Data written with 3 disks (but disk1 detached)" > "/$POOL/testfile3"
zpool sync "$POOL"
echo ""
echo "============================================================"
echo "Step 8: Show disk1 labels (should be wiped after detach)"
echo "============================================================"
echo "--- disk1 label 0 ---"
python3 "$TOOL" show "$DIR/disk1" 2>&1 | head -5
echo ""
echo "--- Restoring uberblock with zhack ---"
zhack label repair -u "$DIR/disk1"
echo ""
echo "--- disk1 after zhack ---"
python3 "$TOOL" show "$DIR/disk1" 2>&1 | head -20
echo ""
echo "============================================================"
echo "Step 9: Export pool"
echo "============================================================"
zpool export "$POOL"
echo "Pool exported"
echo ""
echo "============================================================"
echo "Step 10: Verify round-trip on all disks"
echo "============================================================"
python3 "$TOOL" verify "$DIR/disk2"
python3 "$TOOL" verify "$DIR/disk3"
python3 "$TOOL" verify "$DIR/disk1"
echo ""
echo "============================================================"
echo "Step 11: Add disk1 back to disk2's labels"
echo "============================================================"
python3 "$TOOL" add-child "$DIR/disk2" "$DIR/disk1"
echo ""
echo "============================================================"
echo "Step 12: Add disk1 back to disk3's labels"
echo "============================================================"
python3 "$TOOL" add-child "$DIR/disk3" "$DIR/disk1"
echo ""
echo "============================================================"
echo "Step 13: Sync labels to disk1 (copy config from disk2)"
echo "============================================================"
python3 "$TOOL" sync-labels "$DIR/disk2" "$DIR/disk1"
echo ""
echo "============================================================"
echo "Step 14: Verify all labels after modification"
echo "============================================================"
python3 "$TOOL" verify "$DIR/disk2"
python3 "$TOOL" verify "$DIR/disk3"
python3 "$TOOL" verify "$DIR/disk1"
echo ""
echo "============================================================"
echo "Step 15: Show final label config (disk2)"
echo "============================================================"
python3 "$TOOL" show "$DIR/disk2" 2>&1 | head -60
echo ""
echo "============================================================"
echo "Step 16: Import pool"
echo "============================================================"
zpool import -d "$DIR" "$POOL" && echo "Import successful!" || die "Import failed!"
zpool status "$POOL"
echo ""
echo "============================================================"
echo "Step 17: Verify data integrity"
echo "============================================================"
echo "--- testfile1 ---"
cat "/$POOL/testfile1" || die "testfile1 missing"
echo "--- testfile2 ---"
cat "/$POOL/testfile2" || die "testfile2 missing"
echo "--- testfile3 ---"
cat "/$POOL/testfile3" || die "testfile3 missing"
echo "--- checksum random1 ---"
md5sum -c "$DIR/md5_random1" || die "random1 checksum mismatch"
echo "--- checksum random2 ---"
md5sum -c "$DIR/md5_random2" || die "random2 checksum mismatch"
echo ""
echo "============================================================"
echo "Step 18: Wait for resilver and final check"
echo "============================================================"
sleep 5
zpool status "$POOL"
echo ""
echo "============================================================"
echo "ALL TESTS PASSED!"
echo "============================================================"
# Cleanup
cleanup
#!/usr/bin/env python3
"""
zfs_label_tool.py - ZFS vdev label editor.
Reads, parses, modifies, and writes ZFS vdev labels.
Allows adding a device back to a mirror by modifying nvlist in labels.
Commands:
show <device> - Display label contents
verify <device> - Verify label checksums
add-child <device> <child_device> - Add child_device to device's mirror
sync-labels <source> <target> - Copy vdev_tree from source to target
"""
import struct
import sys
import os
import copy
import argparse
# ========== Constants ==========
LABEL_SIZE = 256 * 1024 # 256 KB per label
VDEV_PHYS_OFFSET = 16 * 1024 # 16 KB (skip blank + boot env)
VDEV_PHYS_SIZE = 112 * 1024 # 112 KB
UBERBLOCK_OFFSET = VDEV_PHYS_OFFSET + VDEV_PHYS_SIZE
UBERBLOCK_SIZE = 1024 # 1 KB per uberblock
NUM_UBERBLOCKS = 128
ZEC_MAGIC = 0x0210da7ab10c7a11
ZEC_SIZE = 40 # 8 (magic) + 32 (checksum)
# NVList data types
DT_BOOLEAN = 1
DT_BYTE = 2
DT_INT16 = 3
DT_UINT16 = 4
DT_INT32 = 5
DT_UINT32 = 6
DT_INT64 = 7
DT_UINT64 = 8
DT_STRING = 9
DT_BYTE_ARRAY = 10
DT_INT16_ARRAY = 11
DT_UINT16_ARRAY = 12
DT_INT32_ARRAY = 13
DT_UINT32_ARRAY = 14
DT_INT64_ARRAY = 15
DT_UINT64_ARRAY = 16
DT_STRING_ARRAY = 17
DT_HRTIME = 18
DT_NVLIST = 19
DT_NVLIST_ARRAY = 20
DT_BOOLEAN_VALUE = 21
DT_INT8 = 22
DT_UINT8 = 23
TYPE_NAMES = {
DT_BOOLEAN: 'BOOLEAN', DT_BYTE: 'BYTE',
DT_INT16: 'INT16', DT_UINT16: 'UINT16',
DT_INT32: 'INT32', DT_UINT32: 'UINT32',
DT_INT64: 'INT64', DT_UINT64: 'UINT64',
DT_STRING: 'STRING', DT_BYTE_ARRAY: 'BYTE_ARRAY',
DT_INT16_ARRAY: 'INT16_ARRAY', DT_UINT16_ARRAY: 'UINT16_ARRAY',
DT_INT32_ARRAY: 'INT32_ARRAY', DT_UINT32_ARRAY: 'UINT32_ARRAY',
DT_INT64_ARRAY: 'INT64_ARRAY', DT_UINT64_ARRAY: 'UINT64_ARRAY',
DT_STRING_ARRAY: 'STRING_ARRAY', DT_HRTIME: 'HRTIME',
DT_NVLIST: 'NVLIST', DT_NVLIST_ARRAY: 'NVLIST_ARRAY',
DT_BOOLEAN_VALUE: 'BOOLEAN_VALUE',
DT_INT8: 'INT8', DT_UINT8: 'UINT8',
}
# ========== Helper Functions ==========
def align4(n):
"""Round up to 4-byte boundary."""
return (n + 3) & ~3
def align8(n):
"""Round up to 8-byte boundary."""
return (n + 7) & ~7
def get_device_size(path):
"""Get size of a file or block device."""
if os.path.isfile(path):
return os.path.getsize(path)
# Block device: seek to end
with open(path, 'rb') as fd:
fd.seek(0, 2)
return fd.tell()
def label_offsets(disk_size):
"""Return byte offsets of all 4 labels."""
return [
0, # L0
LABEL_SIZE, # L1
disk_size - 2 * LABEL_SIZE, # L2
disk_size - LABEL_SIZE, # L3
]
# ========== NVList Data Structures ==========
class NVPair:
"""A single name-value pair."""
def __init__(self, name, dtype, value, orig_decoded_size=None):
self.name = name
self.dtype = dtype
self.value = value
self.orig_decoded_size = orig_decoded_size # preserve from parsing
def __repr__(self):
return f"NVPair({self.name!r}, {TYPE_NAMES.get(self.dtype, self.dtype)}, {self.value!r})"
class NVList:
"""Ordered collection of NVPairs."""
def __init__(self, version=0, flags=1):
self.version = version
self.flags = flags
self.pairs = []
def get(self, name):
"""Get value by name."""
for p in self.pairs:
if p.name == name:
return p.value
return None
def get_pair(self, name):
"""Get NVPair object by name."""
for p in self.pairs:
if p.name == name:
return p
return None
def set(self, name, dtype, value):
"""Set or update a pair. Clears orig_decoded_size on change."""
for p in self.pairs:
if p.name == name:
p.dtype = dtype
p.value = value
p.orig_decoded_size = None # force recomputation
return
self.pairs.append(NVPair(name, dtype, value))
def __repr__(self):
return f"NVList(v={self.version}, f={self.flags}, pairs={self.pairs})"
# ========== NVList XDR Parser ==========
class NVListParser:
"""Parse XDR-encoded NVList from bytes."""
def __init__(self, data):
self.data = data
self.offset = 0
def read_uint32(self):
val = struct.unpack_from('>I', self.data, self.offset)[0]
self.offset += 4
return val
def read_uint64(self):
val = struct.unpack_from('>Q', self.data, self.offset)[0]
self.offset += 8
return val
def read_int64(self):
val = struct.unpack_from('>q', self.data, self.offset)[0]
self.offset += 8
return val
def read_bytes(self, n):
val = self.data[self.offset:self.offset + n]
self.offset += n
return val
def read_padded_string(self, length):
"""Read string of given length, advance past 4-byte aligned data."""
s = self.data[self.offset:self.offset + length]
self.offset += align4(length)
return s.decode('utf-8', errors='replace')
def parse_nvlist(self, top_level=True):
"""Parse a complete nvlist."""
if top_level:
# 4-byte header: encoding(1) + endianness(1) + reserved(2)
header = self.read_uint32()
encoding = (header >> 24) & 0xFF
endian = (header >> 16) & 0xFF
if encoding != 1:
raise ValueError(f"Expected XDR encoding (1), got {encoding}")
version = self.read_uint32()
flags = self.read_uint32()
nvl = NVList(version, flags)
while True:
pair_start = self.offset
encoded_size = self.read_uint32()
decoded_size = self.read_uint32()
if encoded_size == 0 and decoded_size == 0:
break # terminator
# Read name
name_len = self.read_uint32()
name = self.read_padded_string(name_len)
# Read type and nelem
dtype = self.read_uint32()
nelem = self.read_uint32()
# Read value
value = self.parse_value(dtype, nelem)
nvl.pairs.append(NVPair(name, dtype, value, decoded_size))
# Ensure we're at the right position for next pair
self.offset = pair_start + encoded_size
return nvl
def parse_value(self, dtype, nelem):
"""Parse a value based on its type."""
if dtype == DT_BOOLEAN:
return True
elif dtype == DT_BOOLEAN_VALUE:
return self.read_uint32() != 0
elif dtype == DT_BYTE:
val = struct.unpack_from('>B', self.data, self.offset)[0]
self.offset += 4 # XDR pads to 4
return val
elif dtype == DT_INT8:
val = struct.unpack_from('>b', self.data, self.offset)[0]
self.offset += 4
return val
elif dtype == DT_UINT8:
val = struct.unpack_from('>B', self.data, self.offset)[0]
self.offset += 4
return val
elif dtype == DT_INT16:
val = struct.unpack_from('>h', self.data, self.offset)[0]
self.offset += 4
return val
elif dtype == DT_UINT16:
val = struct.unpack_from('>H', self.data, self.offset)[0]
self.offset += 4
return val
elif dtype == DT_INT32:
return self.read_uint32()
elif dtype == DT_UINT32:
return self.read_uint32()
elif dtype == DT_INT64:
return self.read_int64()
elif dtype == DT_UINT64:
return self.read_uint64()
elif dtype == DT_HRTIME:
return self.read_uint64()
elif dtype == DT_STRING:
slen = self.read_uint32()
return self.read_padded_string(slen)
elif dtype == DT_NVLIST:
return self.parse_nvlist(top_level=False)
elif dtype == DT_NVLIST_ARRAY:
result = []
for _ in range(nelem):
result.append(self.parse_nvlist(top_level=False))
return result
elif dtype == DT_UINT64_ARRAY:
return [self.read_uint64() for _ in range(nelem)]
elif dtype == DT_UINT32_ARRAY:
return [self.read_uint32() for _ in range(nelem)]
elif dtype == DT_INT64_ARRAY:
return [self.read_int64() for _ in range(nelem)]
elif dtype == DT_STRING_ARRAY:
result = []
for _ in range(nelem):
slen = self.read_uint32()
result.append(self.read_padded_string(slen))
return result
elif dtype == DT_BYTE_ARRAY:
data = self.read_bytes(nelem)
self.offset += (align4(nelem) - nelem) # padding
return data
else:
raise ValueError(f"Unsupported data type: {dtype}")
# ========== NVList XDR Encoder ==========
class NVListEncoder:
"""Encode NVList to XDR bytes."""
def __init__(self):
self.buf = bytearray()
def write_uint32(self, val):
self.buf += struct.pack('>I', val)
def write_uint64(self, val):
self.buf += struct.pack('>Q', val)
def write_int64(self, val):
self.buf += struct.pack('>q', val)
def write_padded(self, data, length):
"""Write data and pad to 4-byte boundary."""
self.buf += data[:length]
pad = align4(length) - length
if pad:
self.buf += b'\x00' * pad
def encode_nvlist(self, nvl, top_level=True):
"""Encode a complete nvlist."""
if top_level:
# Header: encoding=XDR(1), endian=LE(1), reserved(0,0)
self.write_uint32(0x01010000)
self.write_uint32(nvl.version)
self.write_uint32(nvl.flags)
for pair in nvl.pairs:
self.encode_nvpair(pair)
# Terminator
self.write_uint32(0)
self.write_uint32(0)
def encode_nvpair(self, pair):
"""Encode a single nvpair."""
# Build the pair content first to calculate sizes
content = NVListEncoder()
# Name
name_bytes = pair.name.encode('utf-8')
content.write_uint32(len(name_bytes))
content.write_padded(name_bytes, len(name_bytes))
# Type and nelem
content.write_uint32(pair.dtype)
nelem = self.encode_value(content, pair.dtype, pair.value)
# Calculate sizes
encoded_size = 8 + len(content.buf) # 8 for enc_size + dec_size
# Use original decoded_size if available, otherwise compute
if pair.orig_decoded_size is not None:
decoded_size = pair.orig_decoded_size
else:
decoded_size = self.calc_decoded_size(
pair.name, pair.dtype, pair.value, nelem)
# Write to main buffer
self.write_uint32(encoded_size)
self.write_uint32(decoded_size)
self.buf += content.buf
def encode_value(self, enc, dtype, value):
"""Encode a value. Returns nelem."""
if dtype == DT_BOOLEAN:
enc.write_uint32(0) # nelem = 0
return 0
elif dtype == DT_BOOLEAN_VALUE:
enc.write_uint32(1) # nelem
enc.write_uint32(1 if value else 0)
return 1
elif dtype in (DT_BYTE, DT_UINT8, DT_INT8):
enc.write_uint32(1)
enc.write_uint32(value & 0xFF)
return 1
elif dtype in (DT_UINT16, DT_INT16):
enc.write_uint32(1)
enc.write_uint32(value & 0xFFFF)
return 1
elif dtype in (DT_UINT32, DT_INT32):
enc.write_uint32(1)
enc.write_uint32(value)
return 1
elif dtype in (DT_UINT64, DT_HRTIME):
enc.write_uint32(1)
enc.write_uint64(value)
return 1
elif dtype == DT_INT64:
enc.write_uint32(1)
enc.write_int64(value)
return 1
elif dtype == DT_STRING:
enc.write_uint32(1) # nelem
s = value.encode('utf-8')
enc.write_uint32(len(s))
enc.write_padded(s, len(s))
return 1
elif dtype == DT_NVLIST:
enc.write_uint32(1) # nelem
enc.encode_nvlist(value, top_level=False)
return 1
elif dtype == DT_NVLIST_ARRAY:
enc.write_uint32(len(value)) # nelem
for nvl in value:
enc.encode_nvlist(nvl, top_level=False)
return len(value)
elif dtype == DT_UINT64_ARRAY:
enc.write_uint32(len(value))
for v in value:
enc.write_uint64(v)
return len(value)
elif dtype == DT_UINT32_ARRAY:
enc.write_uint32(len(value))
for v in value:
enc.write_uint32(v)
return len(value)
elif dtype == DT_INT64_ARRAY:
enc.write_uint32(len(value))
for v in value:
enc.write_int64(v)
return len(value)
elif dtype == DT_STRING_ARRAY:
enc.write_uint32(len(value))
for s in value:
sb = s.encode('utf-8')
enc.write_uint32(len(sb))
enc.write_padded(sb, len(sb))
return len(value)
elif dtype == DT_BYTE_ARRAY:
enc.write_uint32(len(value))
enc.write_padded(value, len(value))
return len(value)
else:
raise ValueError(f"Unsupported dtype for encoding: {dtype}")
@staticmethod
def calc_decoded_size(name, dtype, value, nelem):
"""Calculate in-memory (decoded) size on 64-bit system.
decoded_size = NV_ALIGN8(sizeof(nvpair_t) + name_sz) + NV_ALIGN8(data_sz)
sizeof(nvpair_t) = 16 on 64-bit
sizeof(nvlist_t) = 24 on 64-bit
"""
NVP_HEADER = 16 # sizeof(nvpair_t)
SIZEOF_NVLIST = 24 # sizeof(nvlist_t)
name_sz = len(name.encode('utf-8')) + 1 # include null
if dtype in (DT_UINT64, DT_INT64, DT_HRTIME):
data_sz = 8
elif dtype in (DT_UINT32, DT_INT32):
data_sz = 4
elif dtype in (DT_UINT16, DT_INT16):
data_sz = 2
elif dtype in (DT_UINT8, DT_INT8, DT_BYTE):
data_sz = 1
elif dtype == DT_BOOLEAN:
data_sz = 0
elif dtype == DT_BOOLEAN_VALUE:
data_sz = 4
elif dtype == DT_STRING:
data_sz = len(value.encode('utf-8')) + 1
elif dtype == DT_NVLIST:
data_sz = SIZEOF_NVLIST # embedded nvlist_t struct
elif dtype == DT_NVLIST_ARRAY:
# pointer array (nelem * 8) + embedded nvlist_t structs
data_sz = nelem * (8 + SIZEOF_NVLIST)
elif dtype == DT_UINT64_ARRAY:
data_sz = nelem * 8
elif dtype == DT_UINT32_ARRAY:
data_sz = nelem * 4
elif dtype == DT_INT64_ARRAY:
data_sz = nelem * 8
elif dtype == DT_STRING_ARRAY:
data_sz = nelem * 8 # pointers
for s in value:
data_sz += len(s.encode('utf-8')) + 1
elif dtype == DT_BYTE_ARRAY:
data_sz = nelem
else:
data_sz = 8 # safe default
return align8(NVP_HEADER + name_sz) + align8(data_sz)
# ========== Checksum ==========
def sha256_checksum(data):
"""Compute SHA-256 checksum for ZFS label (ZIO_CHECKSUM_LABEL).
ZFS labels use SHA-256, not Fletcher-4.
Result is stored as 4 x uint64_t (LE) in zio_cksum_t.
"""
import hashlib
digest = hashlib.sha256(data).digest()
# SHA-256 produces 32 bytes; interpret as 4 little-endian uint64
return struct.unpack_from('<4Q', digest)
# ========== Label Read/Write ==========
def read_vdev_phys(fd, label_num, disk_size):
"""Read raw vdev_phys data from a label."""
offsets = label_offsets(disk_size)
phys_start = offsets[label_num] + VDEV_PHYS_OFFSET
fd.seek(phys_start)
return bytearray(fd.read(VDEV_PHYS_SIZE))
def verify_checksum(phys_data):
"""Verify Fletcher-4 checksum of vdev_phys block."""
eck_offset = VDEV_PHYS_SIZE - ZEC_SIZE
# Read stored magic and checksum
magic = struct.unpack_from('<Q', phys_data, eck_offset)[0]
stored = struct.unpack_from('<4Q', phys_data, eck_offset + 8)
# Prepare verification buffer
verify = bytearray(phys_data)
struct.pack_into('<Q', verify, eck_offset, ZEC_MAGIC)
struct.pack_into('<4Q', verify, eck_offset + 8, 0, 0, 0, 0)
computed = sha256_checksum(bytes(verify))
return computed == stored, magic, stored, computed
def parse_label(phys_data):
"""Parse nvlist from vdev_phys data."""
parser = NVListParser(bytes(phys_data))
return parser.parse_nvlist(top_level=True)
def encode_and_write_label(fd, label_num, disk_size, nvl):
"""Encode nvlist and write to label (without checksum).
Checksum must be fixed afterwards with: zhack label repair -c <device>
"""
# Encode nvlist
encoder = NVListEncoder()
encoder.encode_nvlist(nvl, top_level=True)
nvlist_bytes = bytes(encoder.buf)
max_nvlist = VDEV_PHYS_SIZE - ZEC_SIZE
if len(nvlist_bytes) > max_nvlist:
raise ValueError(
f"Encoded nvlist too large: {len(nvlist_bytes)} > {max_nvlist}")
# Build vdev_phys block
phys = bytearray(VDEV_PHYS_SIZE)
phys[:len(nvlist_bytes)] = nvlist_bytes
# Set checksum magic, zero checksum (will be fixed by zhack)
eck_offset = VDEV_PHYS_SIZE - ZEC_SIZE
struct.pack_into('<Q', phys, eck_offset, ZEC_MAGIC)
struct.pack_into('<4Q', phys, eck_offset + 8, 0, 0, 0, 0)
# Write to disk
offsets = label_offsets(disk_size)
phys_start = offsets[label_num] + VDEV_PHYS_OFFSET
fd.seek(phys_start)
fd.write(phys)
fd.flush()
def fix_checksum(device):
"""Fix label checksums using zhack."""
import subprocess
result = subprocess.run(
['zhack', 'label', 'repair', '-c', device],
capture_output=True, text=True)
if result.returncode != 0:
print(f"Warning: zhack failed: {result.stderr}")
return False
return True
# ========== Display ==========
def print_nvlist(nvl, indent=0):
"""Pretty-print an NVList."""
prefix = ' ' * indent
for pair in nvl.pairs:
tname = TYPE_NAMES.get(pair.dtype, f'type{pair.dtype}')
if pair.dtype == DT_NVLIST:
print(f"{prefix}{pair.name}:")
print_nvlist(pair.value, indent + 1)
elif pair.dtype == DT_NVLIST_ARRAY:
for i, child in enumerate(pair.value):
print(f"{prefix}{pair.name}[{i}]:")
print_nvlist(child, indent + 1)
elif pair.dtype == DT_STRING:
print(f"{prefix}{pair.name}: '{pair.value}'")
elif pair.dtype == DT_UINT64:
print(f"{prefix}{pair.name}: {pair.value}")
elif pair.dtype == DT_UINT32:
print(f"{prefix}{pair.name}: {pair.value}")
elif pair.dtype == DT_BOOLEAN:
print(f"{prefix}{pair.name}: (boolean)")
elif pair.dtype == DT_BOOLEAN_VALUE:
print(f"{prefix}{pair.name}: {pair.value}")
elif isinstance(pair.value, list):
print(f"{prefix}{pair.name}: {pair.value} ({tname})")
else:
print(f"{prefix}{pair.name}: {pair.value} ({tname})")
# ========== Commands ==========
def cmd_show(device):
"""Show label contents."""
disk_size = get_device_size(device)
print(f"Device: {device} ({disk_size} bytes)")
with open(device, 'rb') as fd:
for i in range(4):
try:
phys_data = read_vdev_phys(fd, i, disk_size)
ok, magic, stored, computed = verify_checksum(phys_data)
nvl = parse_label(phys_data)
print(f"\n{'='*60}")
print(f"LABEL {i} (checksum: {'OK' if ok else 'FAILED'})")
print(f"{'='*60}")
print_nvlist(nvl)
except Exception as e:
print(f"\nLABEL {i}: ERROR: {e}")
def cmd_verify(device):
"""Verify label checksums and test round-trip encode/decode."""
disk_size = get_device_size(device)
with open(device, 'rb') as fd:
for i in range(4):
try:
phys_data = read_vdev_phys(fd, i, disk_size)
ok, magic, stored, computed = verify_checksum(phys_data)
# Test round-trip: parse and re-encode
nvl = parse_label(phys_data)
encoder = NVListEncoder()
encoder.encode_nvlist(nvl, top_level=True)
reencoded = bytes(encoder.buf)
# Compare with original (up to nvlist length)
orig_nvlist = bytes(phys_data[:len(reencoded)])
match = (reencoded == orig_nvlist)
print(f"Label {i}: checksum {'OK' if ok else 'FAILED'}, "
f"round-trip {'OK' if match else 'MISMATCH'} "
f"(orig={len(orig_nvlist)}, re={len(reencoded)})")
if not match:
# Find first difference
for j in range(min(len(reencoded), len(orig_nvlist))):
if reencoded[j] != orig_nvlist[j]:
print(f" First diff at byte {j} (0x{j:x}): "
f"orig=0x{orig_nvlist[j]:02x} "
f"re=0x{reencoded[j]:02x}")
break
except Exception as e:
print(f"Label {i}: ERROR: {e}")
import traceback
traceback.print_exc()
def cmd_add_child(target_device, child_device, dry_run=False):
"""Add child_device to target_device's mirror vdev_tree.
Reads child's label to get its guid/path/devid info,
then adds it as a new child in target's mirror.
"""
target_size = get_device_size(target_device)
child_size = get_device_size(child_device)
# Read target label (label 0)
with open(target_device, 'rb') as fd:
target_phys = read_vdev_phys(fd, 0, target_size)
target_nvl = parse_label(target_phys)
# Read child label (label 0)
with open(child_device, 'rb') as fd:
child_phys = read_vdev_phys(fd, 0, child_size)
child_nvl = parse_label(child_phys)
# Verify target has a mirror
vdev_tree = target_nvl.get('vdev_tree')
if not vdev_tree:
print("Error: no vdev_tree in target label")
return False
vtype = vdev_tree.get('type')
if vtype != 'mirror':
print(f"Error: vdev_tree type is '{vtype}', expected 'mirror'")
return False
children_pair = vdev_tree.get_pair('children')
if not children_pair or children_pair.dtype != DT_NVLIST_ARRAY:
print("Error: no children array in vdev_tree")
return False
# Find child's own entry in its label
child_guid = child_nvl.get('guid')
child_vdev_tree = child_nvl.get('vdev_tree')
new_child_entry = None
if child_vdev_tree:
child_children = child_vdev_tree.get('children')
if child_children:
for c in child_children:
if c.get('guid') == child_guid:
new_child_entry = copy.deepcopy(c)
break
if not new_child_entry:
print(f"Error: couldn't find child entry with guid {child_guid}")
return False
# Check if already present
for c in children_pair.value:
if c.get('guid') == child_guid:
print(f"Child with guid {child_guid} already in mirror")
return False
# Show what we're doing
print(f"Target: {target_device}")
print(f"Child: {child_device}")
print(f"Adding child: guid={child_guid}, "
f"path='{new_child_entry.get('path')}'")
print(f"Current children: {len(children_pair.value)}")
print(f"New children count: {len(children_pair.value) + 1}")
if dry_run:
print("\n[DRY RUN] Would write:")
children_pair.value.append(new_child_entry)
print_nvlist(target_nvl)
return True
# Add new child and reset decoded_size for recomputation
children_pair.value.append(new_child_entry)
children_pair.orig_decoded_size = None
# Write all 4 labels
print(f"\nWriting modified labels to {target_device}...")
with open(target_device, 'r+b') as fd:
for i in range(4):
encode_and_write_label(fd, i, target_size, target_nvl)
print(f" Label {i}: written")
# Fix checksums with zhack
print("Fixing checksums with zhack...")
if fix_checksum(target_device):
print("Checksums fixed!")
else:
print("WARNING: checksum fix failed, labels may be unreadable")
print("Done!")
return True
def cmd_sync_labels(source_device, target_device, dry_run=False):
"""Copy vdev_tree from source to target, keeping target's own guid."""
source_size = get_device_size(source_device)
target_size = get_device_size(target_device)
# Read source label
with open(source_device, 'rb') as fd:
source_phys = read_vdev_phys(fd, 0, source_size)
source_nvl = parse_label(source_phys)
# Read target label
with open(target_device, 'rb') as fd:
target_phys = read_vdev_phys(fd, 0, target_size)
target_nvl = parse_label(target_phys)
# Get target's own guid (must preserve)
target_guid = target_nvl.get('guid')
# Copy vdev_tree from source
source_tree = source_nvl.get('vdev_tree')
if not source_tree:
print("Error: no vdev_tree in source")
return False
# Update target's vdev_tree
target_nvl.set('vdev_tree', DT_NVLIST, copy.deepcopy(source_tree))
# Keep target's guid
target_nvl.set('guid', DT_UINT64, target_guid)
print(f"Source: {source_device}")
print(f"Target: {target_device}")
print(f"Target guid: {target_guid}")
print(f"Copying vdev_tree with {len(source_tree.get('children') or [])} children")
if dry_run:
print("\n[DRY RUN] Would write:")
print_nvlist(target_nvl)
return True
# Write all 4 labels
print(f"\nWriting synced labels to {target_device}...")
with open(target_device, 'r+b') as fd:
for i in range(4):
encode_and_write_label(fd, i, target_size, target_nvl)
print(f" Label {i}: written")
# Fix checksums with zhack
print("Fixing checksums with zhack...")
if fix_checksum(target_device):
print("Checksums fixed!")
else:
print("WARNING: checksum fix failed, labels may be unreadable")
print("Done!")
return True
# ========== Main ==========
def main():
parser = argparse.ArgumentParser(
description='ZFS vdev label editor')
sub = parser.add_subparsers(dest='command')
# show
p_show = sub.add_parser('show', help='Display label contents')
p_show.add_argument('device')
p_show.add_argument('-l', '--label', type=int, default=None,
help='Show only this label (0-3)')
# verify
p_verify = sub.add_parser('verify',
help='Verify checksums and round-trip')
p_verify.add_argument('device')
# add-child
p_add = sub.add_parser('add-child',
help='Add child device to mirror')
p_add.add_argument('target', help='Device to modify')
p_add.add_argument('child', help='Child device to add')
p_add.add_argument('-n', '--dry-run', action='store_true',
help='Show changes without writing')
# sync-labels
p_sync = sub.add_parser('sync-labels',
help='Copy vdev_tree from source to target')
p_sync.add_argument('source', help='Source device (has correct config)')
p_sync.add_argument('target', help='Target device to update')
p_sync.add_argument('-n', '--dry-run', action='store_true',
help='Show changes without writing')
args = parser.parse_args()
if not args.command:
parser.print_help()
return
if args.command == 'show':
cmd_show(args.device)
elif args.command == 'verify':
cmd_verify(args.device)
elif args.command == 'add-child':
cmd_add_child(args.target, args.child, args.dry_run)
elif args.command == 'sync-labels':
cmd_sync_labels(args.source, args.target, args.dry_run)
if __name__ == '__main__':
main()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment