package timesync import ( "fmt" "os" "os/exec" "sync" "time" "github.com/jetkvm/kvm/internal/network" "github.com/rs/zerolog" ) const ( timeSyncRetryStep = 5 * time.Second timeSyncRetryMaxInt = 1 * time.Minute timeSyncWaitNetChkInt = 100 * time.Millisecond timeSyncWaitNetUpInt = 3 * time.Second timeSyncInterval = 1 * time.Hour timeSyncTimeout = 2 * time.Second ) var ( timeSyncRetryInterval = 0 * time.Second ) type TimeSync struct { syncLock *sync.Mutex l *zerolog.Logger ntpServers []string httpUrls []string networkConfig *network.NetworkConfig rtcDevicePath string rtcDevice *os.File //nolint:unused rtcLock *sync.Mutex syncSuccess bool preCheckFunc func() (bool, error) } type TimeSyncOptions struct { PreCheckFunc func() (bool, error) Logger *zerolog.Logger NetworkConfig *network.NetworkConfig } type SyncMode struct { Ntp bool Http bool Ordering []string NtpUseFallback bool HttpUseFallback bool } func NewTimeSync(opts *TimeSyncOptions) *TimeSync { rtcDevice, err := getRtcDevicePath() if err != nil { opts.Logger.Error().Err(err).Msg("failed to get RTC device path") } else { opts.Logger.Info().Str("path", rtcDevice).Msg("RTC device found") } t := &TimeSync{ syncLock: &sync.Mutex{}, l: opts.Logger, rtcDevicePath: rtcDevice, rtcLock: &sync.Mutex{}, preCheckFunc: opts.PreCheckFunc, ntpServers: defaultNTPServers, httpUrls: defaultHTTPUrls, networkConfig: opts.NetworkConfig, } if t.rtcDevicePath != "" { rtcTime, _ := t.readRtcTime() t.l.Info().Interface("rtc_time", rtcTime).Msg("read RTC time") } return t } func (t *TimeSync) getSyncMode() SyncMode { syncMode := SyncMode{ NtpUseFallback: true, HttpUseFallback: true, } var syncModeString string if t.networkConfig != nil { syncModeString = t.networkConfig.TimeSyncMode.String if t.networkConfig.TimeSyncDisableFallback.Bool { syncMode.NtpUseFallback = false syncMode.HttpUseFallback = false } } switch syncModeString { case "ntp_only": syncMode.Ntp = true case "http_only": syncMode.Http = true default: syncMode.Ntp = true syncMode.Http = true } return syncMode } func (t *TimeSync) doTimeSync() { metricTimeSyncStatus.Set(0) for { if ok, err := t.preCheckFunc(); !ok { if err != nil { t.l.Error().Err(err).Msg("pre-check failed") } time.Sleep(timeSyncWaitNetChkInt) continue } t.l.Info().Msg("syncing system time") start := time.Now() err := t.Sync() if err != nil { t.l.Error().Str("error", err.Error()).Msg("failed to sync system time") // retry after a delay timeSyncRetryInterval += timeSyncRetryStep time.Sleep(timeSyncRetryInterval) // reset the retry interval if it exceeds the max interval if timeSyncRetryInterval > timeSyncRetryMaxInt { timeSyncRetryInterval = 0 } continue } t.syncSuccess = true t.l.Info().Str("now", time.Now().Format(time.RFC3339)). Str("time_taken", time.Since(start).String()). Msg("time sync successful") metricTimeSyncStatus.Set(1) time.Sleep(timeSyncInterval) // after the first sync is done } } func (t *TimeSync) Sync() error { var ( now *time.Time offset *time.Duration ) syncMode := t.getSyncMode() metricTimeSyncCount.Inc() if syncMode.Ntp { now, offset = t.queryNetworkTime() } if syncMode.Http && now == nil { now = t.queryAllHttpTime() } if now == nil { return fmt.Errorf("failed to get time from any source") } if offset != nil { newNow := time.Now().Add(*offset) now = &newNow } err := t.setSystemTime(*now) if err != nil { return fmt.Errorf("failed to set system time: %w", err) } metricTimeSyncSuccessCount.Inc() return nil } func (t *TimeSync) IsSyncSuccess() bool { return t.syncSuccess } func (t *TimeSync) Start() { go t.doTimeSync() } func (t *TimeSync) setSystemTime(now time.Time) error { nowStr := now.Format("2006-01-02 15:04:05") output, err := exec.Command("date", "-s", nowStr).CombinedOutput() if err != nil { return fmt.Errorf("failed to run date -s: %w, %s", err, string(output)) } if t.rtcDevicePath != "" { return t.setRtcTime(now) } return nil }