Skip to content

Commit

Permalink
Add support for network interface power management
Browse files Browse the repository at this point in the history
This adds the VintageNet.PowerManager behaviour and allows projects to
register implementations with VintageNet. VintageNet will call the
implementation to power on and off the hardware as needed. For example,
if a device isn't working, VintageNet can power it off and back on.
  • Loading branch information
fhunleth committed Jul 17, 2020
1 parent 7491c5a commit 55c29f9
Show file tree
Hide file tree
Showing 15 changed files with 825 additions and 5 deletions.
7 changes: 6 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ use Mix.Config
# * resolvconf: don't update the real resolv.conf
# * persistence_dir: use the current directory
# * bin_ip: just fail if anything calls ip rather that run it
# * power_managers: register a manager for test0 so that tests
# that need to validate power management calls can use it.
config :vintage_net,
udhcpc_handler: VintageNetTest.CapturingUdhcpcHandler,
udhcpd_handler: VintageNetTest.CapturingUdhcpdHandler,
interface_renamer: VintageNetTest.CapturingInterfaceRenamer,
resolvconf: "/dev/null",
persistence_dir: "./test_tmp/persistence",
bin_ip: "false"
bin_ip: "false",
power_managers: [
{VintageNetTest.TestPowerManager, [ifname: "test0", hold_on_time: 50, power_off_time: 100]}
]
1 change: 1 addition & 0 deletions lib/vintage_net/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ defmodule VintageNet.Application do
children = [
{VintageNet.PropertyTable, properties: properties, name: VintageNet},
{VintageNet.PredictableInterfaceName, hw_path_ifnames},
VintageNet.PowerManager.Supervisor,
VintageNet.InterfacesMonitor,
{VintageNet.ToElixir.Server, socket_path},
{VintageNet.NameResolver, args},
Expand Down
4 changes: 3 additions & 1 deletion lib/vintage_net/interface.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule VintageNet.Interface do
require Logger

alias VintageNet.Interface.{CommandRunner, RawConfig}
alias VintageNet.PowerManager.PMControl

alias VintageNet.{
Persistence,
Expand Down Expand Up @@ -722,7 +723,8 @@ defmodule VintageNet.Interface do

RouteManager.clear_route(ifname)

# More?
# Hint to Power Management that now's a good time to reset.
PMControl.reset(ifname)
end

defp rm(files) do
Expand Down
127 changes: 127 additions & 0 deletions lib/vintage_net/power_manager.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
defmodule VintageNet.PowerManager do
@moduledoc """
This is a behaviour for implementing platform-specific power management.
From VintageNet's point of view, network devices have the following
lifecycle:
```
powered-off ---> powered-on ---> powering-off ---> powered-off
```
Power management does not necessarily mean controlling the power. The end
effect should be similar, since VintageNet will try to toggle the power off
and on if the network interface doesn't seem to be working. For example,
unloading the kernel module for the network device on "power off" and loading
it on "power on" may have the desired effect of getting a network interface
unstuck.
VintageNet calls functions here based on how it wants to transition a device.
VintageNet maintains the device's power status internally, so implementations
can blindly do what VintageNet tells them too in most cases. Powering on and
off can be asynchronous to these function calls. VintageNet uses the presence
or absence of a networking interface (like "wlan0") to determine when one is
available.
Two timeouts are important to consider:
1. power_off_time
2. power_on_hold_time
The `power_off_time` specifies the time in the `powering-off` state. When a
device is in the `powering-off` state, VintageNet won't bother the device
until that time has expired. That means that if there's a request to use the
device, it will wait the `powering-off` time before calling
`finish_power_off` and then it will power the device back on. This allows
hardware time to gracefully power off and is strongly recommended in the app
notes for many devices.
The `power_on_hold_time` specifies how much time a device should be in the
`powered-on` state before it is ok to power off again. This allows devices
some time to initialize and recover on their own.
While normal Erlang supervision expects that it can restart processes
immediately and without regard to how long they have been running, bad things
can happen to hardware if too aggressively restarted. Devices also initialize
asynchronously so it's hard to know when they're fully available and some
flakiness may be naturally due to VintageNet not knowing how to wait for a
component to finish initialization. Please review your network device's power
management guidelines before too aggressively reducing hold times. Cellular
devices, in particular, want to signal their disconnection from the network
to the tower and flush any unsaved configuration changes to Flash before
power removal.
Here's an example for a cellular device with a reset line connected to it:
* `power_on` - De-assert the reset line. Return a `power_on_hold_time` of 10
minutes
* `start_powering_off` - Open the UART and send the power down command to the
modem. Return a `power_off_time` of 1 minute.
* `power_off` - Assert the reset line.
"""

@doc """
Initialize state for managing the power to the specified interface
This is called on start and if the power management GenServer restarts. It
should not assume that hardware is powered down.
It should call `VintageNet.PowerManager.Registry.register/1` for every
interface name that it handles. This may be called dynamically if interface
names are unknown on init.
"""
@callback init(args :: any()) :: {:ok, state :: any()}

@doc """
Power on the hardware for a network interface
The function should turn on power rails, deassert reset lines, load kernel
modules or do whatever else is necessary to make the interface show up in
Linux.
Failure handling is not supported by VintageNet yet, so if power up can fail
and the right handling for that is to try again later, then this function
should do that.
It is ok for this function to return immediately. When the network interface
appears, VintageNet will start trying to use it.
The return tuple should include the number of seconds VintageNet should wait
before trying to power down the module again. This value should be
sufficiently large to avoid getting into loops where VintageNet gives up on a
network interface before it has initialized. 10 minutes (600 seconds), for
example, is a reasonable setting.
"""
@callback power_on(state :: any()) ::
{:ok, next_state :: any(), hold_time :: pos_integer()}

@doc """
Start powering off the hardware for a network interface
This function should start a graceful shutdown of the network interface
hardware. It may return immediately. The return value specifies how long in
seconds VintageNet should wait before calling `power_off/2`. The idea is that
a graceful power off should be allowed some time to complete, but not
forever.
"""
@callback start_powering_off(state :: any()) ::
{:ok, next_state :: any(), power_off_time :: pos_integer()}

@doc """
Power off the hardware
This function should finish powering off the network interface hardware. Since
this is called after the graceful power down should have completed, it should
forcefully turn off the power to the hardware.
"""
@callback power_off(state :: any()) ::
{:ok, next_state :: any()}

@doc """
Handle other messages
All unknown messages sent to the power management `GenServer` bypass the
state machine and can be `GenServer.handle_info/2` for
"""
@callback handle_info(msg :: any(), state :: any()) :: {:noreply, new_state :: any()}
end
189 changes: 189 additions & 0 deletions lib/vintage_net/power_manager/pm_control.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
defmodule VintageNet.PowerManager.PMControl do
use GenServer
require Logger

@moduledoc """
Power management controller
This GenServer runs a PowerManager implementation for a network device. It
runs `reset` and `power_off` requests through the power management state
machine to filter out transients and help implementations avoid the
complexities around minimal reset intervals and mandatory initialization
wait times.
Functions in this module are intended to be called internally by VintageNet,
but may be called externally if there's a strong reason to believe that
a hardware reset is needed. I.e., maybe there's a button in a UI to force
one or there's a hardware fault indicator. Note that manually calling
`reset` might not immediately force a reset and it may even be ignored
if the mandatory initialization hold time hasn't passed. This is for the
protection of the hardware. `shutdown` calls are never ignored, but a
subsequent `reset` will be needed to restart the module. VintageNet will
eventually call `reset` if the network interface is configured.
"""

alias VintageNet.PowerManager.StateMachine

# Filter out poweroff/resets that appear in <10ms. These are generated by
# programmatically removing and reapplying a configuration. These are a
# consequence of VintageNet's strategy of reapplying configurations always
# and not trying to figure out deltas, even for small stuff. The user almost
# certainly doesn't want to wait through the shutdown timeouts and boot time
# to use the device again and that's unnecessary anyway.
@transient_timeout 10

defmodule State do
@moduledoc false

defstruct [:impl, :impl_args, :impl_state, :sm, :timer_id]
end

@doc """
Start up a server.
Arguments:
* `:impl` - the module that implements PowerManager
* `:impl_args` - arguments to pass to the PowerManager's `init/1` call
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(args) do
GenServer.start_link(__MODULE__, args, [])
end

@doc """
Reset the power
This is called when an interface gets an error that might be fixable
by cycling power. The current state in the power management state
machine determines how this is handled.
"""
@spec reset(VintageNet.ifname()) :: :ok
def reset(ifname) do
cast(ifname, :reset)
end

@doc """
Power off
This is called when VintageNet stops using an interface. The current state in the power management state
machine determines how this is handled. For example, the power could already be off.
"""
@spec power_off(VintageNet.ifname()) :: :ok
def power_off(ifname) do
cast(ifname, :power_off)
end

@doc """
Send an arbitrary message to the power manager for an interface
This will be received by the PowerManager's `handle_info/2` callback.
"""
@spec send_message(VintageNet.ifname(), any()) :: any()
def send_message(ifname, message) do
case VintageNet.PowerManager.Registry.lookup(ifname) do
{:ok, pid} ->
send(pid, message)

_error ->
false
end
end

defp cast(ifname, message) do
case VintageNet.PowerManager.Registry.lookup(ifname) do
{:ok, pid} ->
GenServer.cast(pid, message)

_error ->
:ok
end
end

@impl GenServer
def init(opts) do
state = struct(State, opts) |> Map.put(:sm, StateMachine.init())
{:ok, state, {:continue, :init}}
end

@impl GenServer
def handle_continue(:init, state) do
{:ok, impl_state} = state.impl.init(state.impl_args)

{:noreply, %{state | impl_state: impl_state}}
end

@impl GenServer
def handle_cast(:reset, state) do
{new_sm, actions} = StateMachine.reset(state.sm)

new_state = Enum.reduce(actions, %{state | sm: new_sm}, &run_action/2)

{:noreply, new_state}
end

def handle_cast(:power_off, state) do
{new_sm, actions} = StateMachine.shutdown(state.sm)

new_state = Enum.reduce(actions, %{state | sm: new_sm}, &run_action/2)

{:noreply, new_state}
end

@impl GenServer
def handle_info({:server_timeout, timer_id}, %{timer_id: timer_id} = state) do
{new_sm, actions} = StateMachine.timeout(state.sm)

new_state = Enum.reduce(actions, %{state | sm: new_sm, timer_id: nil}, &run_action/2)

{:noreply, new_state}
end

def handle_info({:server_timeout, _timer_id}, state) do
# Ignore old timeouts
{:noreply, state}
end

def handle_info(msg, state) do
{:noreply, new_impl_state} = state.impl.handle_info(msg, state.impl_state)

{:noreply, %{state | impl_state: new_impl_state}}
end

defp run_action(:start_powering_off, state) do
log("Start powering off")
{:ok, new_impl_state, shutdown_time} = state.impl.start_powering_off(state.impl_state)

%{state | impl_state: new_impl_state, timer_id: start_timer(shutdown_time)}
end

defp run_action(:power_off, state) do
log("Complete power off")
{:ok, new_impl_state} = state.impl.power_off(state.impl_state)

%{state | impl_state: new_impl_state}
end

defp run_action(:power_on, state) do
log("Powering on")
{:ok, new_impl_state, hold_time} = state.impl.power_on(state.impl_state)

%{state | impl_state: new_impl_state, timer_id: start_timer(hold_time)}
end

defp run_action(:start_transient_timer, state) do
%{state | timer_id: start_timer(@transient_timeout)}
end

defp start_timer(millis) do
timer_id = make_ref()
Process.send_after(self(), {:server_timeout, timer_id}, millis)
timer_id
end

defp log(message) do
ifnames = VintageNet.PowerManager.Registry.ifnames() |> Enum.intersperse(",")

Logger.info(["PMControl(", ifnames, "): ", message])
end
end
Loading

0 comments on commit 55c29f9

Please sign in to comment.