Netpulse_SasS/server/internal/store/maps_undo.go
byrsapty c28852df66
Some checks are pending
CI / web (push) Waiting to run
CI / server (push) Waiting to run
CI / agent (push) Waiting to run
Syslog: транспорт до сервера і тригер позачергового бекапу
Приймач на зонді лежав без діла — тепер під'єднаний. Окремий стрім
StreamLogs, а не контрольний канал: сплеск логів під час аварії не має
заважати heartbeat і командам.

Хост зіставляється за адресою джерела на зонді: у сервера немає
контексту мережі клієнта, а один приватний діапазон трапляється в
десятках кабінетів. Невідома адреса не привід викинути подію.

Подія, що збіглася зі зразком у ncm.device_policies.syslog_match,
ставить позачерговий збір конфігу. Типовий зразок покриває Cisco,
HP/Huawei, Juniper і MikroTik — навмисно широкий: зайвий бекап коштує
секунд, пропущений — цілої зміни.

Заразом увесь репозиторій прогнано через gofmt: CI, написаний два
кроки тому, перевіряє це і впав би на 29 файлах із порушеннями,
накопиченими за весь проєкт.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 15:20:32 +03:00

135 lines
4.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package store
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
)
// UndoResult — що вийшло після відкату.
type UndoResult struct {
Revision int64 `json:"revision"`
RestoredFrom int64 `json:"restored_from"`
}
// ErrNothingToUndo — історії ще немає (мапу щойно створили).
var ErrNothingToUndo = errors.New("немає до чого відкочуватись")
// UndoMap повертає полотно до попереднього знімка.
//
// Відкат оформлюється як НОВА ревізія, а не як відмотування лічильника.
// Інакше клієнти, що тримають номер 8, після відкату до 7 отримали б
// «свою» ревізію знову актуальною й тихо перезаписали б відкочене.
// Undo — така сама зміна, як будь-яка інша, і має рухати історію вперед.
//
// Ідентифікатори вузлів зберігаються зі знімка: інакше ребра, що на них
// посилаються, довелося б переписувати, а зовнішні посилання (виділення
// в UI, закладки) зламалися б.
func (s *Store) UndoMap(ctx context.Context, tenantID, mapID, authorID string) (*UndoResult, error) {
res := &UndoResult{}
err := s.InTenantTx(ctx, tenantID, func(tx pgx.Tx) error {
var current int64
err := tx.QueryRow(ctx, `
SELECT revision FROM topo.maps
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
FOR UPDATE
`, mapID, tenantID).Scan(&current)
if errors.Is(err, pgx.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
// Найсвіжіший знімок ДО поточного стану. Номери ревізій можуть
// мати проміжки (автопобудова, кілька правок), тому шукаємо
// максимальний менший, а не current-1.
var (
from int64
nodes []byte
edges []byte
backs []byte
)
err = tx.QueryRow(ctx, `
SELECT revision,
(snapshot->'nodes')::text::bytea,
(snapshot->'edges')::text::bytea,
(snapshot->'backgrounds')::text::bytea
FROM topo.map_revisions
WHERE map_id = $1 AND revision < $2
ORDER BY revision DESC
LIMIT 1
`, mapID, current).Scan(&from, &nodes, &edges, &backs)
if errors.Is(err, pgx.ErrNoRows) {
return ErrNothingToUndo
}
if err != nil {
return err
}
// Порядок важливий: ребра посилаються на вузли, тож видаляємо
// їх першими, а вставляємо — останніми.
if _, err := tx.Exec(ctx, `DELETE FROM topo.map_edges WHERE map_id = $1`, mapID); err != nil {
return err
}
if _, err := tx.Exec(ctx, `DELETE FROM topo.map_nodes WHERE map_id = $1`, mapID); err != nil {
return err
}
if _, err := tx.Exec(ctx, `DELETE FROM topo.map_backgrounds WHERE map_id = $1`, mapID); err != nil {
return err
}
// Вузли вставляємо двома проходами: спершу без parent_node_id,
// потім проставляємо його. Інакше вкладений вузол міг би
// зустрітися раніше за свого батька й упертись у FK.
if _, err := tx.Exec(ctx, `
INSERT INTO topo.map_nodes
SELECT (r).* FROM (
SELECT jsonb_populate_record(null::topo.map_nodes, e) AS r
FROM jsonb_array_elements($1::jsonb) e
) t
`, string(nodes)); err != nil {
return fmt.Errorf("відновлення вузлів: %w", err)
}
if _, err := tx.Exec(ctx, `
INSERT INTO topo.map_backgrounds
SELECT (r).* FROM (
SELECT jsonb_populate_record(null::topo.map_backgrounds, e) AS r
FROM jsonb_array_elements($1::jsonb) e
) t
`, string(backs)); err != nil {
return fmt.Errorf("відновлення підкладок: %w", err)
}
if _, err := tx.Exec(ctx, `
INSERT INTO topo.map_edges
SELECT (r).* FROM (
SELECT jsonb_populate_record(null::topo.map_edges, e) AS r
FROM jsonb_array_elements($1::jsonb) e
) t
`, string(edges)); err != nil {
return fmt.Errorf("відновлення ребер: %w", err)
}
if err := tx.QueryRow(ctx, `
UPDATE topo.maps SET revision = revision + 1, updated_at = now()
WHERE id = $1 AND tenant_id = $2
RETURNING revision
`, mapID, tenantID).Scan(&res.Revision); err != nil {
return err
}
res.RestoredFrom = from
return snapshotMap(ctx, tx, tenantID, mapID, res.Revision, authorID,
fmt.Sprintf("відкат до ревізії %d", from))
})
if err != nil {
return nil, mapPgError(err)
}
return res, nil
}