-
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for network interface power management
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
Showing
15 changed files
with
825 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.