farmbot_os/lib/farmbot/bootstrap/supervisor.ex

157 lines
5.2 KiB
Elixir

defmodule Farmbot.Bootstrap.Supervisor do
@moduledoc """
Bootstraps the application.
It is expected that there is authorization credentials in the application's
environment by this point. This can be configured via a `Farmbot.Init` module.
For example:
# config.exs
use Mix.Config
config :farmbot, :init, [
Farmbot.Configurator
]
config :farmbot, :behaviour,
authorization: Farmbot.Configurator
# farmbot_configurator.ex
defmodule Farmbot.Configurator do
@moduledoc false
@behaviour Farmbot.System.Init
@behaviour Farmbot.Bootstrap.Authorization
# Callback for Farmbot.System.Init.
# This can return {:ok, pid} if it should be a supervisor.
def start_link(_args, _opts) do
creds = [
email: "some_user@some_server.org",
password: "some_secret_password_dont_actually_store_in_plain_text",
server: "https://my.farmbot.io"
]
Application.put_env(:farmbot, :behaviour, creds)
:ignore
end
# Callback for Farmbot.Bootstrap.Authorization.
# Should return `{:ok, token}` where `token` is a binary jwt, or
# {:error, reason} reason can be anything, but a binary is easiest to
# Parse.
def authorize(email, password, server) do
# some intense http stuff or whatever.
{:ok, token}
end
end
This will cause the `creds` to be stored in the application's environment.
This moduld then will try to use the configured module to `authorize`.
If either of these things error, the bot try to factory reset
"""
use Supervisor
alias Farmbot.Bootstrap.Authorization, as: Auth
alias Farmbot.System.ConfigStorage
import ConfigStorage, only: [update_config_value: 4, get_config_value: 3]
use Farmbot.Logger
error_msg = """
Please configure an authorization module!
for example:
config: :farmbot, :behaviour, [
authorization: Farmbot.Bootstrap.Authorization
]
"""
@auth_task Application.get_env(:farmbot, :behaviour)[:authorization]
@auth_task || Mix.raise(error_msg)
@doc "Start Bootstrap services."
def start_link() do
Supervisor.start_link(__MODULE__, [], [name: __MODULE__])
end
def init([]) do
# Make sure we log when amqp is connected.
update_config_value(:bool, "settings", "log_amqp_connected", true)
# try to find the creds.
case get_creds() do
# do the actual supervisor init if we have creds. This may still fail.
{email, pass, server} ->
actual_init(email, pass, server)
# This will cause a factory reset.
{:error, reason} ->
{:error, reason}
end
end
@typedoc "Authorization credentials."
@type auth :: {Auth.email(), Auth.password(), Auth.server()}
@spec get_creds() :: auth | {:error, term}
defp get_creds do
try do
# Fetch email, server, password from Storage.
email = get_config_value(:string, "authorization", "email")
pass = get_config_value(:string, "authorization", "password")
server = get_config_value(:string, "authorization", "server")
# Make sure they aren't empty.
email || Logger.warn 1, "Could not find email in configuration. "
pass || raise "No password provided in config storage."
server || raise "No server provided in config storage"
{email, pass, server}
rescue
e in RuntimeError -> {:error, Exception.message(e)}
e -> reraise(e, System.stacktrace())
end
end
defp actual_init(email, pass, server) do
busy_msg = "Beginning Bootstrap authorization: #{email} - #{server}"
Logger.busy(2, busy_msg)
# get a token
case @auth_task.authorize(email, pass, server) do
{:ok, token} ->
success_msg = "Successful Bootstrap authorization: #{email} - #{server}"
Logger.success(2, success_msg)
update_config_value(:bool, "settings", "first_boot", false)
update_config_value(:string, "authorization", "token", token)
children = [
worker(Farmbot.Bootstrap.AuthTask, []),
supervisor(Farmbot.HTTP.Supervisor, []),
worker(Farmbot.Bootstrap.SettingsSync, []),
supervisor(Farmbot.Firmware.Supervisor, []),
supervisor(Farmbot.BotState.Supervisor, []),
supervisor(Farmbot.BotState.Transport.Supervisor, []),
supervisor(Farmbot.FarmEvent.Supervisor, []),
supervisor(Farmbot.Repo.Supervisor, []),
supervisor(Farmbot.Farmware.Supervisor, []),
supervisor(Farmbot.Regimen.Supervisor, []),
]
opts = [strategy: :one_for_one]
supervise(children, opts)
# I don't actually _have_ to factory reset here. It would get detected ad
# an application start fail and we would factory_reset from there,
# the error message is just way more useful here.
{:error, reason} ->
Farmbot.System.factory_reset(reason)
:ignore
# If we got invalid json, just try again.
# HACK(Connor) Sometimes we don't get valid json
# Just try again if that happened.
{:error, :invalid, _} ->
actual_init(email, pass, server)
end
end
end