github.com/simpleiot/simpleiot@v0.18.3/client/ntp.go (about)

     1  package client
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"log"
     7  	"os"
     8  	"os/exec"
     9  	"strings"
    10  
    11  	"github.com/nats-io/nats.go"
    12  	"github.com/simpleiot/simpleiot/data"
    13  	"golang.org/x/exp/slices"
    14  )
    15  
    16  // NTPConfigPath points to the systemd-timesyncd configuration file
    17  const NTPConfigPath = "/etc/systemd/timesyncd.conf.d/simpleiot.conf"
    18  
    19  // NTPClient is a SimpleIoT client that synchronizes NTP servers to local
    20  // systemd-timesync configuration
    21  type NTPClient struct {
    22  	log      *log.Logger
    23  	nc       *nats.Conn
    24  	config   NTP
    25  	stopCh   chan struct{}
    26  	pointsCh chan NewPoints
    27  }
    28  
    29  // NTP client configuration
    30  type NTP struct {
    31  	ID              string   `node:"id"`
    32  	Parent          string   `node:"parent"`
    33  	Servers         []string `point:"server"`
    34  	FallbackServers []string `point:"fallbackServer"`
    35  }
    36  
    37  // NewNTPClient returns a new NTPClient using its
    38  // configuration read from the Client Manager
    39  func NewNTPClient(nc *nats.Conn, config NTP) Client {
    40  	// TODO: Ensure only one NTP client exists
    41  	return &NTPClient{
    42  		log:      log.New(os.Stderr, "NTP: ", log.LstdFlags|log.Lmsgprefix),
    43  		nc:       nc,
    44  		config:   config,
    45  		stopCh:   make(chan struct{}),
    46  		pointsCh: make(chan NewPoints),
    47  	}
    48  }
    49  
    50  // Run starts the NTP Client
    51  func (c *NTPClient) Run() error {
    52  	c.log.Println("Starting NTP client")
    53  	err := c.UpdateConfig()
    54  	if err != nil {
    55  		c.log.Println("error updating systemd-timesyncd config:", err)
    56  	}
    57  loop:
    58  	for {
    59  		select {
    60  		case <-c.stopCh:
    61  			break loop
    62  		case points := <-c.pointsCh:
    63  			// Update local configuration
    64  			err := data.MergePoints(points.ID, points.Points, &c.config)
    65  			if err != nil {
    66  				return fmt.Errorf("merging points: %w", err)
    67  			}
    68  			err = c.UpdateConfig()
    69  			if err != nil {
    70  				c.log.Println("error updating systemd-timesyncd config:", err)
    71  				break // select
    72  			}
    73  		}
    74  	}
    75  	return nil
    76  }
    77  
    78  // UpdateConfig writes the NTP configuration to NTPConfigPath and restarts
    79  // system-timesyncd
    80  func (c *NTPClient) UpdateConfig() error {
    81  	data := []byte(`
    82  # This file is auto-generated by SimpleIoT
    83  # DO NOT EDIT OR REMOVE!
    84  [Time]
    85  `)
    86  	if len(c.config.Servers) > 0 {
    87  		data = append(
    88  			data,
    89  			[]byte(`NTP=`+strings.Join(c.config.Servers, " "))...,
    90  		)
    91  	}
    92  	if len(c.config.FallbackServers) > 0 {
    93  		data = append(
    94  			data,
    95  			[]byte(`FallbackNTP=`+strings.Join(c.config.FallbackServers, " "))...,
    96  		)
    97  	}
    98  	f, err := os.OpenFile(NTPConfigPath, os.O_RDWR|os.O_CREATE, 0644)
    99  	if err != nil {
   100  		return err
   101  	}
   102  	currData, err := io.ReadAll(f)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	if slices.Equal(currData, data) {
   107  		// No changes to file
   108  		return nil
   109  	}
   110  	n, err := f.WriteAt(data, 0)
   111  	if err != nil {
   112  		return err
   113  	}
   114  	err = f.Truncate(int64(n))
   115  	if err != nil {
   116  		return err
   117  	}
   118  	// Restart NTP
   119  	return exec.Command("/usr/bin/systemctl", "restart", "systemd-timesyncd").Run()
   120  }
   121  
   122  // Stop stops the NTP Client
   123  func (c *NTPClient) Stop(error) {
   124  	close(c.stopCh)
   125  }
   126  
   127  // Points is called when the client's node points are updated
   128  func (c *NTPClient) Points(nodeID string, points []data.Point) {
   129  	c.pointsCh <- NewPoints{
   130  		ID:     nodeID,
   131  		Points: points,
   132  	}
   133  }
   134  
   135  // EdgePoints is called when the client's node edge points are updated
   136  func (c *NTPClient) EdgePoints(
   137  	_ string, _ string, _ []data.Point,
   138  ) {
   139  	// Do nothing
   140  }