Skip to content

Commit

Permalink
Add D-Bus API for configuring NTP servers of systemd-timesyncd
Browse files Browse the repository at this point in the history
Add new `time` package that currently provides methods that configure
NTP and FallbackNTP options of systemd-timesyncd service. The service is
restarted when the values are changed. Example usage through gdbus:

  gdbus call --system  --dest io.hass.os \
    --object-path /io/hass/os/Time/Timesyncd \
    --method org.freedesktop.DBus.Properties.Set \
    io.hass.os.Time.Timesyncd NTPServer \
    "<['pool.ntp.org', 'time.google.com']>"

A `lineinfile` helper has been implemented for adjusting Systemd unit
files, the inspiration comes from Ansible's module of the same name,
although the behavior is slightly different (hopefully still quite
intuitive). Unit tests for the core methods handling the file content
are included.

In the future, the `time` package could also handle system timezone and
other time-related tasks.
  • Loading branch information
sairon committed Sep 5, 2024
1 parent 58f7f29 commit 0f09f2b
Show file tree
Hide file tree
Showing 4 changed files with 591 additions and 0 deletions.
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
os_time "github.com/home-assistant/os-agent/time"
"time"

"github.com/coreos/go-systemd/v22/daemon"
Expand Down Expand Up @@ -70,6 +71,7 @@ func main() {
apparmor.InitializeDBus(conn)
cgroup.InitializeDBus(conn)
boards.InitializeDBus(conn, board)
os_time.InitializeDBus(conn)

_, err = daemon.SdNotify(false, daemon.SdNotifyReady)
if err != nil {
Expand Down
175 changes: 175 additions & 0 deletions time/timesyncd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package time

import (
"fmt"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/introspect"
"github.com/godbus/dbus/v5/prop"
"github.com/home-assistant/os-agent/utils/lineinfile"
"regexp"
"strings"

logging "github.com/home-assistant/os-agent/utils/log"
)

const (
objectPath = "/io/hass/os/Time/Timesyncd"
ifaceName = "io.hass.os.Time.Timesyncd"
timesyncdConf = "/etc/systemd/timesyncd.conf"
)

var (
optNTPServer []string
optFallbackNTPServer []string
configFile = lineinfile.LineInFile{FilePath: timesyncdConf}
)

type timesyncd struct {
conn *dbus.Conn
props *prop.Properties
}

func getNTPServers() []string {
return getTimesyncdConfigProperty("NTP")
}

func getFallbackNTPServers() []string {
return getTimesyncdConfigProperty("FallbackNTP")
}

func setNTPServer(c *prop.Change) *dbus.Error {
servers, ok := c.Value.([]string)
if !ok {
return dbus.MakeFailedError(fmt.Errorf("invalid type for NTPServer"))
}

value := strings.Join(servers, " ")

if err := setTimesyncdConfigProperty("NTP", value); err != nil {
return dbus.MakeFailedError(err)
}

optNTPServer = servers
return nil
}

func setFallbackNTPServer(c *prop.Change) *dbus.Error {
servers, ok := c.Value.([]string)
if !ok {
return dbus.MakeFailedError(fmt.Errorf("invalid type for FallbackNTPServer"))
}

value := strings.Join(servers, " ")

if err := setTimesyncdConfigProperty("FallbackNTP", value); err != nil {
return dbus.MakeFailedError(err)
}

optFallbackNTPServer = servers
return nil
}

func getTimesyncdConfigProperty(property string) []string {
value, err := configFile.Find(`^\s*(`+property+`=).*$`, `\[Time\]`, true)

var servers []string

if err != nil || value == nil {
return servers
}

matches := regexp.MustCompile(property + `=([^\s#]+(?:\s+[^\s#]+)*)`).FindStringSubmatch(*value)
if len(matches) > 1 {
servers = strings.Split(matches[1], " ")
}

return servers
}

func setTimesyncdConfigProperty(property string, value string) error {
var params = lineinfile.NewPresentParams("NTP=" + value)
params.Regexp, _ = regexp.Compile(`^\s*#?\s*(` + property + `=).*$`)
// Keep it simple, timesyncd.conf only has the [Time] section
params.After = `\[Time\]`
if err := configFile.Present(params); err != nil {
return fmt.Errorf("failed to set %s: %s", property, err)
}

if err := restartTimesyncd(); err != nil {
return fmt.Errorf("failed to restart timesyncd: %s", err)
}

return nil
}

func restartTimesyncd() error {
conn, err := dbus.SystemBus()
if err != nil {
return err
}

obj := conn.Object("org.freedesktop.systemd1", "/org/freedesktop/systemd1")
call := obj.Call("org.freedesktop.systemd1.Manager.RestartUnit", 0, "systemd-timesyncd.service", "replace")
if call.Err != nil {
return call.Err
}

return nil
}

func InitializeDBus(conn *dbus.Conn) {
d := timesyncd{
conn: conn,
}

optNTPServer = getNTPServers()
optFallbackNTPServer = getFallbackNTPServers()

propsSpec := map[string]map[string]*prop.Prop{
ifaceName: {
"NTPServer": {
Value: optNTPServer,
Writable: true,
Emit: prop.EmitTrue,
Callback: setNTPServer,
},
"FallbackNTPServer": {
Value: optFallbackNTPServer,
Writable: true,
Emit: prop.EmitTrue,
Callback: setFallbackNTPServer,
},
},
}

props, err := prop.Export(conn, objectPath, propsSpec)
if err != nil {
logging.Critical.Panic(err)
}
d.props = props

err = conn.Export(d, objectPath, ifaceName)
if err != nil {
logging.Critical.Panic(err)
}

node := &introspect.Node{
Name: objectPath,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
prop.IntrospectData,
{
Name: ifaceName,
Methods: introspect.Methods(d),
Properties: props.Introspection(ifaceName),
},
},
}

err = conn.Export(introspect.NewIntrospectable(node), objectPath, "org.freedesktop.DBus.Introspectable")
if err != nil {
logging.Critical.Panic(err)
}

logging.Info.Printf("Exposing object %s with interface %s ...", objectPath, ifaceName)
}
Loading

0 comments on commit 0f09f2b

Please sign in to comment.