Skip to content

Commit

Permalink
Add runtime server configuration
Browse files Browse the repository at this point in the history
Changing the list of servers restarts `ntpd`. This also adds support for
supplying an empty server list to disable `ntpd` until the servers are
known.
  • Loading branch information
fhunleth committed Aug 12, 2018
1 parent 745c255 commit 62c2340
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 31 deletions.
58 changes: 53 additions & 5 deletions lib/nerves_time.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,64 @@ defmodule Nerves.Time do
`Nerves.Time` keeps the system clock on [Nerves](http://nerves-project.org)
devices in sync when connected to the network and close to in sync when
disconnected. It's especially useful for devices lacking a [Battery-backed
real-time clock](https://en.wikipedia.org/wiki/Real-time_clock) and will advance
the clock at startup to a reasonable guess.
real-time clock](https://en.wikipedia.org/wiki/Real-time_clock) and will
advance the clock at startup to a reasonable guess.
"""

@doc """
Check whether NTP is synchronized with the configured NTP servers.
It's possible that the time is already set correctly when this
returns false. Nerves.Time decides that NTP is synchronized when
ntpd sends a notification that the device's clock stratum is 4 or less.
It's possible that the time is already set correctly when this returns false.
`Nerves.Time` decides that NTP is synchronized when `ntpd` sends a
notification that the device's clock stratum is 4 or less. Clock adjustments
occur before this, though.
"""
@spec synchronized?() :: boolean()
defdelegate synchronized?, to: Nerves.Time.Ntpd

@doc """
Set the list of NTP servers.
Use this function to replace the list of NTP servers that are queried for
time. It is also possible to set this list in your `config.exs` by doing
something like the following:
```elixir
config :nerves_time, :servers, [
"0.pool.ntp.org",
"1.pool.ntp.org",
"2.pool.ntp.org",
"3.pool.ntp.org"
]
```
`Nerves.Time` uses [NTP Pool](https://www.ntppool.org/en/) by default. To
disable this and configure servers solely at runtime, specify an empty list
in `config.exs`:
```elixir
config :nerves_time, :servers, []
```
"""
@spec set_ntp_servers([String.t()]) :: :ok
defdelegate set_ntp_servers(servers), to: Nerves.Time.Ntpd

@doc """
Return the current NTP servers.
"""
@spec ntp_servers() :: [String.t()] | {:error, term()}
defdelegate ntp_servers(), to: Nerves.Time.Ntpd

@doc """
Manually restart the NTP daemon.
This is normally not necessary since `Nerves.Time` handles restarting it
automatically. An example of a reason to call this function is if you know
when the Internet becomes available. For this case, calling `restart_ntp`
will cancel `ntpd`'s internal timeouts and cause it to immediately send time
requests. If using NTP Pool, be sure not to violate its terms of service by
calling this function too frequently.
"""
@spec restart_ntpd() :: :ok | {:error, term()}
defdelegate restart_ntpd(), to: Nerves.Time.Ntpd
end
104 changes: 78 additions & 26 deletions lib/nerves_time/ntpd.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,57 @@ defmodule Nerves.Time.Ntpd do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end

@spec synchronized?() :: true | false
@spec synchronized?() :: boolean()
def synchronized?() do
GenServer.call(__MODULE__, :synchronized?)
end

@spec set_ntp_servers([String.t()]) :: :ok
def set_ntp_servers(servers) when is_list(servers) do
GenServer.call(__MODULE__, {:set_ntp_servers, servers})
end

@spec ntp_servers() :: [String.t()] | {:error, term()}
def ntp_servers() do
GenServer.call(__MODULE__, :ntp_servers)
end

@spec restart_ntpd() :: :ok | {:error, term()}
def restart_ntpd() do
GenServer.call(__MODULE__, :restart_ntpd)
end

@spec init(any()) :: {:ok, any()}
def init(_args) do
state = %State{port: run_ntpd()}
{:ok, state}
{:ok, do_restart_ntpd(%State{})}
end

def handle_call(:synchronized?, _from, state) do
{:reply, state.synchronized, state}
end

def handle_call({:set_ntp_servers, servers}, _from, state) do
Application.put_env(:nerves_time, :servers, servers)
new_state = do_restart_ntpd(state)
{:reply, :ok, new_state}
end

def handle_call(:restart_ntpd, _from, state) do
new_state = do_restart_ntpd(state)

result =
case new_state.port do
nil -> {:error, "No NTP servers defined"}
_ -> :ok
end

{:reply, result, new_state}
end

def handle_call(:ntp_servers, _from, state) do
{:reply, get_ntp_servers(), state}
end

def handle_info({_, {:exit_status, code}}, state) do
Logger.error("ntpd exited with code: #{code}!")
{:stop, :ntpd_died, state}
Expand All @@ -50,26 +86,8 @@ defmodule Nerves.Time.Ntpd do
|> handle_ntpd(state)
end

defp run_ntpd() do
ntpd_path = Application.get_env(:nerves_time, :ntpd, @default_ntpd_path)
servers = Application.get_env(:nerves_time, :servers, @default_ntp_servers)
ntpd_script_path = Application.app_dir(:nerves_time, "priv/ntpd_script")

args = [ntpd_path, "-n", "-d", "-S", ntpd_script_path] ++ server_args(servers)

Logger.debug("Running ntp as: #{inspect(args)}")

# Call ntpd using muontrap. Muontrap will kill ntpd if this GenServer
# crashes.

Port.open({:spawn_executable, MuonTrap.muontrap_path()}, [
{:args, ["--" | args]},
:exit_status,
:use_stdio,
:binary,
{:line, 2048},
:stderr_to_stdout
])
defp get_ntp_servers() do
Application.get_env(:nerves_time, :servers, @default_ntp_servers)
end

defp server_args(servers) do
Expand All @@ -87,14 +105,13 @@ defmodule Nerves.Time.Ntpd do
end

defp handle_ntpd({:unsync, _result}, state) do
Logger.error("ntpd reports that it is unsynchronized; relaunching")
Logger.error("ntpd reports that it is unsynchronized; restarting")

# According to the Busybox ntpd docs, if you get an `unsync` notification, then
# you should restart ntpd to be safe. This is stated to be due to name resolution
# only being done at initialization.
Port.close(state.port)
new_state = do_restart_ntpd(state)

new_state = %{state | port: run_ntpd(), synchronized: false}
{:noreply, new_state}
end

Expand All @@ -108,6 +125,41 @@ defmodule Nerves.Time.Ntpd do
{:noreply, state}
end

defp do_restart_ntpd(state) do
if is_port(state.port) do
Port.close(state.port)
end

ntpd = maybe_start_ntpd(get_ntp_servers())

%{state | port: ntpd, synchronized: false}
end

defp maybe_start_ntpd([]) do
Logger.debug("Not starting ntpd since no NTP servers.")
nil
end

defp maybe_start_ntpd(servers) do
ntpd_path = Application.get_env(:nerves_time, :ntpd, @default_ntpd_path)
ntpd_script_path = Application.app_dir(:nerves_time, "priv/ntpd_script")

args = [ntpd_path, "-n", "-d", "-S", ntpd_script_path] ++ server_args(servers)

Logger.debug("Starting ntpd as: #{inspect(args)}")

# Call ntpd using muontrap. Muontrap will kill ntpd if this GenServer
# crashes.
Port.open({:spawn_executable, MuonTrap.muontrap_path()}, [
{:args, ["--" | args]},
:exit_status,
:use_stdio,
:binary,
{:line, 2048},
:stderr_to_stdout
])
end

defp maybe_update_clock(%{stratum: stratum})
when stratum <= 4 do
# Update the time assuming that we're getting time from a decent clock.
Expand Down

0 comments on commit 62c2340

Please sign in to comment.