You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
3.7 KiB
153 lines
3.7 KiB
|
2 months ago
|
package systemtime
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Settings configures clock discipline without waiting for an external server
|
||
|
|
// or rewriting an already valid clock. Browser bootstrap remains independent.
|
||
|
|
type Settings struct {
|
||
|
|
Timezone string `json:"timezone"`
|
||
|
|
NTPEnabled bool `json:"ntp_enabled"`
|
||
|
|
Servers []string `json:"servers"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type SettingsController interface {
|
||
|
|
Configure(context.Context, Settings) error
|
||
|
|
}
|
||
|
|
type NTPSettingsApplier interface {
|
||
|
|
ConfigureNTP(context.Context, bool, []string) error
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *TransactionalController) Configure(ctx context.Context, settings Settings) error {
|
||
|
|
zone, err := NormalizeTimezone(settings.Timezone)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if settings.NTPEnabled {
|
||
|
|
normalized, err := NormalizeNTPRequest(NTPRequest{Timezone: zone, Servers: settings.Servers})
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
settings.Servers = normalized.Servers
|
||
|
|
}
|
||
|
|
if c == nil || c.applier == nil {
|
||
|
|
return errors.New("time settings applier unavailable")
|
||
|
|
}
|
||
|
|
ntp, ok := c.ntpApplier.(NTPSettingsApplier)
|
||
|
|
if !ok {
|
||
|
|
return errors.New("NTP settings applier unavailable")
|
||
|
|
}
|
||
|
|
c.mu.Lock()
|
||
|
|
defer c.mu.Unlock()
|
||
|
|
rollback, err := c.applier.ApplyTimezone(ctx, zone)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if rollback == nil {
|
||
|
|
return errors.New("timezone rollback missing")
|
||
|
|
}
|
||
|
|
if err := ntp.ConfigureNTP(ctx, settings.NTPEnabled, settings.Servers); err != nil {
|
||
|
|
return errors.Join(err, rollback())
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (MockController) Configure(ctx context.Context, settings Settings) error {
|
||
|
|
_, err := NormalizeTimezone(settings.Timezone)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *OSNTPApplier) ConfigureNTP(ctx context.Context, enabled bool, servers []string) error {
|
||
|
|
|
||
|
|
var normalized NTPRequest
|
||
|
|
var err error
|
||
|
|
if enabled {
|
||
|
|
normalized, err = NormalizeNTPRequest(NTPRequest{Timezone: "UTC", Servers: servers})
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
previous, mode, existed, err := readExistingNTPConfig(a.configPath)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
running, err := a.running()
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if running {
|
||
|
|
if err := a.runner.Run(ctx, a.initPath, "stop"); err != nil {
|
||
|
|
return fmt.Errorf("stop NTP: %w", err)
|
||
|
|
}
|
||
|
|
if err := a.waitNTPState(ctx, false); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !enabled {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
err = writeAtomicFile(a.configPath, buildNTPConfig(normalized.Servers), 0o644)
|
||
|
|
if err == nil {
|
||
|
|
err = a.runner.Run(ctx, a.initPath, "start")
|
||
|
|
if err == nil {
|
||
|
|
err = a.waitNTPState(ctx, true)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if err == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
// Preserve the previous configuration if the daemon rejects the change.
|
||
|
|
if existed {
|
||
|
|
err = errors.Join(err, writeAtomicFile(a.configPath, previous, mode))
|
||
|
|
} else if removeErr := os.Remove(a.configPath); removeErr != nil && !os.IsNotExist(removeErr) {
|
||
|
|
err = errors.Join(err, removeErr)
|
||
|
|
}
|
||
|
|
return errors.Join(err, a.runRecovery(a.initPath, "start"))
|
||
|
|
}
|
||
|
|
|
||
|
|
func ntpDaemonRunning() (bool, error) {
|
||
|
|
entries, err := os.ReadDir("/proc")
|
||
|
|
if err != nil {
|
||
|
|
return false, err
|
||
|
|
}
|
||
|
|
for _, entry := range entries {
|
||
|
|
if _, err := strconv.Atoi(entry.Name()); err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
exe, err := os.Readlink(filepath.Join("/proc", entry.Name(), "exe"))
|
||
|
|
if err == nil && filepath.Base(strings.TrimSuffix(exe, " (deleted)")) == "ntpd" {
|
||
|
|
return true, nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *OSNTPApplier) waitNTPState(ctx context.Context, desired bool) error {
|
||
|
|
timeout := time.NewTimer(2 * time.Second)
|
||
|
|
defer timeout.Stop()
|
||
|
|
for {
|
||
|
|
running, err := a.running()
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if running == desired {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
select {
|
||
|
|
case <-ctx.Done():
|
||
|
|
return ctx.Err()
|
||
|
|
case <-timeout.C:
|
||
|
|
return errors.New("NTP daemon did not reach configured state")
|
||
|
|
case <-time.After(50 * time.Millisecond):
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|