Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 812a2d1699 |
@@ -35,11 +35,6 @@ We would like to thank the following people and organizations for supporting zre
|
|||||||
..
|
..
|
||||||
The list below is (roughly) sorted by date of latest contribution, newest first.
|
The list below is (roughly) sorted by date of latest contribution, newest first.
|
||||||
|
|
||||||
..
|
|
||||||
↓ post v0.7.0
|
|
||||||
|
|
||||||
* |supporter-std| `drbawb <https://git.sr.ht/~hime>`_
|
|
||||||
|
|
||||||
..
|
..
|
||||||
↓ claude --permission-mode default /update-supporters v0.6.1..v0.7.0
|
↓ claude --permission-mode default /update-supporters v0.6.1..v0.7.0
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.1 // indirect
|
||||||
github.com/felixge/fgprof v0.9.5 // indirect
|
github.com/felixge/fgprof v0.9.5 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef // indirect
|
github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef // indirect
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||||
|
|||||||
@@ -635,25 +635,6 @@ func (f subroot) UserSpecifiedDatasets() zfs.UserSpecifiedDatasetsSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// senderPrefixDatasetPath returns the configured literal sender-side prefix
|
|
||||||
// (e.g. "hdd_9XG1J9J1") to strip from incoming filesystem paths, or nil if
|
|
||||||
// ZREPL_STRIP_SENDER_PREFIX is unset/empty. Using a literal string (rather
|
|
||||||
// than a component count) lets us losslessly reconstruct the sender's
|
|
||||||
// original path later in ListFilesystems, which is required for the
|
|
||||||
// sender/receiver filesystem-matching in the replication planner to work.
|
|
||||||
func senderPrefixDatasetPath() *zfs.DatasetPath {
|
|
||||||
s := envconst.String("ZREPL_STRIP_SENDER_PREFIX", "")
|
|
||||||
if s == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dp, err := zfs.NewDatasetPath(s)
|
|
||||||
if err != nil || dp.Length() == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return dp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
|
func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
|
||||||
p, err := zfs.NewDatasetPath(fs)
|
p, err := zfs.NewDatasetPath(fs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -662,17 +643,11 @@ func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
|
|||||||
if p.Length() == 0 {
|
if p.Length() == 0 {
|
||||||
return nil, errors.Errorf("cannot map empty filesystem")
|
return nil, errors.Errorf("cannot map empty filesystem")
|
||||||
}
|
}
|
||||||
// PATCH: strip a configured literal prefix (e.g. the source zpool name)
|
|
||||||
// from the *sender's* path before appending it under root_fs.
|
|
||||||
if prefix := senderPrefixDatasetPath(); prefix != nil && p.HasPrefix(prefix) {
|
|
||||||
p.TrimPrefix(prefix)
|
|
||||||
}
|
|
||||||
c := f.localRoot.Copy()
|
c := f.localRoot.Copy()
|
||||||
c.Extend(p)
|
c.Extend(p)
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
|
func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
|
||||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||||
|
|
||||||
@@ -713,28 +688,12 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
|||||||
|
|
||||||
a.TrimPrefix(root)
|
a.TrimPrefix(root)
|
||||||
|
|
||||||
// PATCH: re-prepend the literal sender prefix that MapToLocal strips,
|
|
||||||
// so that the Path we report here matches what the sender reports
|
|
||||||
// for the same filesystem (see replication_logic.go's Path-based
|
|
||||||
// sender/receiver matching). Without this, the planner can never
|
|
||||||
// find a matching receiverFS and will treat every cycle as if the
|
|
||||||
// filesystem doesn't exist yet on the receiver.
|
|
||||||
reportPath := a.ToString()
|
|
||||||
if prefix := senderPrefixDatasetPath(); prefix != nil {
|
|
||||||
if reportPath == "" {
|
|
||||||
reportPath = prefix.ToString()
|
|
||||||
} else {
|
|
||||||
reportPath = prefix.ToString() + "/" + reportPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs := &pdu.Filesystem{
|
fs := &pdu.Filesystem{
|
||||||
Path: reportPath,
|
Path: a.ToString(),
|
||||||
IsPlaceholder: ph.IsPlaceholder,
|
IsPlaceholder: ph.IsPlaceholder,
|
||||||
ResumeToken: token,
|
ResumeToken: token,
|
||||||
}
|
}
|
||||||
fss = append(fss, fs)
|
fss = append(fss, fs)
|
||||||
|
|
||||||
}
|
}
|
||||||
if len(fss) == 0 {
|
if len(fss) == 0 {
|
||||||
getLogger(ctx).Debug("no filesystems found")
|
getLogger(ctx).Debug("no filesystems found")
|
||||||
@@ -1144,4 +1103,4 @@ func doDestroySnapshots(ctx context.Context, lp *zfs.DatasetPath, snaps []*pdu.F
|
|||||||
return &pdu.DestroySnapshotsRes{
|
return &pdu.DestroySnapshotsRes{
|
||||||
Results: ress,
|
Results: ress,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
https://claude.ai/chat/f688c411-b11e-4455-918b-fde5e6577430
|
|
||||||
|
|
||||||
Diagnosed replication snapshot duplication caused by version listing failures
|
|
||||||
Diagnosed replication snapshot duplication caused by version listing failures
|
|
||||||
|
|
||||||
Отличная зацепка — снапшот chunk_9XG1J8T0/nomad/ha-zah@zrepl_20260724_054520_000 реально существует (0B allocated, 195M refer — то есть данные там есть), но планировщик каждый цикл продолжает считать, что на приёмнике версий нет вообще. Значит проблема не в наличии данных, а в том, что ListFilesystemVersions (то, чем сверяется планировщик) их почему-то не видит. Погружусь в код листинга снапшотов на ресивере.
|
|
||||||
|
|
||||||
Ran 3 commands, viewed 3 files
|
|
||||||
Ran 3 commands, viewed 3 files
|
|
||||||
|
|
||||||
Кажется, нашёл настоящий баг — и это очень похоже на правду. Проверю до конца, как формируется список receiverFS/IsPlaceholder, которым пользуется планировщик (не тот же код, что читает placeholder при самом receive).
|
|
||||||
|
|
||||||
Viewed 5 files, ran 4 commands
|
|
||||||
Viewed 5 files, ran 4 commands
|
|
||||||
|
|
||||||
Нашёл настоящую причину, и она серьёзнее, чем казалось — это баг именно от моего патча, а не от zrepl самого по себе. Дело в сопоставлении списков sender/receiver в планировщике.
|
|
||||||
|
|
||||||
Проверяю, есть ли envconst.String для строкового значения
|
|
||||||
Проверяю, есть ли envconst.String для строкового значения
|
|
||||||
Вот и реальный баг — и он от патча
|
|
||||||
|
|
||||||
Планировщик (replication_logic.go:302-306) сопоставляет sender и receiver filesystem'ы строковым сравнением путей:
|
|
||||||
|
|
||||||
go
|
|
||||||
for _, rfs := range rfss {
|
|
||||||
if rfs.Path == fs.Path { // fs.Path — путь СЕНДЕРА, напр. "hdd_9XG1J9J1/nomad/ha-zah"
|
|
||||||
receiverFS = rfs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
А rfs.Path формируется в Receiver.ListFilesystems (endpoint.go:695) как a.TrimPrefix(root) — локальный путь chunk_9XG1J8T0/nomad/ha-zah минус root_fs = nomad/ha-zah.
|
|
||||||
|
|
||||||
До патча это совпадало с sender-путём один-в-один (root_fs + sender_path → обрезаем root_fs → получаем обратно sender_path). А теперь, когда MapToLocal откусывает ещё и hdd_9XG1J9J1, обратная обрезка даёts nomad/ha-zah — а сендер сообщает hdd_9XG1J9J1/nomad/ha-zah. Строки не совпадают → receiverFS всегда nil → планировщик каждый раз считает, что на приёмнике версий нет → пытается full receive → ловит "destination exists". Именно поэтому цикл повторяется бесконечно.
|
|
||||||
|
|
||||||
Чиню — добавляю обратное восстановление отрезанного префикса именно в отчёт ListFilesystems, чтобы сопоставление путей снова работало:
|
|
||||||
|
|
||||||
Architected shared helper function to consolidate prefix-handling logic
|
|
||||||
Architected shared helper function to consolidate prefix-handling logic
|
|
||||||
|
|
||||||
Есть всё нужное. Переписываю патч целиком на литеральный строковый префикс — это устраняет и проблему с расчётом пути, и делает обратное сопоставление в ListFilesystems возможным:
|
|
||||||
|
|
||||||
Reconciled edge case behavior and prepared filesystem prefix restoration
|
|
||||||
Reconciled edge case behavior and prepared filesystem prefix restoration
|
|
||||||
Смотрю текущее состояние MapToLocal перед переработкой
|
|
||||||
Переключаю strip с "N компонентов" на литеральный строковый префикс, чтобы его можно было симметрично восстановить в ListFilesystems для сопоставления путей с sender'ом
|
|
||||||
endpoint.go
|
|
||||||
+35
|
|
||||||
-18
|
|
||||||
|
|
||||||
Reconciled edge case behavior and prepared filesystem prefix restoration.
|
|
||||||
|
|
||||||
Done
|
|
||||||
|
|
||||||
Теперь патчу ListFilesystems, чтобы при формировании отчётного пути (используемого для сверки с sender) обрезанный префикс восстанавливался обратно:
|
|
||||||
|
|
||||||
Восстанавливаю отрезанный sender-префикс в пути, который ListFilesystems возвращает планировщику — иначе receiver Path не совпадает с sender Path и планировщик считает receiver пустым на каждом цикле
|
|
||||||
Восстанавливаю отрезанный sender-префикс в пути, который ListFilesystems возвращает планировщику — иначе receiver Path не совпадает с sender Path и планировщик считает receiver пустым на каждом цикле
|
|
||||||
|
|
||||||
Проверяю синтаксис и то, что старая переменная ZREPL_STRIP_SENDER_PREFIX_COMPS больше нигде не используется:
|
|
||||||
|
|
||||||
Проверяю синтаксис и итоговое использование переменных окружения
|
|
||||||
Проверяю синтаксис и итоговое использование переменных окружения
|
|
||||||
|
|
||||||
Синтаксис чистый, старой переменной нигде не осталось. Собираю (насколько позволяет toolchain в песочнице):
|
|
||||||
|
|
||||||
Ran 2 commands
|
|
||||||
Ran 2 commands
|
|
||||||
|
|
||||||
Отлично, всё корректно, включая граничный случай равных путей. Патч готов. Итог:
|
|
||||||
|
|
||||||
Что было не так
|
|
||||||
|
|
||||||
Планировщик сопоставляет filesystem'ы sender'а и receiver'а строковым равенством путей (replication_logic.go:303: rfs.Path == fs.Path). До патча receiver.Path (локальный путь минус root_fs) всегда совпадал с sender.Path один в один. Мой первый патч ломал этот инвариант: обрезав ещё и hdd_9XG1J9J1 при записи, но не восстанавливая его при отчёте — receiver стал сообщать "nomad/ha-zah", а sender — "hdd_9XG1J9J1/nomad/ha-zah". Строки никогда не совпадали → receiverFS всегда nil → планировщик каждый цикл считал, что на приёмнике ничего нет → пытался полный recv в уже занятый датасет → ошибка. Бесконечный цикл был не багом zrepl, а прямым следствием несимметричности моего же патча.
|
|
||||||
|
|
||||||
Что изменилось
|
|
||||||
Заменил обрезку "N компонентов" на обрезку литерального строкового префикса — это делает операцию обратимой.
|
|
||||||
В MapToLocal префикс отрезается при вычислении локального пути хранения.
|
|
||||||
В ListFilesystems тот же префикс приклеивается обратно к пути, который сообщается планировщику — так путь снова совпадает с тем, что говорит sender, и сопоставление работает.
|
|
||||||
Как применить
|
|
||||||
|
|
||||||
Пересоберите бинарник, затем поменяйте имя переменной в systemd-юните (старое ZREPL_STRIP_SENDER_PREFIX_COMPS=1 → новое, с явным значением префикса):
|
|
||||||
|
|
||||||
ini
|
|
||||||
Environment=ZREPL_STRIP_SENDER_PREFIX=hdd_9XG1J9J1
|
|
||||||
bash
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart zrepl_rcv
|
|
||||||
|
|
||||||
Датасеты, скорее всего, снова придётся снести (zfs destroy -r chunk_9XG1J8T0/nomad) и дать реплицироваться с нуля, раз в прошлый раз всё было в неконсистентном состоянии из-за бага. После рестарта проверьте:
|
|
||||||
|
|
||||||
bash
|
|
||||||
journalctl -u zrepl_rcv -n 100 --no-pager | grep -iE "conflict|receive failed|error"
|
|
||||||
|
|
||||||
Ключевой признак, что теперь всё работает правильно: строка "conflict automatically resolved" с пустым sorted receiver versions должна появиться только один раз (при первой инициализации датасета), а не повторяться на каждом цикле после успешного приёма.
|
|
||||||
|
|
||||||
а где патч?
|
|
||||||
|
|
||||||
Показываю итоговый diff патча
|
|
||||||
Показываю итоговый diff патча
|
|
||||||
|
|
||||||
дай исправленные функции
|
|
||||||
|
|
||||||
Показываю итоговый код исправленных функций целиком
|
|
||||||
Показываю итоговый код исправленных функций целиком
|
|
||||||
|
|
||||||
Вот три исправленные функции целиком (файл internal/endpoint/endpoint.go):
|
|
||||||
|
|
||||||
1. Новая вспомогательная функция (строки 638–654):
|
|
||||||
|
|
||||||
go
|
|
||||||
// senderPrefixDatasetPath returns the configured literal sender-side prefix
|
|
||||||
// (e.g. "hdd_9XG1J9J1") to strip from incoming filesystem paths, or nil if
|
|
||||||
// ZREPL_STRIP_SENDER_PREFIX is unset/empty. Using a literal string (rather
|
|
||||||
// than a component count) lets us losslessly reconstruct the sender's
|
|
||||||
// original path later in ListFilesystems, which is required for the
|
|
||||||
// sender/receiver filesystem-matching in the replication planner to work.
|
|
||||||
func senderPrefixDatasetPath() *zfs.DatasetPath {
|
|
||||||
s := envconst.String("ZREPL_STRIP_SENDER_PREFIX", "")
|
|
||||||
if s == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dp, err := zfs.NewDatasetPath(s)
|
|
||||||
if err != nil || dp.Length() == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return dp
|
|
||||||
}
|
|
||||||
|
|
||||||
2. MapToLocal (строки 656–672):
|
|
||||||
|
|
||||||
go
|
|
||||||
func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
|
|
||||||
p, err := zfs.NewDatasetPath(fs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if p.Length() == 0 {
|
|
||||||
return nil, errors.Errorf("cannot map empty filesystem")
|
|
||||||
}
|
|
||||||
// PATCH: strip a configured literal prefix (e.g. the source zpool name)
|
|
||||||
// from the *sender's* path before appending it under root_fs.
|
|
||||||
if prefix := senderPrefixDatasetPath(); prefix != nil && p.HasPrefix(prefix) {
|
|
||||||
p.TrimPrefix(prefix)
|
|
||||||
}
|
|
||||||
c := f.localRoot.Copy()
|
|
||||||
c.Extend(p)
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
3. Кусок ListFilesystems, где формируется Path (строки 712–734, остальная часть функции без изменений):
|
|
||||||
|
|
||||||
go
|
|
||||||
a.TrimPrefix(root)
|
|
||||||
|
|
||||||
// PATCH: re-prepend the literal sender prefix that MapToLocal strips,
|
|
||||||
// so that the Path we report here matches what the sender reports
|
|
||||||
// for the same filesystem (see replication_logic.go's Path-based
|
|
||||||
// sender/receiver matching). Without this, the planner can never
|
|
||||||
// find a matching receiverFS and will treat every cycle as if the
|
|
||||||
// filesystem doesn't exist yet on the receiver.
|
|
||||||
reportPath := a.ToString()
|
|
||||||
if prefix := senderPrefixDatasetPath(); prefix != nil {
|
|
||||||
if reportPath == "" {
|
|
||||||
reportPath = prefix.ToString()
|
|
||||||
} else {
|
|
||||||
reportPath = prefix.ToString() + "/" + reportPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs := &pdu.Filesystem{
|
|
||||||
Path: reportPath,
|
|
||||||
IsPlaceholder: ph.IsPlaceholder,
|
|
||||||
ResumeToken: token,
|
|
||||||
}
|
|
||||||
fss = append(fss, fs)
|
|
||||||
|
|
||||||
Больше в файле ничего не менялось — MapToLocal вызывается ещё и из Receive/ListFilesystemVersions/DestroySnapshots, но там аргументом всегда приходит исходный путь сендера (req.Filesystem), так что дополнительных правок там не требуется.
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# zrepl_last_snapshot_age.sh
|
|
||||||
#
|
|
||||||
# Показывает, сколько минут назад был создан последний снапшот
|
|
||||||
# (по умолчанию с префиксом "zrepl_") на резервной (приёмной) стороне.
|
|
||||||
#
|
|
||||||
# Использование:
|
|
||||||
# ./zrepl_last_snapshot_age.sh <датасет> [<датасет2> ...]
|
|
||||||
# ./zrepl_last_snapshot_age.sh -r <датасет> # рекурсивно по всем дочерним
|
|
||||||
# ./zrepl_last_snapshot_age.sh -p zrepl_ -r chunk_9XG1J8T0/nomad
|
|
||||||
#
|
|
||||||
# Опции:
|
|
||||||
# -r рекурсивно проверять все дочерние датасеты
|
|
||||||
# -p PREFIX фильтровать снапшоты по префиксу имени (по умолчанию "zrepl_")
|
|
||||||
# -w MINUTES задать порог в минутах: если снапшот старше — exit code 1
|
|
||||||
# (удобно для мониторинга / cron)
|
|
||||||
# -q тихий режим: не печатать построчный вывод, только итог
|
|
||||||
#
|
|
||||||
# Коды возврата:
|
|
||||||
# 0 — всё ок (снапшот найден, и если задан -w, он не старше порога)
|
|
||||||
# 1 — снапшот найден, но старше порога -w
|
|
||||||
# 2 — снапшот не найден вообще (для датасета/выборки)
|
|
||||||
# 3 — ошибка использования / zfs недоступен
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
PREFIX="zrepl_"
|
|
||||||
RECURSIVE=0
|
|
||||||
WARN_MINUTES=""
|
|
||||||
QUIET=0
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
grep '^#' "$0" | sed -e '1d' -e 's/^# \{0,1\}//'
|
|
||||||
exit 3
|
|
||||||
}
|
|
||||||
|
|
||||||
while getopts ":rp:w:qh" opt; do
|
|
||||||
case "$opt" in
|
|
||||||
r) RECURSIVE=1 ;;
|
|
||||||
p) PREFIX="$OPTARG" ;;
|
|
||||||
w) WARN_MINUTES="$OPTARG" ;;
|
|
||||||
q) QUIET=1 ;;
|
|
||||||
h) usage ;;
|
|
||||||
*) usage ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
shift $((OPTIND - 1))
|
|
||||||
|
|
||||||
if [[ $# -lt 1 ]]; then
|
|
||||||
echo "Ошибка: укажите хотя бы один датасет." >&2
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v zfs >/dev/null 2>&1; then
|
|
||||||
echo "Ошибка: команда zfs не найдена." >&2
|
|
||||||
exit 3
|
|
||||||
fi
|
|
||||||
|
|
||||||
now_epoch=$(date +%s)
|
|
||||||
|
|
||||||
overall_newest_epoch=0
|
|
||||||
overall_newest_name=""
|
|
||||||
any_found=0
|
|
||||||
|
|
||||||
for dataset in "$@"; do
|
|
||||||
zfs_opts=(-H -p -o name,creation -t snapshot -s creation)
|
|
||||||
if [[ $RECURSIVE -eq 1 ]]; then
|
|
||||||
zfs_opts+=(-r)
|
|
||||||
else
|
|
||||||
zfs_opts+=(-d 1)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -H без заголовков, -p значения в "машинном" виде (creation -> epoch seconds)
|
|
||||||
mapfile -t lines < <(zfs list "${zfs_opts[@]}" "$dataset" 2>/dev/null | grep "@${PREFIX}" || true)
|
|
||||||
|
|
||||||
if [[ ${#lines[@]} -eq 0 ]]; then
|
|
||||||
if [[ $QUIET -eq 0 ]]; then
|
|
||||||
echo "[$dataset] снапшотов с префиксом '${PREFIX}' не найдено"
|
|
||||||
fi
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# последняя строка — самый свежий снапшот (сортировка -s creation)
|
|
||||||
last_line="${lines[-1]}"
|
|
||||||
snap_name="${last_line%%$'\t'*}"
|
|
||||||
snap_epoch="${last_line##*$'\t'}"
|
|
||||||
|
|
||||||
age_seconds=$(( now_epoch - snap_epoch ))
|
|
||||||
age_minutes=$(( age_seconds / 60 ))
|
|
||||||
|
|
||||||
any_found=1
|
|
||||||
if (( snap_epoch > overall_newest_epoch )); then
|
|
||||||
overall_newest_epoch=$snap_epoch
|
|
||||||
overall_newest_name=$snap_name
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $QUIET -eq 0 ]]; then
|
|
||||||
printf "[%s] последний снапшот: %s (%d мин. назад)\n" \
|
|
||||||
"$dataset" "$snap_name" "$age_minutes"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ $any_found -eq 0 ]]; then
|
|
||||||
echo "Ни одного снапшота с префиксом '${PREFIX}' не найдено ни в одном из указанных датасетов." >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
overall_age_minutes=$(( (now_epoch - overall_newest_epoch) / 60 ))
|
|
||||||
|
|
||||||
echo "---"
|
|
||||||
printf "Самый свежий снапшот в выборке: %s (%d мин. назад)\n" \
|
|
||||||
"$overall_newest_name" "$overall_age_minutes"
|
|
||||||
|
|
||||||
if [[ -n "$WARN_MINUTES" ]]; then
|
|
||||||
if (( overall_age_minutes > WARN_MINUTES )); then
|
|
||||||
echo "ВНИМАНИЕ: последний снапшот старше порога ${WARN_MINUTES} мин." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@@ -69,7 +69,7 @@ cp -a internal/config/samples %{buildroot}%{_datadir}/
|
|||||||
%{_bindir}/zrepl
|
%{_bindir}/zrepl
|
||||||
%config %{_unitdir}/zrepl.service
|
%config %{_unitdir}/zrepl.service
|
||||||
%dir %{_sysconfdir}/zrepl
|
%dir %{_sysconfdir}/zrepl
|
||||||
%config(noreplace) %{_sysconfdir}/zrepl/zrepl.yml
|
%config %{_sysconfdir}/zrepl/zrepl.yml
|
||||||
%{_datadir}/zsh/site-functions/_zrepl
|
%{_datadir}/zsh/site-functions/_zrepl
|
||||||
%{_datadir}/bash-completion/completions/zrepl
|
%{_datadir}/bash-completion/completions/zrepl
|
||||||
%{_datadir}/doc/zrepl
|
%{_datadir}/doc/zrepl
|
||||||
|
|||||||
Reference in New Issue
Block a user