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 секунд. Шлюз НРД перейдёт на новый пояс при своём следующем перезапуске.") }) }