f0dec0c238
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
131 lines
5.1 KiB
Go
131 lines
5.1 KiB
Go
package lkgateway
|
|
|
|
// Сервер времени (NTP) — из веб-интерфейса (05.10.2026). В закрытом контуре внешние серверы времени
|
|
// недоступны и часы уплывают, а время важно для подписей и обмена с НРД. Меняет root-скрипт
|
|
// bj-system-admin (ntpd, chrony или systemd-timesyncd — что стоит на сервере).
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// TimeSettings — серверы времени, заданные в bj.
|
|
type TimeSettings struct {
|
|
Servers string `json:"servers,omitempty"`
|
|
SavedAt time.Time `json:"saved_at,omitempty"`
|
|
}
|
|
|
|
// timeStatus — синхронизируются ли часы и текущее время сервера.
|
|
func timeStatus(ctx context.Context) (synced bool, now string) {
|
|
cctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
|
defer cancel()
|
|
out, err := exec.CommandContext(cctx, "timedatectl", "show", "-p", "NTPSynchronized", "--value").Output()
|
|
return err == nil && strings.TrimSpace(string(out)) == "yes", time.Now().Format("02.01.2006 15:04:05 MST")
|
|
}
|
|
|
|
func registerTime(mux *http.ServeMux, rc *RuntimeConfig) {
|
|
mux.HandleFunc("/admin/setup/time", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
servers := strings.Join(strings.Fields(strings.NewReplacer(",", " ", ";", " ").Replace(r.FormValue("servers"))), " ")
|
|
if servers == "" {
|
|
setupFlash(w, r, "Время: укажите сервер времени (NTP) — адрес или имя")
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
|
|
defer cancel()
|
|
res, err := runKeymediaOp(ctx, "system-ntp", map[string]string{"extra": servers})
|
|
if err != nil {
|
|
setupFlash(w, r, "Время: "+err.Error())
|
|
return
|
|
}
|
|
if res["ok"] != true {
|
|
setupFlash(w, r, "Время: "+pickStr(asString(res["error"]), "не применено"))
|
|
return
|
|
}
|
|
_ = rc.UpdateTime(TimeSettings{Servers: servers, SavedAt: time.Now()})
|
|
setupFlash(w, r, "Время: "+asString(res["output"])+". Синхронизация займёт до нескольких минут.")
|
|
})
|
|
}
|
|
|
|
func asString(v any) string {
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// wizTimeZones — часовые пояса России для выбора (время НРД — московское).
|
|
var wizTimeZones = []struct{ Zone, Title string }{
|
|
{"Europe/Kaliningrad", "Калининград (UTC+2)"},
|
|
{"Europe/Moscow", "Москва (UTC+3)"},
|
|
{"Europe/Samara", "Самара (UTC+4)"},
|
|
{"Asia/Yekaterinburg", "Екатеринбург (UTC+5)"},
|
|
{"Asia/Omsk", "Омск (UTC+6)"},
|
|
{"Asia/Novosibirsk", "Новосибирск (UTC+7)"},
|
|
{"Asia/Krasnoyarsk", "Красноярск (UTC+7)"},
|
|
{"Asia/Irkutsk", "Иркутск (UTC+8)"},
|
|
{"Asia/Yakutsk", "Якутск (UTC+9)"},
|
|
{"Asia/Vladivostok", "Владивосток (UTC+10)"},
|
|
{"Asia/Magadan", "Магадан (UTC+11)"},
|
|
{"Asia/Kamchatka", "Камчатка (UTC+12)"},
|
|
}
|
|
|
|
// serverTimeZone — часовой пояс сервера (timedatectl; запасной — /etc/timezone).
|
|
func serverTimeZone() string {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
if out, err := exec.CommandContext(ctx, "timedatectl", "show", "-p", "Timezone", "--value").Output(); err == nil {
|
|
if z := strings.TrimSpace(string(out)); z != "" {
|
|
return z
|
|
}
|
|
}
|
|
if b, err := os.ReadFile("/etc/timezone"); err == nil {
|
|
return strings.TrimSpace(string(b))
|
|
}
|
|
return time.Local.String()
|
|
}
|
|
|
|
func registerTimeZone(mux *http.ServeMux) {
|
|
mux.HandleFunc("/admin/setup/timezone", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
zone := strings.TrimSpace(r.FormValue("zone"))
|
|
known := false
|
|
for _, z := range wizTimeZones {
|
|
if z.Zone == zone {
|
|
known = true
|
|
}
|
|
}
|
|
if !known {
|
|
setupFlash(w, r, "Часовой пояс: выберите из списка")
|
|
return
|
|
}
|
|
if zone == serverTimeZone() {
|
|
setupFlash(w, r, "Часовой пояс уже "+zone)
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
|
|
defer cancel()
|
|
res, err := runKeymediaOp(ctx, "system-tz", map[string]string{"extra": zone})
|
|
if err != nil || res["ok"] != true {
|
|
setupFlash(w, r, "Часовой пояс не изменён: "+pickStr(asString(res["error"]), errStr(err)))
|
|
return
|
|
}
|
|
// время процесса bj берётся при запуске — перезапускаем, чтобы журналы и сроки шли по новому поясу
|
|
go func() {
|
|
time.Sleep(2 * time.Second)
|
|
_, _ = restartUnit(context.Background(), "bj-server")
|
|
}()
|
|
setupFlash(w, r, asString(res["output"])+". bj перезапускается — обновите страницу через 15 секунд. Шлюз НРД перейдёт на новый пояс при своём следующем перезапуске.")
|
|
})
|
|
}
|