Stub raus, vollständige Implementierung analog Unbound/Squid:
* Migration 0015: ntp_settings (single-row mit listen_addresses,
allow_acl, serve_clients, makestep, rtcsync) + ntp_pools (kind
pool|server, address, iburst/prefer, minpoll/maxpoll). Default
4 deutsche pool.ntp.org-Server seeded.
* Models DNSSettings/NTPPool, services/ntp Repo, handlers/ntp.go
REST /api/v1/ntp/{settings,pools} mit Auto-Restart nach Mutation.
* internal/chrony/chrony.cfg.tpl + chrony.go: Renderer schreibt
/etc/chrony/conf.d/edgeguard.conf direkt (analog unbound — distro
chrony.conf included conf.d automatisch). Listen-bind nur wenn
serve_clients=true; sonst port 0 (= Client-only).
* main.go: ntpRepo + chronyReloader injiziert.
* render.go: chrony als sechste generator.
* postinst:
- chrony als hard Depends im control file.
- Conf-Datei /etc/chrony/conf.d/edgeguard.conf wird als
edgeguard:edgeguard 0644 angelegt.
- Sudoers für systemctl reload + restart chrony.
* Auto-FW-Rule-Generator: udp/123 wenn serve_clients=true und
listen_addresses non-loopback enthält.
* Frontend /ntp: PageHeader + Quellen-Tab + Settings-Tab. Listen-
Addresses als Multi-Select aus Kernel-IPs (analog DNS).
* Sidebar-Eintrag unter Network.
* i18n DE/EN für ntp.* Block.
chrony.service hat kein 'reload' — Renderer ruft RestartService auf.
Verified: 4 default-pool-server connected (chronyc sources zeigt
sie nach erstem render).
Version 1.0.40.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144 lines
4.5 KiB
Go
144 lines
4.5 KiB
Go
// Package ntp provides CRUD against ntp_settings (single-row) and
|
|
// ntp_pools. Renderer in internal/chrony consumes the same data.
|
|
package ntp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
|
)
|
|
|
|
var ErrPoolNotFound = errors.New("ntp pool not found")
|
|
|
|
type Repo struct {
|
|
Pool *pgxpool.Pool
|
|
}
|
|
|
|
func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
|
|
|
|
// ── Settings ───────────────────────────────────────────────────
|
|
|
|
func (r *Repo) GetSettings(ctx context.Context) (*models.NTPSettings, error) {
|
|
row := r.Pool.QueryRow(ctx, `
|
|
SELECT id, listen_addresses, allow_acl, serve_clients,
|
|
makestep_secs, makestep_limit, rtcsync, leapsectz, updated_at
|
|
FROM ntp_settings WHERE id=1`)
|
|
var s models.NTPSettings
|
|
if err := row.Scan(&s.ID, &s.ListenAddresses, &s.AllowACL, &s.ServeClients,
|
|
&s.MakestepSecs, &s.MakestepLimit, &s.RTCSync, &s.LeapsecTZ, &s.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
func (r *Repo) UpdateSettings(ctx context.Context, s models.NTPSettings) (*models.NTPSettings, error) {
|
|
row := r.Pool.QueryRow(ctx, `
|
|
UPDATE ntp_settings SET
|
|
listen_addresses=$1, allow_acl=$2, serve_clients=$3,
|
|
makestep_secs=$4, makestep_limit=$5, rtcsync=$6, leapsectz=$7,
|
|
updated_at=NOW()
|
|
WHERE id=1
|
|
RETURNING id, listen_addresses, allow_acl, serve_clients,
|
|
makestep_secs, makestep_limit, rtcsync, leapsectz, updated_at`,
|
|
s.ListenAddresses, s.AllowACL, s.ServeClients,
|
|
s.MakestepSecs, s.MakestepLimit, s.RTCSync, s.LeapsecTZ)
|
|
var out models.NTPSettings
|
|
if err := row.Scan(&out.ID, &out.ListenAddresses, &out.AllowACL, &out.ServeClients,
|
|
&out.MakestepSecs, &out.MakestepLimit, &out.RTCSync, &out.LeapsecTZ, &out.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// ── Pools ──────────────────────────────────────────────────────
|
|
|
|
const poolSelect = `
|
|
SELECT id, kind, address, iburst, prefer, minpoll, maxpoll, active,
|
|
description, created_at, updated_at
|
|
FROM ntp_pools
|
|
`
|
|
|
|
func (r *Repo) ListPools(ctx context.Context) ([]models.NTPPool, error) {
|
|
rows, err := r.Pool.Query(ctx, poolSelect+" ORDER BY id ASC")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make([]models.NTPPool, 0, 8)
|
|
for rows.Next() {
|
|
p, err := scanPool(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, *p)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (r *Repo) GetPool(ctx context.Context, id int64) (*models.NTPPool, error) {
|
|
row := r.Pool.QueryRow(ctx, poolSelect+" WHERE id=$1", id)
|
|
p, err := scanPool(row)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrPoolNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (r *Repo) CreatePool(ctx context.Context, p models.NTPPool) (*models.NTPPool, error) {
|
|
row := r.Pool.QueryRow(ctx, `
|
|
INSERT INTO ntp_pools (kind, address, iburst, prefer, minpoll, maxpoll, active, description)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
|
RETURNING id, kind, address, iburst, prefer, minpoll, maxpoll, active,
|
|
description, created_at, updated_at`,
|
|
p.Kind, p.Address, p.Iburst, p.Prefer, p.MinPoll, p.MaxPoll, p.Active, p.Description)
|
|
return scanPool(row)
|
|
}
|
|
|
|
func (r *Repo) UpdatePool(ctx context.Context, id int64, p models.NTPPool) (*models.NTPPool, error) {
|
|
row := r.Pool.QueryRow(ctx, `
|
|
UPDATE ntp_pools SET kind=$1, address=$2, iburst=$3, prefer=$4,
|
|
minpoll=$5, maxpoll=$6, active=$7, description=$8, updated_at=NOW()
|
|
WHERE id=$9
|
|
RETURNING id, kind, address, iburst, prefer, minpoll, maxpoll, active,
|
|
description, created_at, updated_at`,
|
|
p.Kind, p.Address, p.Iburst, p.Prefer, p.MinPoll, p.MaxPoll, p.Active, p.Description, id)
|
|
out, err := scanPool(row)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrPoolNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *Repo) DeletePool(ctx context.Context, id int64) error {
|
|
tag, err := r.Pool.Exec(ctx, `DELETE FROM ntp_pools WHERE id=$1`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return ErrPoolNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func scanPool(row interface{ Scan(...any) error }) (*models.NTPPool, error) {
|
|
var p models.NTPPool
|
|
if err := row.Scan(
|
|
&p.ID, &p.Kind, &p.Address, &p.Iburst, &p.Prefer,
|
|
&p.MinPoll, &p.MaxPoll, &p.Active, &p.Description,
|
|
&p.CreatedAt, &p.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
return &p, nil
|
|
}
|