From 56f6749595f3e9895a06fbaa32204824426918b2 Mon Sep 17 00:00:00 2001 From: Connor Rigby Date: Wed, 28 Nov 2018 12:12:25 -0800 Subject: [PATCH] Nerves hub pt2 (#654) * Rework Networking * Rework Configurator * Fix NervesHub not connecting --- .circleci/config.yml | 74 ------ farmbot_core/lib/asset.ex | 10 + farmbot_core/lib/asset/device_cert.ex | 36 +++ .../lib/asset_workers/fbos_config_worker.ex | 19 -- farmbot_core/lib/bot_state/bot_state.ex | 37 ++- .../bot_state_ng/informational_settings.ex | 7 +- .../lib/config_storage/network_interface.ex | 3 + .../lib/init => farmbot_core/lib}/ecto.ex | 83 +++--- farmbot_core/lib/farmbot_core.ex | 14 +- ...181019181000_create_device_certs_table.exs | 13 + farmbot_core/test/bot_state_ng_test.exs | 18 ++ farmbot_ext/config/config.exs | 2 +- farmbot_ext/config/ecto.exs | 3 + farmbot_ext/lib/amqp/auto_sync_transport.ex | 73 ++++-- farmbot_ext/lib/amqp/bot_state_transport.ex | 41 ++- .../lib/amqp/celery_script_transport.ex | 56 +++-- farmbot_ext/lib/amqp/channel_supervisor.ex | 12 +- farmbot_ext/lib/amqp/connection_worker.ex | 53 ++-- farmbot_ext/lib/amqp/log_transport.ex | 54 ++-- farmbot_ext/lib/amqp/nerves_hub_transport.ex | 102 ++++++++ farmbot_ext/lib/api.ex | 4 +- farmbot_ext/lib/api/dirty_worker.ex | 2 +- .../lib/api/dirty_worker/supervisor.ex | 2 + farmbot_ext/lib/bootstrap.ex | 58 +++++ farmbot_ext/lib/bootstrap/authorization.ex | 6 +- farmbot_ext/lib/bootstrap/supervisor.ex | 137 +--------- farmbot_ext/lib/farmbot_ext.ex | 2 +- farmbot_os/config/config.exs | 10 +- farmbot_os/config/host/dev.exs | 3 - farmbot_os/config/host/test.exs | 1 + farmbot_os/config/target/dev.exs | 58 +++-- farmbot_os/config/target/prod.exs | 58 +++-- farmbot_os/config/target/rpi3.exs | 2 + farmbot_os/lib/core_start.ex | 31 --- farmbot_os/lib/ext_start.ex | 34 --- farmbot_os/lib/farmbot_os.ex | 2 - farmbot_os/lib/firmware/auto_detector.ex | 3 + farmbot_os/lib/init/supervisor.ex | 4 +- farmbot_os/lib/nerves_hub.ex | 18 +- farmbot_os/lib/shoehorn_handler.ex | 31 --- farmbot_os/mix.exs | 9 +- farmbot_os/mix.lock.rpi | 3 +- farmbot_os/mix.lock.rpi3 | 1 + .../target/configurator/configurator.ex | 115 --------- .../platform/target/configurator/router.ex | 119 +++++---- .../target/configurator/supervisor.ex | 23 ++ .../platform/target/configurator/validator.ex | 108 ++++++++ .../{disk_usage_worker.ex => disk_usage.ex} | 2 +- ...memory_usage_worker.ex => memory_usage.ex} | 2 +- .../{soc_temp_worker.ex => soc_temp.ex} | 8 +- .../target/info_workers/supervisor.ex | 28 +++ .../{uptime_worker.ex => uptime.ex} | 4 +- .../target/info_workers/wifi_level.ex | 42 ++++ farmbot_os/platform/target/network.ex | 238 ++++++++++++++++++ .../platform/target/network/distribution.ex | 175 +++++++++++++ .../target/network/info_supervisor.ex | 19 -- .../platform/target/network/info_worker.ex | 31 --- .../target/network/network_not_found_timer.ex | 100 -------- .../platform/target/network/supervisor.ex | 35 +++ farmbot_os/platform/target/network/utils.ex | 141 +++++++++++ .../platform/target/network/wait_for_time.ex | 38 --- .../target/{network => }/tzdata_task.ex | 2 +- farmbot_os/rel/vm.args | 41 ++- farmbot_os/rootfs_overlay/etc/iex.exs | 23 +- 64 files changed, 1523 insertions(+), 960 deletions(-) create mode 100644 farmbot_core/lib/asset/device_cert.ex rename {farmbot_os/lib/init => farmbot_core/lib}/ecto.ex (53%) create mode 100644 farmbot_core/priv/asset/migrations/20181019181000_create_device_certs_table.exs create mode 100644 farmbot_ext/lib/amqp/nerves_hub_transport.ex create mode 100644 farmbot_ext/lib/bootstrap.ex create mode 100644 farmbot_os/config/target/rpi3.exs delete mode 100644 farmbot_os/lib/core_start.ex delete mode 100644 farmbot_os/lib/ext_start.ex create mode 100644 farmbot_os/lib/firmware/auto_detector.ex delete mode 100644 farmbot_os/lib/shoehorn_handler.ex delete mode 100644 farmbot_os/platform/target/configurator/configurator.ex create mode 100644 farmbot_os/platform/target/configurator/supervisor.ex create mode 100644 farmbot_os/platform/target/configurator/validator.ex rename farmbot_os/platform/target/info_workers/{disk_usage_worker.ex => disk_usage.ex} (94%) rename farmbot_os/platform/target/info_workers/{memory_usage_worker.ex => memory_usage.ex} (91%) rename farmbot_os/platform/target/info_workers/{soc_temp_worker.ex => soc_temp.ex} (77%) create mode 100644 farmbot_os/platform/target/info_workers/supervisor.ex rename farmbot_os/platform/target/info_workers/{uptime_worker.ex => uptime.ex} (87%) create mode 100644 farmbot_os/platform/target/info_workers/wifi_level.ex create mode 100644 farmbot_os/platform/target/network.ex create mode 100644 farmbot_os/platform/target/network/distribution.ex delete mode 100644 farmbot_os/platform/target/network/info_supervisor.ex delete mode 100644 farmbot_os/platform/target/network/info_worker.ex delete mode 100644 farmbot_os/platform/target/network/network_not_found_timer.ex create mode 100644 farmbot_os/platform/target/network/supervisor.ex create mode 100644 farmbot_os/platform/target/network/utils.ex delete mode 100644 farmbot_os/platform/target/network/wait_for_time.ex rename farmbot_os/platform/target/{network => }/tzdata_task.ex (91%) diff --git a/.circleci/config.yml b/.circleci/config.yml index c50e7e12..7c9256e4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -56,15 +56,6 @@ build_firmware_steps: &build_firmware_steps - run: name: Setup ENV command: | -<<<<<<< HEAD - echo $MIX_TARGET > MIX_TARGET - echo $MIX_ENV > MIX_ENV - cp mix.lock MIX_LOCK - - restore_cache: - key: v9-fbos-{{ checksum "MIX_TARGET" }}-{{ checksum "MIX_ENV" }}-dependency-cache-{{ checksum "MIX_LOCK" }} - - restore_cache: - key: v9-fbos-host-test-dependency-cache-{{ checksum "mix.lock" }} -======= echo "/nerves/build/farmbot_os/$MIX_TARGET" > MIX_TARGET echo "/nerves/build/farmbot_os/$MIX_ENV" > MIX_ENV cp /nerves/build/farmbot_os/mix.lock.$MIX_TARGET MIX_LOCK @@ -72,7 +63,6 @@ build_firmware_steps: &build_firmware_steps key: v9-fbos-{{ checksum "MIX_TARGET" }}-{{ checksum "MIX_ENV" }}-dependency-cache-{{ checksum "MIX_LOCK" }} - restore_cache: key: v9-fbos-host-test-dependency-cache-{{ checksum "mix.lock.host" }} ->>>>>>> e9b01741... [WIP] Pull in NervesHub updates - <<: *install_elixir - <<: *install_hex_archives - run: @@ -88,11 +78,7 @@ build_firmware_steps: &build_firmware_steps - run: name: Create artifacts command: | -<<<<<<< HEAD - cp _build/${MIX_TARGET}_${MIX_ENV}/nerves/images/farmbot.fw /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw -======= cp /nerves/build/farmbot_os/_build/${MIX_TARGET}/${MIX_ENV}/nerves/images/farmbot.fw /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw ->>>>>>> e9b01741... [WIP] Pull in NervesHub updates - save_cache: key: v9-fbos-{{ checksum "MIX_TARGET" }}-{{ checksum "MIX_ENV" }}-dependency-cache-{{ checksum "MIX_LOCK" }} paths: @@ -111,15 +97,9 @@ deploy_nerves_hub_firmware_steps: &deploy_nerves_hub_firmware_steps - run: name: Setup ENV command: | -<<<<<<< HEAD - echo $MIX_TARGET > MIX_TARGET - echo $MIX_ENV > MIX_ENV - cp mix.lock MIX_LOCK -======= echo "/nerves/build/farmbot_os/$MIX_TARGET" > MIX_TARGET echo "/nerves/build/farmbot_os/$MIX_ENV" > MIX_ENV cp /nerves/build/farmbot_os/mix.lock.$MIX_TARGET MIX_LOCK ->>>>>>> e9b01741... [WIP] Pull in NervesHub updates - restore_cache: key: v9-fbos-{{ checksum "MIX_TARGET" }}-{{ checksum "MIX_ENV" }}-dependency-cache-{{ checksum "MIX_LOCK" }} - restore_cache: @@ -174,12 +154,8 @@ jobs: environment: MIX_ENV: test MIX_TARGET: host -<<<<<<< HEAD - ELIXIR_VERSION: 1.8.1 -======= NERVES_LOG_DISABLE_PROGRESS_BAR: "yes" ELIXIR_VERSION: 1.7.3 ->>>>>>> e9b01741... [WIP] Pull in NervesHub updates steps: - checkout - restore_cache: @@ -213,9 +189,6 @@ jobs: - checkout - run: git submodule update --init --recursive - restore_cache: -<<<<<<< HEAD - key: v9-fbos-host-test-dependency-cache-{{ checksum "mix.lock" }} -======= keys: - v9-fbcore-test-dependency-cache-{{ checksum "farmbot_core/mix.lock" }} - restore_cache: @@ -296,7 +269,6 @@ jobs: - run: git submodule update --init --recursive - restore_cache: key: v9-fbos-host-test-dependency-cache-{{ checksum "farmbot_os/mix.lock.host" }} ->>>>>>> e9b01741... [WIP] Pull in NervesHub updates - <<: *install_elixir - <<: *install_hex_archives - run: @@ -308,17 +280,10 @@ jobs: mix format --check-formatted mix test - save_cache: -<<<<<<< HEAD - key: v9-fbos-host-test-dependency-cache-{{ checksum "mix.lock" }} - paths: - - _build/host_test - - deps -======= key: v9-fbos-host-test-dependency-cache-{{ checksum "farmbot_os/mix.lock.host" }} paths: - farmbot_os/_build/host - farmbot_os/deps/host ->>>>>>> e9b01741... [WIP] Pull in NervesHub updates ################################################################################ # target=rpi app_env=prod # @@ -433,22 +398,14 @@ jobs: name: Setup ENV command: | echo rpi3 > MIX_TARGET_RPI3 -<<<<<<< HEAD - cp mix.lock MIX_LOCK_RPI3 -======= cp /nerves/build/farmbot_os/mix.lock.rpi3 MIX_LOCK_RPI3 ->>>>>>> e9b01741... [WIP] Pull in NervesHub updates # echo rpi > MIX_TARGET_RPI # cp /nerves/build/farmbot_os/mix.lock.rpi MIX_LOCK_RPI echo $MIX_ENV > MIX_ENV - restore_cache: key: nerves/deploy/system-{{ checksum "MIX_TARGET_RPI3" }}-{{ .Branch }}-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }} # - restore_cache: -<<<<<<< HEAD - # key: nerves/deploy/system-{{ checksum "MIX_TARGET_RPI" }}-{{ .Branch }}-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }} -======= # key: nerves/deploy/system-{{ checksum "MIX_TARGET_RPI" }}-{{ .Branch }}-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }} ->>>>>>> e9b01741... [WIP] Pull in NervesHub updates - <<: *install_elixir - <<: *install_hex_archives - <<: *install_ghr @@ -497,11 +454,7 @@ jobs: name: Setup ENV command: | echo rpi3 > MIX_TARGET_RPI3 -<<<<<<< HEAD - cp mix.lock MIX_LOCK_RPI3 -======= cp /nerves/build/farmbot_os/mix.lock.rpi3 MIX_LOCK_RPI3 ->>>>>>> e9b01741... [WIP] Pull in NervesHub updates # echo rpi > MIX_TARGET_RPI # cp /nerves/build/farmbot_os/mix.lock.rpi MIX_LOCK_RPI echo $MIX_ENV > MIX_ENV @@ -695,32 +648,6 @@ workflows: # - build_rpi_prod - build_rpi3_prod -<<<<<<< HEAD - - # # staging branch to staging.farmbot.io - # nerves_hub_rpi_prod_staging_staging: - # jobs: - # # - build_rpi_prod: - # # context: farmbot-staging - # # filters: - # # branches: - # # only: - # # - staging - # - build_rpi3_prod: - # context: farmbot-staging - # filters: - # branches: - # only: - # - staging - # # - deploy_rpi_prod_staging: - # # context: farmbot-staging - # # requires: - # # - build_rpi_prod - # - deploy_rpi3_prod_staging: - # context: farmbot-staging - # requires: - # - build_rpi3_prod -======= # staging branch to staging.farmbot.io nerves_hub_rpi_prod_staging_staging: jobs: @@ -822,4 +749,3 @@ workflows: # - next # requires: # - build_rpi_prod ->>>>>>> e9b01741... [WIP] Pull in NervesHub updates diff --git a/farmbot_core/lib/asset.ex b/farmbot_core/lib/asset.ex index bbb7658f..52c80693 100644 --- a/farmbot_core/lib/asset.ex +++ b/farmbot_core/lib/asset.ex @@ -2,6 +2,7 @@ defmodule Farmbot.Asset do alias Farmbot.Asset.{ Repo, Device, + DeviceCert, DiagnosticDump, FarmwareEnv, FarmwareInstallation, @@ -166,4 +167,13 @@ defmodule Farmbot.Asset do end ## End DiagnosticDump + + ## Begin DeviceCert + + def new_device_cert(params) do + DeviceCert.changeset(%DeviceCert{}, params) + |> Repo.insert() + end + + ## End DeviceCert end diff --git a/farmbot_core/lib/asset/device_cert.ex b/farmbot_core/lib/asset/device_cert.ex new file mode 100644 index 00000000..fb4114e0 --- /dev/null +++ b/farmbot_core/lib/asset/device_cert.ex @@ -0,0 +1,36 @@ +defmodule Farmbot.Asset.DeviceCert do + @moduledoc """ + DeviceCerts describe a connection to NervesHub + """ + + use Farmbot.Asset.Schema, path: "/api/device_cert" + + schema "device_certs" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:serial_number, :string) + field(:tags, {:array, :string}) + + timestamps() + end + + view device_cert do + %{ + id: device_cert.id, + serial_number: device_cert.serial_number, + tags: device_cert.tags + } + end + + def changeset(device_cert, params \\ %{}) do + device_cert + |> cast(params, [:id, :serial_number, :tags, :created_at, :updated_at]) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset_workers/fbos_config_worker.ex b/farmbot_core/lib/asset_workers/fbos_config_worker.ex index 0ab348cb..187b8571 100644 --- a/farmbot_core/lib/asset_workers/fbos_config_worker.ex +++ b/farmbot_core/lib/asset_workers/fbos_config_worker.ex @@ -16,7 +16,6 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FbosConfig do end def handle_info(:timeout, %FbosConfig{} = fbos_config) do - maybe_reinit_firmware(fbos_config) bool("arduino_debug_messages", fbos_config.arduino_debug_messages) bool("auto_sync", fbos_config.auto_sync) bool("beta_opt_in", fbos_config.beta_opt_in) @@ -50,22 +49,4 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FbosConfig do update_config_value(:float, "settings", key, val / 1) :ok = Farmbot.BotState.set_config_value(key, val) end - - defp maybe_reinit_firmware(%FbosConfig{firmware_hardware: nil}) do - :ok - end - - defp maybe_reinit_firmware(%FbosConfig{firmware_path: nil}) do - :ok - end - - defp maybe_reinit_firmware(%FbosConfig{}) do - alias Farmbot.Firmware - alias Farmbot.Core.FirmwareSupervisor - - if is_nil(Process.whereis(Firmware)) do - Logger.warn("Starting Farmbot firmware") - FirmwareSupervisor.reinitialize() - end - end end diff --git a/farmbot_core/lib/bot_state/bot_state.ex b/farmbot_core/lib/bot_state/bot_state.ex index a103b79b..ddea8683 100644 --- a/farmbot_core/lib/bot_state/bot_state.ex +++ b/farmbot_core/lib/bot_state/bot_state.ex @@ -58,11 +58,24 @@ defmodule Farmbot.BotState do GenServer.call(bot_state_server, {:set_firmware_locked, false}) end + @doc "Sets informational_settings.status" def set_sync_status(bot_state_server \\ __MODULE__, s) when s in ["syncing", "synced", "error"] do GenServer.call(bot_state_server, {:set_sync_status, s}) end + @doc "sets informational_settings.update_available" + def set_update_available(bot_state_server \\ __MODULE__, bool) + when is_boolean(bool) do + GenServer.call(bot_state_server, {:set_update_available, bool}) + end + + @doc "sets informational_settings.node_name" + def set_node_name(bot_state_server \\ __MODULE__, node_name) + when is_binary(node_name) do + GenServer.call(bot_state_server, {:set_node_name, node_name}) + end + @doc "Fetch the current state." def fetch(bot_state_server \\ __MODULE__) do GenServer.call(bot_state_server, :fetch) @@ -209,6 +222,26 @@ defmodule Farmbot.BotState do {:reply, reply, state} end + def handle_call({:set_update_available, bool}, _from, state) do + change = %{informational_settings: %{update_available: bool}} + + {reply, state} = + BotStateNG.changeset(state.tree, change) + |> dispatch_and_apply(state) + + {:reply, reply, state} + end + + def handle_call({:set_node_name, node_name}, _from, state) do + change = %{informational_settings: %{node_name: node_name}} + + {reply, state} = + BotStateNG.changeset(state.tree, change) + |> dispatch_and_apply(state) + + {:reply, reply, state} + end + def handle_call({:report_disk_usage, percent}, _form, state) do change = %{informational_settings: %{disk_usage: percent}} @@ -219,7 +252,7 @@ defmodule Farmbot.BotState do {:reply, reply, state} end - def handle_call({:memory_usage, megabytes}, _form, state) do + def handle_call({:report_memory_usage, megabytes}, _form, state) do change = %{informational_settings: %{memory_usage: megabytes}} {reply, state} = @@ -239,7 +272,7 @@ defmodule Farmbot.BotState do {:reply, reply, state} end - def handle_call({:uptime, seconds}, _form, state) do + def handle_call({:report_uptime, seconds}, _form, state) do change = %{informational_settings: %{uptime: seconds}} {reply, state} = diff --git a/farmbot_core/lib/bot_state_ng/informational_settings.ex b/farmbot_core/lib/bot_state_ng/informational_settings.ex index d4e6860e..9ae6ca89 100644 --- a/farmbot_core/lib/bot_state_ng/informational_settings.ex +++ b/farmbot_core/lib/bot_state_ng/informational_settings.ex @@ -26,6 +26,7 @@ defmodule Farmbot.BotStateNG.InformationalSettings do field(:last_status, :string) field(:cache_bust, :integer) field(:busy, :boolean) + field(:update_available, :boolean, default: false) end def new do @@ -52,7 +53,8 @@ defmodule Farmbot.BotStateNG.InformationalSettings do locked: informational_settings.locked, last_status: informational_settings.last_status, cache_bust: informational_settings.cache_bust, - busy: informational_settings.busy + busy: informational_settings.busy, + update_available: informational_settings.update_available } end @@ -75,7 +77,8 @@ defmodule Farmbot.BotStateNG.InformationalSettings do :locked, :last_status, :cache_bust, - :busy + :busy, + :update_available ]) end end diff --git a/farmbot_core/lib/config_storage/network_interface.ex b/farmbot_core/lib/config_storage/network_interface.ex index 41a052be..b885a033 100644 --- a/farmbot_core/lib/config_storage/network_interface.ex +++ b/farmbot_core/lib/config_storage/network_interface.ex @@ -19,7 +19,9 @@ defmodule Farmbot.Config.NetworkInterface do field(:password, :string) # Advanced settings. + # this should be ipv4_address_method field(:ipv4_method, :string) + field(:ipv4_address, :string) field(:ipv4_gateway, :string) field(:ipv4_subnet_mask, :string) @@ -36,6 +38,7 @@ defmodule Farmbot.Config.NetworkInterface do config |> cast(params, @required_fields) |> validate_required(@required_fields) + |> validate_inclusion(:type, ["wireless", "wired"]) |> unique_constraint(:name) end end diff --git a/farmbot_os/lib/init/ecto.ex b/farmbot_core/lib/ecto.ex similarity index 53% rename from farmbot_os/lib/init/ecto.ex rename to farmbot_core/lib/ecto.ex index 025441e0..eb2b5c45 100644 --- a/farmbot_os/lib/init/ecto.ex +++ b/farmbot_core/lib/ecto.ex @@ -1,62 +1,20 @@ -defmodule Farmbot.System.Init.Ecto do - @moduledoc "Init module for bringup and teardown of ecto." - use Supervisor - - @doc "This will run migrations on all Farmbot Repos." - def start_link(args) do - Supervisor.start_link(__MODULE__, args, name: __MODULE__) +defmodule Farmbot.EctoMigrator do + def child_spec(_opts) do + %{ + id: __MODULE__, + start: {__MODULE__, :migrate, []}, + type: :worker, + restart: :transient, + shutdown: 500 + } end - def init([]) do - migrate() - :ignore - end - - @doc "Replacement for Mix.Tasks.Ecto.Create" - def setup do - repos = Application.get_env(:farmbot, :ecto_repos) - - for repo <- repos do - Application.put_env(:farmbot, :repo_hack, repo) - setup(repo) - end - end - - def setup(repo) do - db_file = Application.get_env(:farmbot, repo)[:database] - - unless File.exists?(db_file) do - :ok = repo.__adapter__.storage_up(repo.config) - end - end - - @doc "Replacement for Mix.Tasks.Ecto.Drop" - def drop do - repos = Application.get_env(:farmbot, :ecto_repos) - - for repo <- repos do - case drop(repo) do - :ok -> :ok - {:error, :already_down} -> :ok - {:error, reason} -> raise reason - end - end - end - - def drop(repo) do - repo.__adapter__.storage_down(repo.config) - end @doc "Replacement for Mix.Tasks.Ecto.Migrate" def migrate do - repos = Application.get_env(:farmbot, :ecto_repos) - Application.put_env(:farmbot, :repo_hack, nil) - - for repo <- repos do - Application.put_env(:farmbot, :repo_hack, repo) - # setup(repo) - migrate(repo) - end + repos = Application.get_env(:farmbot_core, :ecto_repos) + for repo <- repos, do: migrate(repo) + :ignore end def migrate(Farmbot.Asset.Repo) do @@ -81,4 +39,21 @@ defmodule Farmbot.System.Init.Ecto do Mix.Ecto.restart_apps_if_migrated(apps, migrated) Process.sleep(500) end + + @doc "Replacement for Mix.Tasks.Ecto.Drop" + def drop do + repos = Application.get_env(:farmbot_core, :ecto_repos) + + for repo <- repos do + case drop(repo) do + :ok -> :ok + {:error, :already_down} -> :ok + {:error, reason} -> raise reason + end + end + end + + def drop(repo) do + repo.__adapter__.storage_down(repo.config) + end end diff --git a/farmbot_core/lib/farmbot_core.ex b/farmbot_core/lib/farmbot_core.ex index b9f6fb59..25cb8566 100644 --- a/farmbot_core/lib/farmbot_core.ex +++ b/farmbot_core/lib/farmbot_core.ex @@ -9,13 +9,15 @@ defmodule Farmbot.Core do def start(_, args), do: Supervisor.start_link(__MODULE__, args, name: __MODULE__) def init([]) do + children = [ - {Farmbot.BotState, []}, - {Farmbot.Logger.Supervisor, []}, - {Farmbot.Config.Supervisor, []}, - {Farmbot.Asset.Supervisor, []}, - {Farmbot.Core.FirmwareSupervisor, []}, - {Farmbot.Core.CeleryScript.Supervisor, []}, + Farmbot.EctoMigrator, + Farmbot.BotState, + Farmbot.Logger.Supervisor, + Farmbot.Config.Supervisor, + Farmbot.Asset.Supervisor, + Farmbot.Core.FirmwareSupervisor, + Farmbot.Core.CeleryScript.Supervisor, ] Supervisor.init(children, [strategy: :one_for_all]) end diff --git a/farmbot_core/priv/asset/migrations/20181019181000_create_device_certs_table.exs b/farmbot_core/priv/asset/migrations/20181019181000_create_device_certs_table.exs new file mode 100644 index 00000000..0bb0b360 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181019181000_create_device_certs_table.exs @@ -0,0 +1,13 @@ +defmodule Elixir.Farmbot.Asset.Repo.Migrations.CreateDeviceCertsTable do + use Ecto.Migration + + def change do + create table("device_certs", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:serial_number, :string) + add(:tags, {:array, :string}) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/test/bot_state_ng_test.exs b/farmbot_core/test/bot_state_ng_test.exs index b705b3fb..28b15f4b 100644 --- a/farmbot_core/test/bot_state_ng_test.exs +++ b/farmbot_core/test/bot_state_ng_test.exs @@ -41,6 +41,24 @@ defmodule Farmbot.BotStateNGTest do end describe "informational_settings" do + test "sets update_available" do + orig = BotStateNG.new() + + assert orig.informational_settings.update_available == false + + mut1 = + BotStateNG.changeset(orig, %{informational_settings: %{update_available: true}}) + |> Ecto.Changeset.apply_changes() + + assert mut1.informational_settings.update_available == true + + mut2 = + BotStateNG.changeset(orig, %{informational_settings: %{update_available: false}}) + |> Ecto.Changeset.apply_changes() + + assert mut2.informational_settings.update_available == false + end + test "reports soc_temp" do orig = BotStateNG.new() diff --git a/farmbot_ext/config/config.exs b/farmbot_ext/config/config.exs index e72dd899..8224e87a 100644 --- a/farmbot_ext/config/config.exs +++ b/farmbot_ext/config/config.exs @@ -1,5 +1,5 @@ use Mix.Config -config :farmbot_ext, :behaviour, authorization: Farmbot.Bootstrap.Authorization +config :logger, handle_otp_reports: true, handle_sasl_reports: true import_config "ecto.exs" import_config "farmbot_core.exs" import_config "lagger.exs" diff --git a/farmbot_ext/config/ecto.exs b/farmbot_ext/config/ecto.exs index 0e4ce4d8..f26e17b0 100644 --- a/farmbot_ext/config/ecto.exs +++ b/farmbot_ext/config/ecto.exs @@ -1,5 +1,8 @@ use Mix.Config +config :farmbot_core, + ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo] + config :farmbot_ext, ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo] diff --git a/farmbot_ext/lib/amqp/auto_sync_transport.ex b/farmbot_ext/lib/amqp/auto_sync_transport.ex index d6b2af69..6136d952 100644 --- a/farmbot_ext/lib/amqp/auto_sync_transport.ex +++ b/farmbot_ext/lib/amqp/auto_sync_transport.ex @@ -7,9 +7,11 @@ defmodule Farmbot.AMQP.AutoSyncTransport do Queue } - require Farmbot.Logger + alias Farmbot.AMQP.ConnectionWorker + require Logger - import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4] + require Farmbot.Logger + import Farmbot.Config, only: [get_config_value: 3] alias Farmbot.{ API.EagerLoader, @@ -22,7 +24,7 @@ defmodule Farmbot.AMQP.AutoSyncTransport do @exchange "amq.topic" - defstruct [:conn, :chan, :bot] + defstruct [:conn, :chan, :jwt] alias __MODULE__, as: State @doc false @@ -30,21 +32,39 @@ defmodule Farmbot.AMQP.AutoSyncTransport do GenServer.start_link(__MODULE__, args, name: __MODULE__) end - def init([conn, jwt]) do + def init(args) do Process.flag(:sensitive, true) - {:ok, chan} = Channel.open(conn) - :ok = Basic.qos(chan, global: true) - {:ok, _} = Queue.declare(chan, jwt.bot <> "_auto_sync", auto_delete: false) - - :ok = - Queue.bind(chan, jwt.bot <> "_auto_sync", @exchange, routing_key: "bot.#{jwt.bot}.sync.#") - - {:ok, _tag} = Basic.consume(chan, jwt.bot <> "_auto_sync", self(), no_ack: true) - {:ok, struct(State, conn: conn, chan: chan, bot: jwt.bot)} + jwt = Keyword.fetch!(args, :jwt) + {:ok, %State{conn: nil, chan: nil, jwt: jwt}, 1000} end - def terminate(_reason, _state) do - update_config_value(:bool, "settings", "needs_http_sync", true) + def terminate(reason, state) do + Farmbot.Logger.error(1, "Disconnected from AutoSync channel: #{inspect(reason)}") + # If a channel was still open, close it. + if state.chan, do: AMQP.Channel.close(state.chan) + end + + def handle_info(:timeout, state) do + jwt = state.jwt + bot = jwt.bot + auto_sync = bot <> "_auto_sync" + route = "bot.#{bot}.sync.#" + + with %{} = conn <- ConnectionWorker.connection(), + {:ok, chan} <- Channel.open(conn), + :ok <- Basic.qos(chan, global: true), + {:ok, _} <- Queue.declare(chan, auto_sync, auto_delete: false), + :ok <- Queue.bind(chan, auto_sync, @exchange, routing_key: route), + {:ok, _} <- Basic.consume(chan, auto_sync, self(), no_ack: true) do + {:noreply, %{state | conn: conn, chan: chan}} + else + nil -> + {:noreply, %{state | conn: nil, chan: nil}, 5000} + + error -> + Farmbot.Logger.error(1, "Failed to connect to AutoSync channel: #{inspect(error)}") + {:noreply, %{state | conn: nil, chan: nil}, 1000} + end end # Confirmation sent by the broker after registering this process as a consumer @@ -64,13 +84,21 @@ defmodule Farmbot.AMQP.AutoSyncTransport do end def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do + device = state.jwt.bot + + case String.split(key, ".") do + ["bot", ^device, "sync", asset_kind, id_str] -> + asset_kind = Module.concat([Farmbot, Asset, asset_kind]) + data = JSON.decode!(payload) + id = data["id"] || String.to_integer(id_str) + params = data["body"] + label = data["args"]["label"] + handle_asset(asset_kind, label, id, params, state) + end + end + + def handle_asset(asset_kind, label, id, params, state) do auto_sync? = get_config_value(:bool, "settings", "auto_sync") - device = state.bot - ["bot", ^device, "sync", asset_kind, id_str] = String.split(key, ".") - asset_kind = Module.concat([Farmbot, Asset, asset_kind]) - data = JSON.decode!(payload) - id = data["id"] || String.to_integer(id_str) - params = data["body"] cond do # TODO(Connor) no way to cache a deletion yet @@ -125,7 +153,8 @@ defmodule Farmbot.AMQP.AutoSyncTransport do end end - json = JSON.encode!(%{args: %{label: data["args"]["label"]}, kind: "rpc_ok"}) + device = state.bot + json = JSON.encode!(%{args: %{label: label}, kind: "rpc_ok"}) :ok = Basic.publish(state.chan, @exchange, "bot.#{device}.from_device", json) {:noreply, state} end diff --git a/farmbot_ext/lib/amqp/bot_state_transport.ex b/farmbot_ext/lib/amqp/bot_state_transport.ex index 85d7e579..5c6b06f6 100644 --- a/farmbot_ext/lib/amqp/bot_state_transport.ex +++ b/farmbot_ext/lib/amqp/bot_state_transport.ex @@ -1,14 +1,17 @@ defmodule Farmbot.AMQP.BotStateTransport do use GenServer use AMQP + alias AMQP.Channel + require Farmbot.Logger + alias Farmbot.AMQP.ConnectionWorker # Pushes a state tree every 5 seconds for good luck. @default_force_time_ms 5_000 @default_error_retry_ms 100 @exchange "amq.topic" - defstruct [:conn, :chan, :bot, :state_cache] + defstruct [:conn, :chan, :jwt, :state_cache] alias __MODULE__, as: State def force do @@ -20,20 +23,40 @@ defmodule Farmbot.AMQP.BotStateTransport do GenServer.start_link(__MODULE__, args, name: __MODULE__) end - def init([conn, jwt]) do + def init(args) do + jwt = Keyword.fetch!(args, :jwt) Process.flag(:sensitive, true) - initial_bot_state = Farmbot.BotState.subscribe() - {:ok, chan} = AMQP.Channel.open(conn) - :ok = Basic.qos(chan, global: true) - {:ok, struct(State, conn: conn, chan: chan, bot: jwt.bot, state_cache: initial_bot_state), 0} + {:ok, %State{conn: nil, chan: nil, jwt: jwt, state_cache: nil}, 0} + end + + def terminate(reason, state) do + Farmbot.Logger.error(1, "Disconnected from BotState channel: #{inspect(reason)}") + # If a channel was still open, close it. + if state.chan, do: Channel.close(state.chan) end def handle_cast(:force, state) do {:noreply, state, 0} end - def handle_info(:timeout, %{state_cache: bot_state} = state) do - case push_bot_state(state.chan, state.bot, bot_state) do + def handle_info(:timeout, %{state_cache: nil} = state) do + with %{} = conn <- ConnectionWorker.connection(), + {:ok, chan} <- Channel.open(conn), + :ok <- Basic.qos(chan, global: true) do + initial_bot_state = Farmbot.BotState.subscribe() + {:noreply, %{state | conn: conn, chan: chan, state_cache: initial_bot_state}, 0} + else + nil -> + {:noreply, %{state | conn: nil, chan: nil, state_cache: nil}, 5000} + + err -> + Farmbot.Logger.error(1, "Failed to connect to BotState channel: #{inspect(err)}") + {:noreply, %{state | conn: nil, chan: nil, state_cache: nil}, 1000} + end + end + + def handle_info(:timeout, %{state_cache: %{} = bot_state, chan: %{}} = state) do + case push_bot_state(state.chan, state.jwt.bot, bot_state) do :ok -> {:noreply, state, @default_force_time_ms} @@ -54,6 +77,6 @@ defmodule Farmbot.AMQP.BotStateTransport do |> Farmbot.BotStateNG.view() |> Farmbot.JSON.encode!() - AMQP.Basic.publish(chan, @exchange, "bot.#{bot}.status", json) + Basic.publish(chan, @exchange, "bot.#{bot}.status", json) end end diff --git a/farmbot_ext/lib/amqp/celery_script_transport.ex b/farmbot_ext/lib/amqp/celery_script_transport.ex index d1b4d188..0237c94e 100644 --- a/farmbot_ext/lib/amqp/celery_script_transport.ex +++ b/farmbot_ext/lib/amqp/celery_script_transport.ex @@ -1,13 +1,22 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do use GenServer use AMQP + + alias AMQP.{ + Channel, + Queue + } + require Farmbot.Logger require Logger + + alias Farmbot.AMQP.ConnectionWorker + import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4] @exchange "amq.topic" - defstruct [:conn, :chan, :bot] + defstruct [:conn, :chan, :jwt] alias __MODULE__, as: State @doc false @@ -15,20 +24,39 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do GenServer.start_link(__MODULE__, args, name: __MODULE__) end - def init([conn, jwt]) do + def init(args) do + jwt = Keyword.fetch!(args, :jwt) Process.flag(:sensitive, true) - {:ok, chan} = AMQP.Channel.open(conn) - :ok = Basic.qos(chan, global: true) - {:ok, _} = AMQP.Queue.declare(chan, jwt.bot <> "_from_clients", auto_delete: true) - {:ok, _} = AMQP.Queue.purge(chan, jwt.bot <> "_from_clients") + {:ok, %State{conn: nil, chan: nil, jwt: jwt}, 0} + end - :ok = - AMQP.Queue.bind(chan, jwt.bot <> "_from_clients", @exchange, - routing_key: "bot.#{jwt.bot}.from_clients" - ) + def terminate(reason, state) do + Farmbot.Logger.error(1, "Disconnected from CeleryScript channel: #{inspect(reason)}") + # If a channel was still open, close it. + if state.chan, do: AMQP.Channel.close(state.chan) + end - {:ok, _tag} = Basic.consume(chan, jwt.bot <> "_from_clients", self(), no_ack: true) - {:ok, struct(State, conn: conn, chan: chan, bot: jwt.bot)} + def handle_info(:timeout, state) do + bot = state.jwt.bot + from_clients = bot <> "_from_clients" + route = "bot.#{bot}.from_clients" + + with %{} = conn <- ConnectionWorker.connection(), + {:ok, chan} <- Channel.open(conn), + :ok <- Basic.qos(chan, global: true), + {:ok, _} <- Queue.declare(chan, from_clients, auto_delete: true), + {:ok, _} <- Queue.purge(chan, from_clients), + :ok <- Queue.bind(chan, from_clients, @exchange, routing_key: route), + {:ok, _tag} <- Basic.consume(chan, from_clients, self(), no_ack: true) do + {:noreply, %{state | conn: conn, chan: chan}} + else + nil -> + {:noreply, %{state | conn: nil, chan: nil}, 5000} + + err -> + Farmbot.Logger.error(1, "Failed to connect to CeleryScript channel: #{inspect(err)}") + {:noreply, %{state | conn: nil, chan: nil}, 1000} + end end # Confirmation sent by the broker after registering this process as a consumer @@ -53,7 +81,7 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do end def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do - device = state.bot + device = state.jwt.bot ["bot", ^device, "from_clients"] = String.split(key, ".") spawn_link(fn -> @@ -76,7 +104,7 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do Logger.error(message) end - AMQP.Basic.publish(state.chan, @exchange, "bot.#{state.bot}.from_device", reply) + AMQP.Basic.publish(state.chan, @exchange, "bot.#{state.jwt.bot}.from_device", reply) results_ast end) end diff --git a/farmbot_ext/lib/amqp/channel_supervisor.ex b/farmbot_ext/lib/amqp/channel_supervisor.ex index ac4a5eae..53199f95 100644 --- a/farmbot_ext/lib/amqp/channel_supervisor.ex +++ b/farmbot_ext/lib/amqp/channel_supervisor.ex @@ -4,7 +4,7 @@ defmodule Farmbot.AMQP.ChannelSupervisor do alias Farmbot.JWT alias Farmbot.AMQP.{ - ConnectionWorker, + NervesHubTransport, LogTransport, BotStateTransport, AutoSyncTransport, @@ -17,14 +17,14 @@ defmodule Farmbot.AMQP.ChannelSupervisor do def init([token]) do Process.flag(:sensitive, true) - conn = ConnectionWorker.connection() jwt = JWT.decode!(token) children = [ - {LogTransport, [conn, jwt]}, - {BotStateTransport, [conn, jwt]}, - {AutoSyncTransport, [conn, jwt]}, - {CeleryScriptTransport, [conn, jwt]} + {NervesHubTransport, [jwt: jwt]}, + {LogTransport, [jwt: jwt]}, + {BotStateTransport, [jwt: jwt]}, + {AutoSyncTransport, [jwt: jwt]}, + {CeleryScriptTransport, [jwt: jwt]} ] Supervisor.init(children, strategy: :one_for_one) diff --git a/farmbot_ext/lib/amqp/connection_worker.ex b/farmbot_ext/lib/amqp/connection_worker.ex index 6424f965..0e30ad19 100644 --- a/farmbot_ext/lib/amqp/connection_worker.ex +++ b/farmbot_ext/lib/amqp/connection_worker.ex @@ -3,7 +3,8 @@ defmodule Farmbot.AMQP.ConnectionWorker do alias Farmbot.JWT require Farmbot.Logger require Logger - import Farmbot.Config, only: [update_config_value: 4] + + defstruct [:opts, :conn] def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) @@ -14,20 +15,16 @@ defmodule Farmbot.AMQP.ConnectionWorker do end def init(opts) do - token = Keyword.fetch!(opts, :token) - email = Keyword.fetch!(opts, :email) Process.flag(:sensitive, true) Process.flag(:trap_exit, true) - jwt = JWT.decode!(token) - IO.puts("OPEN") - {:ok, conn} = open_connection(token, email, jwt.bot, jwt.mqtt, jwt.vhost) - IO.puts("OPENED") - Process.link(conn.pid) - Process.monitor(conn.pid) - {:ok, conn} + {:ok, %__MODULE__{conn: nil, opts: opts}, 0} end - def terminate(reason, conn) do + def terminate(reason, %{conn: nil}) do + Logger.info("AMQP connection not open: #{inspect(reason)}") + end + + def terminate(reason, %{conn: conn}) do if Process.alive?(conn.pid) do try do Logger.info("Closing AMQP connection: #{inspect(reason)}") @@ -40,19 +37,29 @@ defmodule Farmbot.AMQP.ConnectionWorker do end end - def handle_info({:DOWN, _, :process, _pid, reason}, conn) do - ok_reasons = [:normal, :shutdown, :token_refresh] - update_config_value(:bool, "settings", "ignore_fbos_config", false) + def handle_info(:timeout, state) do + token = Keyword.fetch!(state.opts, :token) + email = Keyword.fetch!(state.opts, :email) + jwt = JWT.decode!(token) - if reason not in ok_reasons do - Farmbot.Logger.error(1, "AMQP Connection closed: #{inspect(reason)}") - update_config_value(:bool, "settings", "log_amqp_connected", true) + case open_connection(token, email, jwt.bot, jwt.mqtt, jwt.vhost) do + {:ok, conn} -> + Process.link(conn.pid) + Process.monitor(conn.pid) + {:noreply, %{state | conn: conn}} + + err -> + Logger.error("Error connecting to AMPQ: #{inspect(err)}") + {:noreply, %{state | conn: nil}, 5000} end + end + def handle_info({:DOWN, _, :process, _pid, reason}, conn) do + Logger.error("Connection crash: #{inspect(reason)}") {:stop, reason, conn} end - def handle_call(:connection, _, conn), do: {:reply, conn, conn} + def handle_call(:connection, _, %{conn: conn} = state), do: {:reply, conn, state} defp open_connection(token, email, bot, mqtt_server, vhost) do Logger.info("Opening new AMQP connection.") @@ -74,14 +81,6 @@ defmodule Farmbot.AMQP.ConnectionWorker do virtual_host: vhost ] - case AMQP.Connection.open(opts) do - {:ok, conn} -> - {:ok, conn} - - {:error, reason} -> - Logger.error("Error connecting to AMPQ: #{inspect(reason)}") - Process.sleep(5000) - open_connection(token, email, bot, mqtt_server, vhost) - end + AMQP.Connection.open(opts) end end diff --git a/farmbot_ext/lib/amqp/log_transport.ex b/farmbot_ext/lib/amqp/log_transport.ex index 02ce7a4f..fa7657ed 100644 --- a/farmbot_ext/lib/amqp/log_transport.ex +++ b/farmbot_ext/lib/amqp/log_transport.ex @@ -1,14 +1,16 @@ defmodule Farmbot.AMQP.LogTransport do use GenServer use AMQP + alias AMQP.Channel + + alias Farmbot.AMQP.ConnectionWorker require Farmbot.Logger require Logger - import Farmbot.Config, only: [update_config_value: 4] @exchange "amq.topic" @checkup_ms 100 - defstruct [:conn, :chan, :bot, :state_cache] + defstruct [:conn, :chan, :jwt, :state_cache] alias __MODULE__, as: State @doc false @@ -16,26 +18,36 @@ defmodule Farmbot.AMQP.LogTransport do GenServer.start_link(__MODULE__, args, name: __MODULE__) end - def init([conn, jwt]) do + def init(args) do + jwt = Keyword.fetch!(args, :jwt) Process.flag(:sensitive, true) - initial_bot_state = Farmbot.BotState.subscribe() - {:ok, chan} = AMQP.Channel.open(conn) - :ok = Basic.qos(chan, global: true) - state = struct(State, conn: conn, chan: chan, bot: jwt.bot, state_cache: initial_bot_state) - {:ok, state, 0} + {:ok, %State{conn: nil, chan: nil, jwt: jwt, state_cache: nil}, 0} end def terminate(reason, state) do - ok_reasons = [:normal, :shutdown, :token_refresh] - update_config_value(:bool, "settings", "ignore_fbos_config", false) - - if reason not in ok_reasons do - Farmbot.Logger.error(1, "Logger amqp client Died: #{inspect(reason)}") - update_config_value(:bool, "settings", "log_amqp_connected", true) - end - + Farmbot.Logger.error(1, "Disconnected from Log channel: #{inspect(reason)}") # If a channel was still open, close it. - if state.chan, do: AMQP.Channel.close(state.chan) + if state.chan, do: Channel.close(state.chan) + end + + def handle_info(:timeout, %{state_cache: nil} = state) do + with %{} = conn <- ConnectionWorker.connection(), + {:ok, chan} <- Channel.open(conn), + :ok <- Basic.qos(chan, global: true) do + initial_bot_state = Farmbot.BotState.subscribe() + {:noreply, %{state | conn: conn, chan: chan, state_cache: initial_bot_state}, 0} + else + nil -> + {:noreply, %{state | conn: nil, chan: nil, state_cache: nil}, 5000} + + err -> + Farmbot.Logger.error(1, "Failed to connect to Log channel: #{inspect(err)}") + {:noreply, %{state | conn: nil, chan: nil, state_cache: nil}, 1000} + end + end + + def handle_info(:timeout, state) do + {:noreply, state, {:continue, Farmbot.Logger.handle_all_logs()}} end def handle_info({Farmbot.BotState, change}, state) do @@ -43,10 +55,6 @@ defmodule Farmbot.AMQP.LogTransport do {:noreply, %{state | state_cache: new_state_cache}, @checkup_ms} end - def handle_info(:timeout, state) do - {:noreply, state, {:continue, Farmbot.Logger.handle_all_logs()}} - end - def handle_continue([log | rest], state) do case do_handle_log(log, state) do :ok -> @@ -86,13 +94,13 @@ defmodule Farmbot.AMQP.LogTransport do } json_log = add_position_to_log(log_without_pos, location_data) - push_bot_log(state.chan, state.bot, json_log) + push_bot_log(state.chan, state.jwt.bot, json_log) end end defp push_bot_log(chan, bot, log) do json = Farmbot.JSON.encode!(log) - :ok = AMQP.Basic.publish(chan, @exchange, "bot.#{bot}.logs", json) + :ok = Basic.publish(chan, @exchange, "bot.#{bot}.logs", json) end defp add_position_to_log(%{} = log, %{position: %{x: x, y: y, z: z}}) do diff --git a/farmbot_ext/lib/amqp/nerves_hub_transport.ex b/farmbot_ext/lib/amqp/nerves_hub_transport.ex new file mode 100644 index 00000000..ba8eb7e9 --- /dev/null +++ b/farmbot_ext/lib/amqp/nerves_hub_transport.ex @@ -0,0 +1,102 @@ +defmodule Farmbot.AMQP.NervesHubTransport do + use GenServer + use AMQP + + alias AMQP.{ + Channel, + Queue + } + + require Farmbot.Logger + require Logger + alias Farmbot.JSON + + alias Farmbot.AMQP.ConnectionWorker + + @exchange "amq.topic" + + defstruct [:conn, :chan, :jwt] + alias __MODULE__, as: State + + @doc false + def start_link(args) do + GenServer.start_link(__MODULE__, args, name: __MODULE__) + end + + def init(args) do + jwt = Keyword.fetch!(args, :jwt) + Process.flag(:sensitive, true) + {:ok, %State{conn: nil, chan: nil, jwt: jwt}, 0} + end + + def terminate(reason, state) do + Farmbot.Logger.error(1, "Disconnected from NervesHub AMQP channel: #{inspect(reason)}") + # If a channel was still open, close it. + if state.chan, do: AMQP.Channel.close(state.chan) + end + + def handle_info(:timeout, state) do + bot = state.jwt.bot + nerves_hub = bot <> "_nerves_hub" + route = "bot.#{bot}.nerves_hub" + + with %{} = conn <- ConnectionWorker.connection(), + {:ok, chan} <- Channel.open(conn), + :ok <- Basic.qos(chan, global: true), + {:ok, _} <- Queue.declare(chan, nerves_hub, auto_delete: false, durable: true), + :ok <- Queue.bind(chan, nerves_hub, @exchange, routing_key: route), + {:ok, _tag} <- Basic.consume(chan, nerves_hub, self(), []) do + {:noreply, %{state | conn: conn, chan: chan}} + else + nil -> + {:noreply, %{state | conn: nil, chan: nil}, 5000} + + err -> + Farmbot.Logger.error(1, "Failed to connect to NervesHub AMQP channel: #{inspect(err)}") + {:noreply, %{state | conn: nil, chan: nil}, 1000} + end + end + + # Confirmation sent by the broker after registering this process as a consumer + def handle_info({:basic_consume_ok, _}, state) do + {:noreply, state} + end + + # Sent by the broker when the consumer is + # unexpectedly cancelled (such as after a queue deletion) + def handle_info({:basic_cancel, _}, state) do + {:stop, :normal, state} + end + + # Confirmation sent by the broker to the consumer process after a Basic.cancel + def handle_info({:basic_cancel_ok, _}, state) do + {:noreply, state} + end + + def handle_info({:basic_deliver, payload, %{routing_key: key} = opts}, state) do + device = state.jwt.bot + ["bot", ^device, "nerves_hub"] = String.split(key, ".") + handle_nerves_hub(payload, opts, state) + end + + def handle_nerves_hub(payload, options, state) do + alias Farmbot.System.NervesHub + + with {:ok, %{"cert" => base64_cert, "key" => base64_key}} <- JSON.decode(payload), + {:ok, cert} <- Base.decode64(base64_cert), + {:ok, key} <- Base.decode64(base64_key), + :ok <- NervesHub.configure_certs(cert, key), + :ok <- NervesHub.connect() do + :ok = Basic.ack(state.chan, options[:delivery_tag]) + {:noreply, state} + else + {:error, reason} -> + Logger.error(1, "NervesHub failed to configure. #{inspect(reason)}") + {:noreply, state} + + :error -> + Logger.error(1, "NervesHub payload invalid. (base64)") + {:noreply, state} + end + end +end diff --git a/farmbot_ext/lib/api.ex b/farmbot_ext/lib/api.ex index acd170f5..ade9c932 100644 --- a/farmbot_ext/lib/api.ex +++ b/farmbot_ext/lib/api.ex @@ -22,7 +22,7 @@ defmodule Farmbot.API do uri = Map.fetch!(tkn, :iss) |> URI.parse() url = (uri.scheme || "https") <> "://" <> uri.host <> ":" <> to_string(uri.port) - Tesla.build_client([ + Tesla.client([ {Tesla.Middleware.BaseUrl, url}, {Tesla.Middleware.Headers, [ @@ -34,7 +34,7 @@ defmodule Farmbot.API do end def storage_client(%StorageAuth{url: url}) do - Tesla.build_client( + Tesla.client( [ {Tesla.Middleware.BaseUrl, "https:" <> url}, {Tesla.Middleware.Headers, diff --git a/farmbot_ext/lib/api/dirty_worker.ex b/farmbot_ext/lib/api/dirty_worker.ex index d70a6a7d..9b2314ad 100644 --- a/farmbot_ext/lib/api/dirty_worker.ex +++ b/farmbot_ext/lib/api/dirty_worker.ex @@ -26,7 +26,7 @@ defmodule Farmbot.API.DirtyWorker do @impl GenServer def init(args) do - Logger.disable(self()) + # Logger.disable(self()) module = Keyword.fetch!(args, :module) timeout = Keyword.get(args, :timeout, @timeout) {:ok, %{module: module, timeout: timeout}, timeout} diff --git a/farmbot_ext/lib/api/dirty_worker/supervisor.ex b/farmbot_ext/lib/api/dirty_worker/supervisor.ex index eb5c105c..89d4f7d7 100644 --- a/farmbot_ext/lib/api/dirty_worker/supervisor.ex +++ b/farmbot_ext/lib/api/dirty_worker/supervisor.ex @@ -4,6 +4,7 @@ defmodule Farmbot.API.DirtyWorker.Supervisor do alias Farmbot.Asset.{ Device, + DeviceCert, DiagnosticDump, FarmEvent, FarmwareEnv, @@ -27,6 +28,7 @@ defmodule Farmbot.API.DirtyWorker.Supervisor do def init(_args) do children = [ {DirtyWorker, Device}, + {DirtyWorker, DeviceCert}, {DirtyWorker, DiagnosticDump}, {DirtyWorker, FarmEvent}, {DirtyWorker, FarmwareEnv}, diff --git a/farmbot_ext/lib/bootstrap.ex b/farmbot_ext/lib/bootstrap.ex new file mode 100644 index 00000000..a21ed317 --- /dev/null +++ b/farmbot_ext/lib/bootstrap.ex @@ -0,0 +1,58 @@ +defmodule Farmbot.Bootstrap do + use GenServer + + require Logger + alias Farmbot.Bootstrap.Authorization + import Farmbot.Config, only: [update_config_value: 4, get_config_value: 3] + + def start_link(args) do + GenServer.start_link(__MODULE__, args, name: __MODULE__) + end + + def init([]) do + update_config_value(:bool, "settings", "log_amqp_connected", true) + {:ok, nil, 0} + end + + def handle_info(:timeout, nil) do + email = get_config_value(:string, "authorization", "email") + server = get_config_value(:string, "authorization", "server") + password = get_config_value(:string, "authorization", "password") + secret = get_config_value(:string, "authorization", "secret") + try_auth(email, server, password, secret) + end + + def try_auth(nil, _server, _password, _secret) do + {:noreply, nil, 5000} + end + + def try_auth(_email, nil, _password, _secret) do + {:noreply, nil, 5000} + end + + def try_auth(email, server, nil, secret) when is_binary(secret) do + Logger.debug("using secret to auth") + + with {:ok, tkn} <- Authorization.authorize_with_secret(email, secret, server), + _ <- update_config_value(:string, "authorization", "token", tkn), + {:ok, pid} <- Supervisor.start_child(Farmbot.Ext, Farmbot.Bootstrap.Supervisor) do + {:noreply, pid} + else + _ -> {:noreply, nil, 5000} + end + end + + # TODO(Connor) - drop password and save secret here somehow. + def try_auth(email, server, password, _secret) do + Logger.debug("using password to auth") + # require IEx; IEx.pry + + with {:ok, tkn} <- Authorization.authorize_with_password(email, password, server), + _ <- update_config_value(:string, "authorization", "token", tkn), + {:ok, pid} <- Supervisor.start_child(Farmbot.Ext, Farmbot.Bootstrap.Supervisor) do + {:noreply, pid} + else + _ -> {:noreply, nil, 5000} + end + end +end diff --git a/farmbot_ext/lib/bootstrap/authorization.ex b/farmbot_ext/lib/bootstrap/authorization.ex index 1890ebc3..be9cb9c7 100644 --- a/farmbot_ext/lib/bootstrap/authorization.ex +++ b/farmbot_ext/lib/bootstrap/authorization.ex @@ -50,12 +50,12 @@ defmodule Farmbot.Bootstrap.Authorization do |> build_payload() end - defp build_payload(secret) do + def build_payload(secret) do %{user: %{credentials: secret |> Base.encode64()}} |> Farmbot.JSON.encode() end - defp build_secret(email, password, rsa_key) do + def build_secret(email, password, rsa_key) do %{email: email, password: password, id: UUID.uuid1(), version: 1} |> Farmbot.JSON.encode!() |> RSA.encrypt({:public, rsa_key}) @@ -67,7 +67,7 @@ defmodule Farmbot.Bootstrap.Authorization do ] @spec fetch_rsa_key(server) :: {:ok, term} | {:error, String.t() | atom} - def fetch_rsa_key(server) do + def fetch_rsa_key(server) when is_binary(server) do url = "#{server}/api/public_key" with {:ok, body} <- do_request({:get, url, "", @headers}) do diff --git a/farmbot_ext/lib/bootstrap/supervisor.ex b/farmbot_ext/lib/bootstrap/supervisor.ex index a3f35b1f..f2dfbdfd 100644 --- a/farmbot_ext/lib/bootstrap/supervisor.ex +++ b/farmbot_ext/lib/bootstrap/supervisor.ex @@ -1,138 +1,21 @@ 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_ext, :init, [ - Farmbot.Configurator - ] - - config :farmbot_ext, :behaviour, - authorization: Farmbot.Configurator - - # farmbot_configurator.ex - - defmodule Farmbot.Configurator do - @moduledoc false - @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_ext, :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.Config - import Config, only: [update_config_value: 4, get_config_value: 3] - require Farmbot.Logger - error_msg = """ - Please configure an authorization module! - for example: - config: :farmbot_ext, :behaviour, [ - authorization: Farmbot.Bootstrap.Authorization - ] - """ - - @auth_task Application.get_env(:farmbot_ext, :behaviour)[:authorization] - @auth_task || Mix.raise(error_msg) - - @doc "Start Bootstrap services." + @doc "Start Bootstraped services." def start_link(args) do Supervisor.start_link(__MODULE__, args, name: __MODULE__) end def init([]) do - # Make sure we log when amqp is connected. - update_config_value(:bool, "settings", "log_amqp_connected", true) - email = get_config_value(:string, "authorization", "email") - server = get_config_value(:string, "authorization", "server") - password = get_config_value(:string, "authorization", "password") - secret = get_config_value(:string, "authorization", "secret") + children = [ + Farmbot.API.EagerLoader.Supervisor, + Farmbot.API.DirtyWorker.Supervisor, + Farmbot.Bootstrap.APITask, + Farmbot.AMQP.Supervisor, + Farmbot.API.ImageUploader + ] - cond do - is_nil(email) -> exit("No email") - is_nil(server) -> exit("No server") - is_nil(secret) && is_nil(password) -> exit("No password or secret.") - secret -> actual_init(&auth_with_secret/3, email, secret, server) - password -> actual_init(&auth_with_password/3, email, password, server) - end - end - - defp actual_init(fun, email, password_or_secret, server) when is_function(fun) do - busy_msg = "Beginning Bootstrap authorization: #{email} - #{server}" - Farmbot.Logger.busy(2, busy_msg) - # get a token - case apply_auth_fun(fun, [email, password_or_secret, server]) do - {:ok, token} -> - success_msg = "Successful Bootstrap authorization: #{email} - #{server}" - Farmbot.Logger.success(2, success_msg) - update_config_value(:bool, "settings", "first_boot", false) - update_config_value(:bool, "settings", "needs_http_sync", true) - update_config_value(:string, "authorization", "token", token) - - children = [ - Farmbot.API.EagerLoader.Supervisor, - Farmbot.API.DirtyWorker.Supervisor, - Farmbot.Bootstrap.APITask, - Farmbot.AMQP.Supervisor, - Farmbot.API.ImageUploader - ] - - opts = [strategy: :one_for_one] - Supervisor.init(children, opts) - - {:error, reason} -> - exit(reason) - end - end - - defp apply_auth_fun(fun, args) do - try do - apply(fun, args) - rescue - e -> {:error, Exception.message(e)} - end - end - - defp auth_with_secret(e, sec, s) do - Farmbot.Logger.debug(3, "Using secret to authorize.") - @auth_task.authorize_with_secret(e, sec, s) - end - - defp auth_with_password(e, pass, s) do - Farmbot.Logger.debug(3, "Using password to authorize.") - @auth_task.authorize_with_password(e, pass, s) + opts = [strategy: :one_for_one] + Supervisor.init(children, opts) end end diff --git a/farmbot_ext/lib/farmbot_ext.ex b/farmbot_ext/lib/farmbot_ext.ex index 013000d9..3af817b1 100644 --- a/farmbot_ext/lib/farmbot_ext.ex +++ b/farmbot_ext/lib/farmbot_ext.ex @@ -8,7 +8,7 @@ defmodule Farmbot.Ext do def start(_type, _args) do # List all child processes to be supervised children = [ - {Farmbot.Bootstrap.Supervisor, []} + Farmbot.Bootstrap ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/farmbot_os/config/config.exs b/farmbot_os/config/config.exs index cbc7a67f..77d3011c 100644 --- a/farmbot_os/config/config.exs +++ b/farmbot_os/config/config.exs @@ -35,7 +35,7 @@ config :farmbot_core, :behaviour, config :farmbot_ext, :behaviour, authorization: Farmbot.Bootstrap.Authorization config :ecto, json_library: Farmbot.JSON -config :farmbot, +config :farmbot_core, ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo] config :farmbot_core, Farmbot.Config.Repo, @@ -67,7 +67,13 @@ config :farmbot, Farmbot.Platform.Supervisor, import_config("lagger.exs") if Mix.Project.config()[:target] == "host" do - import_config("host/#{Mix.env()}.exs") + if File.exists?("config/host/#{Mix.env()}.exs") do + import_config("host/#{Mix.env()}.exs") + end else import_config("target/#{Mix.env()}.exs") + + if File.exists?("config/target/#{Mix.Project.config()[:target]}.exs") do + import_config("target/#{Mix.Project.config()[:target]}.exs") + end end diff --git a/farmbot_os/config/host/dev.exs b/farmbot_os/config/host/dev.exs index f628a478..b0b3e734 100644 --- a/farmbot_os/config/host/dev.exs +++ b/farmbot_os/config/host/dev.exs @@ -40,6 +40,3 @@ config :farmbot_core, :behaviour, pin_binding_handler: Farmbot.PinBinding.StubHandler, celery_script_io_layer: Farmbot.OS.IOLayer, firmware_handler: Farmbot.Firmware.UartHandler - -config :farmbot_core, :uart_handler, tty: "/dev/ttyACM0" -import_config("auth_secret.exs") diff --git a/farmbot_os/config/host/test.exs b/farmbot_os/config/host/test.exs index f7a4e81a..d1317e8f 100644 --- a/farmbot_os/config/host/test.exs +++ b/farmbot_os/config/host/test.exs @@ -1,6 +1,7 @@ use Mix.Config data_path = Path.join(["/", "tmp", "farmbot"]) +File.mkdir_p(data_path) config :farmbot_ext, data_path: data_path diff --git a/farmbot_os/config/target/dev.exs b/farmbot_os/config/target/dev.exs index afa70afd..ad284437 100644 --- a/farmbot_os/config/target/dev.exs +++ b/farmbot_os/config/target/dev.exs @@ -3,8 +3,22 @@ local_file = Path.join(System.user_home!(), ".ssh/id_rsa.pub") local_key = if File.exists?(local_file), do: [File.read!(local_file)], else: [] config :nerves_firmware_ssh, - authorized_keys: local_key, - ssh_console_port: 22 + authorized_keys: local_key + +config :nerves_network, regulatory_domain: "US" + +config :nerves_init_gadget, + ifname: "usb0", + address_method: :dhcpd, + mdns_domain: "farmbot.local", + node_name: "farmbot", + node_host: :mdns_domain + +config :shoehorn, + init: [:nerves_runtime, :nerves_init_gadget, :nerves_firmware_ssh, :farmbot_core, :farmbot_ext], + app: :farmbot + +config :tzdata, :autoupdate, :disabled config :farmbot_core, :behaviour, firmware_handler: Farmbot.Firmware.StubHandler, @@ -15,8 +29,7 @@ config :farmbot_core, :behaviour, data_path = Path.join("/", "root") -config :farmbot_ext, - data_path: data_path +config :farmbot, Farmbot.OS.FileSystem, data_path: data_path config :logger_backend_ecto, LoggerBackendEcto.Repo, adapter: Sqlite.Ecto2, @@ -38,31 +51,24 @@ config :farmbot_core, Farmbot.Asset.Repo, database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3") config :farmbot, - ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo], + ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo] + +config :farmbot, Farmbot.System.Init.Supervisor, init_children: [ - {Farmbot.Target.Leds.AleHandler, []}, - {Farmbot.Firmware.UartHandler.AutoDetector, []} - ], - platform_children: [ - {Farmbot.Target.Bootstrap.Configurator, []}, - {Farmbot.Target.Network, []}, - {Farmbot.Target.SSHConsole, []}, - {Farmbot.Target.Network.WaitForTime, []}, - {Farmbot.Target.Network.DnsTask, []}, - {Farmbot.Target.Network.TzdataTask, []}, - # Reports Disk usage every 60 seconds. - {Farmbot.Target.DiskUsageWorker, []}, - # Reports Memory usage every 60 seconds. - {Farmbot.Target.MemoryUsageWorker, []}, - # Reports SOC temperature every 60 seconds. - {Farmbot.Target.SocTempWorker, []}, - # Reports Uptime every 60 seconds. - {Farmbot.Target.UptimeWorker, []}, - {Farmbot.Target.Network.InfoSupervisor, []}, - {Farmbot.Target.Uevent.Supervisor, []} + Farmbot.Target.Leds.AleHandler ] -config :farmbot, :behaviour, system_tasks: Farmbot.Target.SystemTasks +config :farmbot, Farmbot.Platform.Supervisor, + platform_children: [ + Farmbot.System.NervesHub, + Farmbot.Target.Network.Supervisor, + Farmbot.Target.Configurator.Supervisor, + Farmbot.Target.SSHConsole, + Farmbot.Target.Uevent.Supervisor, + Farmbot.Target.InfoWorker.Supervisor + ] + +config :farmbot, Farmbot.System, system_tasks: Farmbot.Target.SystemTasks config :farmbot, Farmbot.System.NervesHub, farmbot_nerves_hub_handler: Farmbot.System.NervesHubClient diff --git a/farmbot_os/config/target/prod.exs b/farmbot_os/config/target/prod.exs index 6d73454a..509d8fbd 100644 --- a/farmbot_os/config/target/prod.exs +++ b/farmbot_os/config/target/prod.exs @@ -1,5 +1,23 @@ use Mix.Config +config :nerves_firmware_ssh, + authorized_keys: [] + +config :nerves_network, regulatory_domain: "US" + +config :nerves_init_gadget, + ifname: "usb0", + address_method: :dhcpd, + mdns_domain: "farmbot.local", + node_name: "farmbot", + node_host: :mdns_domain + +config :shoehorn, + init: [:nerves_runtime, :nerves_init_gadget, :nerves_firmware_ssh, :farmbot_core, :farmbot_ext], + app: :farmbot + +config :tzdata, :autoupdate, :disabled + config :farmbot_core, :behaviour, firmware_handler: Farmbot.Firmware.StubHandler, leds_handler: Farmbot.Target.Leds.AleHandler, @@ -9,8 +27,7 @@ config :farmbot_core, :behaviour, data_path = Path.join("/", "root") -config :farmbot_ext, - data_path: data_path +config :farmbot, Farmbot.OS.FileSystem, data_path: data_path config :logger_backend_ecto, LoggerBackendEcto.Repo, adapter: Sqlite.Ecto2, @@ -32,31 +49,24 @@ config :farmbot_core, Farmbot.Asset.Repo, database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3") config :farmbot, - ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo], + ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo] + +config :farmbot, Farmbot.System.Init.Supervisor, init_children: [ - {Farmbot.Target.Leds.AleHandler, []}, - {Farmbot.Firmware.UartHandler.AutoDetector, []} - ], - platform_children: [ - {Farmbot.Target.Bootstrap.Configurator, []}, - {Farmbot.Target.Network, []}, - {Farmbot.Target.SSHConsole, []}, - {Farmbot.Target.Network.WaitForTime, []}, - {Farmbot.Target.Network.DnsTask, []}, - {Farmbot.Target.Network.TzdataTask, []}, - # Reports Disk usage every 60 seconds. - {Farmbot.Target.DiskUsageWorker, []}, - # Reports Memory usage every 60 seconds. - {Farmbot.Target.MemoryUsageWorker, []}, - # Reports SOC temperature every 60 seconds. - {Farmbot.Target.SocTempWorker, []}, - # Reports Uptime every 60 seconds. - {Farmbot.Target.UptimeWorker, []}, - {Farmbot.Target.Network.InfoSupervisor, []}, - {Farmbot.Target.Uevent.Supervisor, []} + Farmbot.Target.Leds.AleHandler ] -config :farmbot, :behaviour, system_tasks: Farmbot.Target.SystemTasks +config :farmbot, Farmbot.Platform.Supervisor, + platform_children: [ + Farmbot.System.NervesHub, + Farmbot.Target.Network.Supervisor, + Farmbot.Target.Configurator.Supervisor, + Farmbot.Target.SSHConsole, + Farmbot.Target.Uevent.Supervisor, + Farmbot.Target.InfoWorker.Supervisor + ] + +config :farmbot, Farmbot.System, system_tasks: Farmbot.Target.SystemTasks config :farmbot, Farmbot.System.NervesHub, farmbot_nerves_hub_handler: Farmbot.System.NervesHubClient diff --git a/farmbot_os/config/target/rpi3.exs b/farmbot_os/config/target/rpi3.exs new file mode 100644 index 00000000..08ae3398 --- /dev/null +++ b/farmbot_os/config/target/rpi3.exs @@ -0,0 +1,2 @@ +use Mix.Config +config :farmbot, :captive_portal_address, "192.168.24.1" diff --git a/farmbot_os/lib/core_start.ex b/farmbot_os/lib/core_start.ex deleted file mode 100644 index e8c77e04..00000000 --- a/farmbot_os/lib/core_start.ex +++ /dev/null @@ -1,31 +0,0 @@ -defmodule Farmbot.System.CoreStart do - @moduledoc false - use Supervisor - require Logger - - @doc false - def start_link(args) do - Supervisor.start_link(__MODULE__, args, [name: __MODULE__]) - end - - def init([]) do - :ok = start_core_app(Farmbot.BootState.read()) - Supervisor.init([], [strategy: :one_for_one]) - end - - defp start_core_app(state) do - case Application.ensure_all_started(:farmbot_core) do - {:ok, _} -> - Farmbot.BootState.write(:UPANDRUNNING) - :ok - {:error, {:farmbot_core, {{:shutdown, {:failed_to_start_child, child, reason}}, _}}} -> - msg = "Failed to start farmbot_core while in state: #{inspect state} child: #{child} => #{inspect reason}" - maybe_reset(msg) - :ok - end - end - - defp maybe_reset(msg) do - Farmbot.System.factory_reset(msg) - end -end diff --git a/farmbot_os/lib/ext_start.ex b/farmbot_os/lib/ext_start.ex deleted file mode 100644 index 20affb7a..00000000 --- a/farmbot_os/lib/ext_start.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Farmbot.System.ExtStart do - @moduledoc false - use Supervisor - require Logger - - @doc false - def start_link(args) do - Supervisor.start_link(__MODULE__, args, [name: __MODULE__]) - end - - def init([]) do - :ok = start_ext_app(Farmbot.BootState.read()) - Supervisor.init([], [strategy: :one_for_one]) - end - - defp start_ext_app(state) do - case Application.ensure_all_started(:farmbot_ext) do - {:ok, _} -> - Farmbot.BootState.write(:UPANDRUNNING) - :ok - {:error, {:farmbot_ext, {{:shutdown, {:failed_to_start_child, child, reason}}, _}}} -> - msg = "Failed to start farmbot_ext while in state: #{inspect state} child: #{child} => #{inspect reason}" - maybe_reset(msg) - :ok - end - end - - defp maybe_reset(msg) do - case Farmbot.Config.get_config_value(:bool, "settings", "first_boot") do - true -> Farmbot.System.factory_reset(msg) - false -> Logger.error(msg) - end - end -end diff --git a/farmbot_os/lib/farmbot_os.ex b/farmbot_os/lib/farmbot_os.ex index 55d9830d..e5b70999 100644 --- a/farmbot_os/lib/farmbot_os.ex +++ b/farmbot_os/lib/farmbot_os.ex @@ -8,9 +8,7 @@ defmodule Farmbot.OS do def start(_type, _args) do children = [ {Farmbot.System.Init.Supervisor, []}, - {Farmbot.System.CoreStart, []}, {Farmbot.Platform.Supervisor, []}, - {Farmbot.System.ExtStart, []}, {Farmbot.EasterEggs, []}, ] opts = [strategy: :one_for_one, name: __MODULE__] diff --git a/farmbot_os/lib/firmware/auto_detector.ex b/farmbot_os/lib/firmware/auto_detector.ex new file mode 100644 index 00000000..b4bc25a4 --- /dev/null +++ b/farmbot_os/lib/firmware/auto_detector.ex @@ -0,0 +1,3 @@ +defmodule Farmbot.Firmware.AutoDetector do + +end diff --git a/farmbot_os/lib/init/supervisor.ex b/farmbot_os/lib/init/supervisor.ex index 63a1ff2e..46a85ee2 100644 --- a/farmbot_os/lib/init/supervisor.ex +++ b/farmbot_os/lib/init/supervisor.ex @@ -6,9 +6,9 @@ defmodule Farmbot.System.Init.Supervisor do end def init([]) do - children = Application.get_env(:farmbot, :init_children, []) ++ [ + config = Application.get_env(:farmbot, __MODULE__) + children = (config[:init_children] || []) ++ [ {Farmbot.System.Init.FSCheckup, []}, - {Farmbot.System.Init.Ecto, []}, ] Supervisor.init(children, [strategy: :one_for_all]) end diff --git a/farmbot_os/lib/nerves_hub.ex b/farmbot_os/lib/nerves_hub.ex index 9370cf7c..2013aff1 100644 --- a/farmbot_os/lib/nerves_hub.ex +++ b/farmbot_os/lib/nerves_hub.ex @@ -105,6 +105,9 @@ defmodule Farmbot.System.NervesHub do def get_config do @handler.config() + |> Enum.map(fn(val) -> + if val == "", do: nil, else: val + end) end def connect do @@ -126,18 +129,21 @@ defmodule Farmbot.System.NervesHub do # Creates a device in NervesHub # or updates it if one exists. def configure(tags) when is_list(tags) do - Farmbot.Logger.debug 1, "Configuring OTA Service: #{inspect tags}" - payload = %{ + alias Farmbot.Asset.{Repo, DeviceCert} + Farmbot.Logger.debug 1, "Configuring OTA Service DeviceCert: #{inspect tags}" + serial = serial() + old = Repo.get_by(DeviceCert, serial_number: serial) || %DeviceCert{} + params = %{ serial_number: serial(), tags: tags - } |> Farmbot.JSON.encode!() - case Farmbot.HTTP.post("/api/device_cert", payload) do + } + change = DeviceCert.changeset(old, params) + case Repo.insert_or_update(change) do {:ok, _} -> :ok {:error, reason} -> - Logger.error 1, "Failed to configure nerveshub due to http error: #{inspect(reason)}" + Logger.error 1, "Failed to configure nerveshub due to database error: #{inspect(reason)}" end - :ok end # Message comes over AMQP. diff --git a/farmbot_os/lib/shoehorn_handler.ex b/farmbot_os/lib/shoehorn_handler.ex deleted file mode 100644 index fcb51663..00000000 --- a/farmbot_os/lib/shoehorn_handler.ex +++ /dev/null @@ -1,31 +0,0 @@ -defmodule Farmbot.OS.ShoehornHandler do - use Shoehorn.Handler - require Logger - - def init do - {:ok, %{}} - end - - def application_exited(:farmbot_core, reason, state) do - Logger.error "FarmbotCore exited: #{inspect reason}" - Application.stop(:farmbot) - Application.ensure_all_started(:farmbot) - {:continue, state} - end - - def application_exited(:farmbot, reason, state) do - Logger.error "FarmbotOS exited: #{inspect reason}" - Application.ensure_all_started(:farmbot) - {:continue, state} - end - - def application_exited(:farmbot_ext, reason, state) do - Logger.error "FarmbotExt exited: #{inspect reason}" - Application.ensure_all_started(:farmbot_ext) - {:continue, state} - end - - def application_exited(_app, _reason, state) do - {:continue, state} - end -end diff --git a/farmbot_os/mix.exs b/farmbot_os/mix.exs index 455cc81e..ec204645 100644 --- a/farmbot_os/mix.exs +++ b/farmbot_os/mix.exs @@ -41,8 +41,7 @@ defmodule Farmbot.OS.MixProject do def application do [ mod: {Farmbot.OS, []}, - extra_applications: [:logger, :runtime_tools, :eex], - included_applications: [:farmbot_core, :farmbot_ext] + extra_applications: [:logger, :runtime_tools, :eex] ] end @@ -69,9 +68,6 @@ defmodule Farmbot.OS.MixProject do defp deps(target) do [ - # Configurator - {:cowboy, "~> 2.5"}, - {:plug, "~> 1.6"}, # override: true because AMQP {:ranch, "~> 1.5", override: true}, {:cors_plug, "~> 2.0"}, @@ -85,7 +81,8 @@ defmodule Farmbot.OS.MixProject do {:mdns, "~> 1.0"}, {:nerves_firmware_ssh, "~> 0.3"}, {:nerves_init_gadget, "~> 0.5", only: :dev}, - {:elixir_ale, "~> 1.2"} + {:elixir_ale, "~> 1.2"}, + {:toolshed, "~> 0.2"} ] ++ system(target) end diff --git a/farmbot_os/mix.lock.rpi b/farmbot_os/mix.lock.rpi index 99f8f098..4fa1b229 100644 --- a/farmbot_os/mix.lock.rpi +++ b/farmbot_os/mix.lock.rpi @@ -92,7 +92,8 @@ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, "system_registry": {:hex, :system_registry, "0.8.1", "7df1f66f0e4fcd0940ecd0473d2787d69d2abd6267d21e8f8ecbab58a14415ce", [:mix], [], "hexpm"}, "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, - "timex": {:hex, :timex, "3.4.1", "e63fc1a37453035e534c3febfe9b6b9e18583ec7b37fd9c390efdef97397d70b", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "timex": {:hex, :timex, "3.4.2", "d74649c93ad0e12ce5b17cf5e11fbd1fb1b24a3d114643e86dba194b64439547", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "toolshed": {:hex, :toolshed, "0.2.2", "30fc70ef7d5ce9cdad17f99c28cfbd4ba4ea6199fb9245c542b674fb9f0248a3", [:mix], [], "hexpm"}, "tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "uboot_env": {:hex, :uboot_env, "0.1.0", "176d277c8461d849614d8b82595060bf03ace6ca59d49c5b53707d90cb9e2f5a", [:mix], [], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, diff --git a/farmbot_os/mix.lock.rpi3 b/farmbot_os/mix.lock.rpi3 index eb88a1b1..55f2802e 100644 --- a/farmbot_os/mix.lock.rpi3 +++ b/farmbot_os/mix.lock.rpi3 @@ -81,6 +81,7 @@ "system_registry": {:hex, :system_registry, "0.8.1", "7df1f66f0e4fcd0940ecd0473d2787d69d2abd6267d21e8f8ecbab58a14415ce", [:mix], [], "hexpm"}, "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "timex": {:hex, :timex, "3.4.2", "d74649c93ad0e12ce5b17cf5e11fbd1fb1b24a3d114643e86dba194b64439547", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "toolshed": {:hex, :toolshed, "0.2.2", "30fc70ef7d5ce9cdad17f99c28cfbd4ba4ea6199fb9245c542b674fb9f0248a3", [:mix], [], "hexpm"}, "tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "uboot_env": {:hex, :uboot_env, "0.1.0", "176d277c8461d849614d8b82595060bf03ace6ca59d49c5b53707d90cb9e2f5a", [:mix], [], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, diff --git a/farmbot_os/platform/target/configurator/configurator.ex b/farmbot_os/platform/target/configurator/configurator.ex deleted file mode 100644 index 8fe13354..00000000 --- a/farmbot_os/platform/target/configurator/configurator.ex +++ /dev/null @@ -1,115 +0,0 @@ -defmodule Farmbot.Target.Bootstrap.Configurator do - @moduledoc """ - This init module is used to bring up initial configuration. - If it can't find a configuration it will bring up a captive portal for a device to connect to. - """ - - use Supervisor - require Farmbot.Logger - alias Farmbot.Config - import Config, only: [get_config_value: 3, update_config_value: 4] - alias Farmbot.Target.Bootstrap.Configurator - - @doc """ - This particular init module should block until all settings have been validated. - It handles things such as: - * Initial flashing of the firmware. - * Initial configuration of network settings. - * Initial configuration of farmbot web app settings. - - When finished will return `:ignore` if all went well, or - `{:error, reason}` if there were errors. This will cause a factory - reset and the user will need to configureate again. - """ - def start_link(_) do - Farmbot.Logger.busy(3, "Configuring Farmbot.") - supervisor = Supervisor.start_link(__MODULE__, [self()], name: __MODULE__) - - case supervisor do - {:ok, pid} -> - wait(pid) - - :ignore -> - :ignore - end - end - - defp wait(pid) do - if Process.alive?(pid) do - Process.sleep(1000) - wait(pid) - else - :ignore - end - end - - def init(_) do - first_boot? = get_config_value(:bool, "settings", "first_boot") - - autoconfigure? = - Nerves.Runtime.KV.get("farmbot_auto_configure") - |> case do - "" -> false - other when is_binary(other) -> true - _ -> false - end - - if first_boot? do - maybe_configurate(autoconfigure?) - else - :ignore - end - end - - def stop(supervisor, status) do - Supervisor.stop(supervisor, :normal) - status - end - - defp maybe_configurate(false) do - Farmbot.Logger.info(3, "Building new configuration.") - update_config_value(:bool, "settings", "ignore_fbos_config", true) - update_config_value(:bool, "settings", "ignore_fw_config", true) - import Supervisor.Spec - :ets.new(:session, [:named_table, :public, read_concurrency: true]) - Config.destroy_all_network_configs() - - children = [ - worker(Configurator.CaptivePortal, [], restart: :transient), - {Plug.Adapters.Cowboy, - scheme: :http, plug: Configurator.Router, options: [port: 80, acceptors: 1]} - ] - - opts = [strategy: :one_for_one] - Supervisor.init(children, opts) - end - - defp maybe_configurate(_) do - ifname = Nerves.Runtime.KV.get("farmbot_network_iface") - ssid = Nerves.Runtime.KV.get("farmbot_network_ssid") - psk = Nerves.Runtime.KV.get("farmbot_network_psk") - email = Nerves.Runtime.KV.get("farmbot_email") - server = Nerves.Runtime.KV.get("farmbot_server") - password = Nerves.Runtime.KV.get("farmbot_password") - - Config.input_network_config!(%{ - name: ifname, - ssid: ssid, - security: "WPA-PSK", - psk: psk, - type: if(ssid, do: "wireless", else: "wired"), - domain: nil, - name_servers: nil, - ipv4_method: "dhcp", - ipv4_address: nil, - ipv4_gateway: nil, - ipv4_subnet_mask: nil - }) - - update_config_value(:string, "authorization", "email", email) - update_config_value(:string, "authorization", "password", password) - update_config_value(:string, "authorization", "server", server) - update_config_value(:string, "authorization", "token", nil) - :ignore - end -end diff --git a/farmbot_os/platform/target/configurator/router.ex b/farmbot_os/platform/target/configurator/router.ex index 89aa3643..114f582e 100644 --- a/farmbot_os/platform/target/configurator/router.ex +++ b/farmbot_os/platform/target/configurator/router.ex @@ -1,4 +1,4 @@ -defmodule Farmbot.Target.Bootstrap.Configurator.Router do +defmodule Farmbot.Target.Configurator.Router do @moduledoc "Routes web connections." use Plug.Router @@ -12,6 +12,7 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do require Farmbot.Logger import Phoenix.HTML alias Farmbot.Config + alias Farmbot.Target.Network import Config, only: [ @@ -24,7 +25,7 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do end @version Farmbot.Project.version() - @data_path Application.get_env(:farmbot, :data_path) + @data_path Farmbot.OS.FileSystem.data_path() get "/generate_204" do send_resp(conn, 204, "") @@ -82,7 +83,7 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do # NETWORKCONFIG get "/network" do - interfaces = Farmbot.Target.Network.get_interfaces() + interfaces = Network.list_interfaces() render_page(conn, "network", interfaces: interfaces, post_action: "select_interface") end @@ -119,7 +120,7 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do opts = [ ifname: ifname, - ssids: Farmbot.Target.Network.scan(ifname), + ssids: Network.Utils.scan(ifname), post_action: "config_wireless_step_1" ] @@ -230,24 +231,41 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do update_config_value(:string, "settings", "authorized_ssh_key", ssh_key) end - Config.input_network_config!(%{ - name: ifname, - ssid: ssid, - security: security, - psk: psk, - type: if(ssid, do: "wireless", else: "wired"), - domain: domain, - identity: identity, - password: password, - name_servers: name_servers, - ipv4_method: ipv4_method, - ipv4_address: ipv4_address, - ipv4_gateway: ipv4_gateway, - ipv4_subnet_mask: ipv4_subnet_mask, - regulatory_domain: reg_domain - }) + type = if(ssid, do: "wireless", else: "wired") - redir(conn, "/firmware") + old = + Config.Repo.get_by(Config.NetworkInterface, name: ifname) || %Config.NetworkInterface{} + + changeset = + Config.NetworkInterface.changeset(old, %{ + name: ifname, + ssid: ssid, + security: security, + psk: psk, + type: type, + domain: domain, + identity: identity, + password: password, + name_servers: name_servers, + ipv4_method: ipv4_method, + ipv4_address: ipv4_address, + ipv4_gateway: ipv4_gateway, + ipv4_subnet_mask: ipv4_subnet_mask, + regulatory_domain: reg_domain + }) + + case Config.Repo.insert_or_update(changeset) do + {:ok, _} -> + redir(conn, "/firmware") + + {:error, %{errors: r}} when type == "wireless" -> + Farmbot.Logger.error(1, "Network error: #{inspect(r)}") + redir(conn, "/config_wireless?ifname=#{ifname}") + + {:error, %{errors: r}} when type == "wired" -> + Farmbot.Logger.error(1, "Network error: #{inspect(r)}") + redir(conn, "/config_wired?ifname=#{ifname}") + end rescue e in MissingField -> Farmbot.Logger.error(1, Exception.message(e)) @@ -257,21 +275,6 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do # /NETWORKCONFIG - get "/credentials" do - email = get_config_value(:string, "authorization", "email") || "" - pass = get_config_value(:string, "authorization", "password") || "" - server = get_config_value(:string, "authorization", "server") || "" - first_boot = get_config_value(:bool, "settings", "first_boot") - update_config_value(:string, "authorization", "token", nil) - - render_page(conn, "credentials", - server: server, - email: email, - password: pass, - first_boot: first_boot - ) - end - get "/firmware" do render_page(conn, "firmware") end @@ -285,9 +288,9 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do if Application.get_env(:farmbot, :behaviour)[:firmware_handler] == Farmbot.Firmware.UartHandler do - Farmbot.Logger.warn(1, "Updating #{hw} firmware.") + Farmbot.Logger.warn(1, "Updating #{hw} firmware is broke!!!!!!") # /shrug? - Farmbot.Firmware.UartHandler.Update.force_update_firmware(hw) + # Farmbot.Firmware.UartHandler.Update.force_update_firmware(hw) end redir(conn, "/credentials") @@ -304,6 +307,21 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do end end + get "/credentials" do + email = get_config_value(:string, "authorization", "email") || "" + pass = get_config_value(:string, "authorization", "password") || "" + server = get_config_value(:string, "authorization", "server") || "" + first_boot = get_config_value(:bool, "settings", "first_boot") + update_config_value(:string, "authorization", "token", nil) + + render_page(conn, "credentials", + server: server, + email: email, + password: pass, + first_boot: first_boot + ) + end + post "/configure_credentials" do {:ok, _, conn} = read_body(conn) @@ -333,29 +351,8 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do network = !Enum.empty?(Config.get_all_network_configs()) if email && pass && server && network do - conn = render_page(conn, "finish") - - spawn(fn -> - try do - alias Farmbot.Target.Bootstrap.Configurator - Farmbot.Logger.success(2, "Configuration finished.") - # Allow the page to render and send. - Process.sleep(2500) - :ok = GenServer.stop(Configurator.CaptivePortal, :normal) - # :ok = Supervisor.terminate_child(Configurator, Configurator.CaptivePortal) - :ok = Supervisor.stop(Configurator) - # Good luck. - Process.sleep(2500) - rescue - e -> - Farmbot.Logger.warn( - 1, - "Falied to close captive portal. Good luck. " <> Exception.message(e) - ) - end - end) - - conn + Network.reload() + render_page(conn, "finish") else Farmbot.Logger.warn(3, "Not configured yet. Restarting configuration.") redir(conn, "/") diff --git a/farmbot_os/platform/target/configurator/supervisor.ex b/farmbot_os/platform/target/configurator/supervisor.ex new file mode 100644 index 00000000..5c8d8a71 --- /dev/null +++ b/farmbot_os/platform/target/configurator/supervisor.ex @@ -0,0 +1,23 @@ +defmodule Farmbot.Target.Configurator.Supervisor do + use Supervisor + alias Farmbot.Target.Configurator.{Router, Validator} + + def start_link(args) do + Supervisor.start_link(__MODULE__, args, name: __MODULE__) + end + + def init(_args) do + transport_opts = [ + num_acceptors: 1 + ] + + opts = [port: 80, transport_options: transport_opts] + + children = [ + Validator, + {Plug.Adapters.Cowboy, scheme: :http, plug: Router, options: opts} + ] + + Supervisor.init(children, strategy: :one_for_one) + end +end diff --git a/farmbot_os/platform/target/configurator/validator.ex b/farmbot_os/platform/target/configurator/validator.ex new file mode 100644 index 00000000..4b259f43 --- /dev/null +++ b/farmbot_os/platform/target/configurator/validator.ex @@ -0,0 +1,108 @@ +defmodule Farmbot.Target.Configurator.Validator do + use GenServer + require Logger + import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4] + alias Farmbot.Target.Network + import Farmbot.Target.Network.Utils + alias Farmbot.Bootstrap.Authorization + + @steps [ + :ntp, + :dns, + :auth + ] + + @checkup_time_ms 7000 + # 30 minutes + @success_time_ms 1_800_000 + + def start_link(args) do + GenServer.start_link(__MODULE__, args, name: __MODULE__) + end + + def init(_args) do + {:ok, 0, 0} + end + + def handle_info(:timeout, -1) do + {:noreply, 0, 0} + end + + def handle_info(:timeout, step_index) do + case Enum.at(@steps, step_index) do + :ntp -> check_ntp(step_index) + :dns -> check_dns(step_index) + :auth -> check_auth(step_index) + end + end + + def check_ntp(indx) do + Logger.info("Checking for valid NTP") + + if Nerves.Time.synchronized?() do + {:noreply, indx + 1, 0} + else + Nerves.Time.restart_ntpd() + {:noreply, 0, @checkup_time_ms} + end + end + + def check_dns(indx) do + Logger.info("Checking for valid DNS") + + case test_dns() do + {:ok, _} -> {:noreply, indx + 1, 0} + {:error, _} -> {:noreply, 0, @checkup_time_ms} + end + end + + def check_auth(_indx) do + email = get_config_value(:string, "authorization", "email") + password = get_config_value(:string, "authorization", "password") + secret = get_config_value(:string, "authorization", "secret") + server = get_config_value(:string, "authorization", "server") + + cond do + email && password && server -> + authorize_with_password(email, password, server) + + email && secret && server -> + authorize_with_secret(email, secret, server) + end + end + + defp authorize_with_password(email, password, server) do + with {:ok, {:RSAPublicKey, _, _} = rsa_key} <- Authorization.fetch_rsa_key(server), + secret <- Authorization.build_secret(email, password, rsa_key), + {:ok, payload} <- Authorization.build_payload(secret), + {:ok, resp} <- Authorization.request_token(server, payload), + {:ok, %{"token" => %{"encoded" => tkn}}} <- Farmbot.JSON.decode(resp) do + update_config_value(:string, "authorization", "token", tkn) + update_config_value(:string, "authorization", "secret", secret) + update_config_value(:string, "authorization", "password", nil) + Farmbot.Target.Network.hostap_down() + Network.validate() + {:noreply, -1, @success_time_ms} + else + error -> + Logger.error("Authorization with password failure: #{inspect(error)}") + {:noreply, 0, @checkup_time_ms} + end + end + + defp authorize_with_secret(_email, secret, server) do + with {:ok, payload} <- Authorization.build_payload(secret), + {:ok, resp} <- Authorization.request_token(server, payload), + {:ok, %{"token" => %{"encoded" => tkn}}} <- Farmbot.JSON.decode(resp) do + update_config_value(:string, "authorization", "token", tkn) + update_config_value(:string, "authorization", "password", nil) + Farmbot.Target.Network.hostap_down() + Network.validate() + {:noreply, -1, @success_time_ms} + else + error -> + Logger.error("Authorization with secret failure: #{inspect(error)}") + {:noreply, 0, @checkup_time_ms} + end + end +end diff --git a/farmbot_os/platform/target/info_workers/disk_usage_worker.ex b/farmbot_os/platform/target/info_workers/disk_usage.ex similarity index 94% rename from farmbot_os/platform/target/info_workers/disk_usage_worker.ex rename to farmbot_os/platform/target/info_workers/disk_usage.ex index 733d70ab..c2c4f619 100644 --- a/farmbot_os/platform/target/info_workers/disk_usage_worker.ex +++ b/farmbot_os/platform/target/info_workers/disk_usage.ex @@ -1,4 +1,4 @@ -defmodule Farmbot.Target.DiskUsageWorker do +defmodule Farmbot.Target.InfoWorker.DiskUsage do @moduledoc false use GenServer diff --git a/farmbot_os/platform/target/info_workers/memory_usage_worker.ex b/farmbot_os/platform/target/info_workers/memory_usage.ex similarity index 91% rename from farmbot_os/platform/target/info_workers/memory_usage_worker.ex rename to farmbot_os/platform/target/info_workers/memory_usage.ex index d0dd6a0e..0a211391 100644 --- a/farmbot_os/platform/target/info_workers/memory_usage_worker.ex +++ b/farmbot_os/platform/target/info_workers/memory_usage.ex @@ -1,4 +1,4 @@ -defmodule Farmbot.Target.MemoryUsageWorker do +defmodule Farmbot.Target.InfoWorker.MemoryUsage do @moduledoc false use GenServer diff --git a/farmbot_os/platform/target/info_workers/soc_temp_worker.ex b/farmbot_os/platform/target/info_workers/soc_temp.ex similarity index 77% rename from farmbot_os/platform/target/info_workers/soc_temp_worker.ex rename to farmbot_os/platform/target/info_workers/soc_temp.ex index 147c4f1c..29e7a13e 100644 --- a/farmbot_os/platform/target/info_workers/soc_temp_worker.ex +++ b/farmbot_os/platform/target/info_workers/soc_temp.ex @@ -1,4 +1,4 @@ -defmodule Farmbot.Target.SocTempWorker do +defmodule Farmbot.Target.InfoWorker.SocTemp do @moduledoc false use GenServer @@ -13,7 +13,7 @@ defmodule Farmbot.Target.SocTempWorker do {:ok, nil, 0} end - def handle_info(:report_temp, state) do + def handle_info(:timeout, state) do {temp_str, 0} = Nerves.Runtime.cmd("vcgencmd", ["measure_temp"], :return) temp = @@ -21,11 +21,11 @@ defmodule Farmbot.Target.SocTempWorker do |> String.trim() |> String.split("=") |> List.last() - |> Float.parse() + |> Integer.parse() |> elem(0) if GenServer.whereis(Farmbot.BotState) do - Farmbot.BotState.report_soc_temp(temp) + :ok = Farmbot.BotState.report_soc_temp(temp) {:noreply, state, @default_timeout_ms} else {:noreply, state, @error_timeout_ms} diff --git a/farmbot_os/platform/target/info_workers/supervisor.ex b/farmbot_os/platform/target/info_workers/supervisor.ex new file mode 100644 index 00000000..bba407db --- /dev/null +++ b/farmbot_os/platform/target/info_workers/supervisor.ex @@ -0,0 +1,28 @@ +defmodule Farmbot.Target.InfoWorker.Supervisor do + @moduledoc false + use Supervisor + + alias Farmbot.Target.InfoWorker.{ + DiskUsage, + MemoryUsage, + SocTemp, + Uptime, + WifiLevel + } + + def start_link(args) do + Supervisor.start_link(__MODULE__, args, name: __MODULE__) + end + + def init([]) do + children = [ + DiskUsage, + MemoryUsage, + SocTemp, + Uptime, + {WifiLevel, ifname: "wlan0"} + ] + + Supervisor.init(children, strategy: :one_for_one) + end +end diff --git a/farmbot_os/platform/target/info_workers/uptime_worker.ex b/farmbot_os/platform/target/info_workers/uptime.ex similarity index 87% rename from farmbot_os/platform/target/info_workers/uptime_worker.ex rename to farmbot_os/platform/target/info_workers/uptime.ex index d81799ae..4065a903 100644 --- a/farmbot_os/platform/target/info_workers/uptime_worker.ex +++ b/farmbot_os/platform/target/info_workers/uptime.ex @@ -1,4 +1,4 @@ -defmodule Farmbot.Target.UptimeWorker do +defmodule Farmbot.Target.InfoWorker.Uptime do @moduledoc false use GenServer @@ -13,7 +13,7 @@ defmodule Farmbot.Target.UptimeWorker do {:ok, nil, 0} end - def handle_info(:report_uptime, state) do + def handle_info(:timeout, state) do usage = collect_report() if GenServer.whereis(Farmbot.BotState) do diff --git a/farmbot_os/platform/target/info_workers/wifi_level.ex b/farmbot_os/platform/target/info_workers/wifi_level.ex new file mode 100644 index 00000000..f660bc01 --- /dev/null +++ b/farmbot_os/platform/target/info_workers/wifi_level.ex @@ -0,0 +1,42 @@ +defmodule Farmbot.Target.InfoWorker.WifiLevel do + use GenServer + alias Farmbot.Config + import Farmbot.Target.Network.Utils + + @checkup_time_ms 15_000 + @error_time_ms 60_000 + + def start_link(args) do + GenServer.start_link(__MODULE__, args) + end + + def init(args) do + ifname = Keyword.fetch!(args, :ifname) + {:ok, ifname, 0} + end + + def handle_info(:timeout, ifname) do + case Config.Repo.get_by(Config.NetworkInterface, name: ifname) do + nil -> + {:noreply, ifname, @error_time_ms} + + %{ssid: ssid} -> + do_check_level(ssid, ifname) + end + end + + def do_check_level(ssid, ifname) do + scan(ifname) + |> Enum.find(fn %{ssid: scanned} -> + scanned == ssid + end) + |> case do + nil -> + {:noreply, ifname, @error_time_ms} + + %{level: level} -> + Farmbot.BotState.report_wifi_level(level) + {:noreply, ifname, @checkup_time_ms} + end + end +end diff --git a/farmbot_os/platform/target/network.ex b/farmbot_os/platform/target/network.ex new file mode 100644 index 00000000..1a94b566 --- /dev/null +++ b/farmbot_os/platform/target/network.ex @@ -0,0 +1,238 @@ +defmodule Farmbot.Target.Network do + @moduledoc "Manages Network Connections" + use GenServer + require Logger + + alias Nerves.NetworkInterface + import Farmbot.Target.Network.Utils + alias Farmbot.Config + + @validation_ms 30_000 + + defmodule State do + @moduledoc false + defstruct ifnames: [], + config: %{}, + hostap: :down, + hostap_dhcp_server_pid: nil, + hostap_wpa_supplicant_pid: nil + end + + def reload do + for %{name: ifname} = settings <- Config.Repo.all(Config.NetworkInterface) do + settings = validate_settings(settings) + Logger.warn("Trying to configure #{ifname}: #{inspect(settings)}") + setup(ifname, settings) + end + end + + def validate_settings(%{type: "wired"} = settings) do + validate_advanced(settings) + end + + def validate_settings(settings) do + ssid = Map.fetch!(settings, :ssid) + psk = Map.fetch!(settings, :psk) + + key_mgmt = + case Map.fetch!(settings, :security) do + "WPA-PSK" -> :"WPA-PSK" + "WPA2-PSK" -> :"WPA-PSK" + "NONE" -> :NONE + end + + [ + ssid: ssid, + psk: psk, + key_mgmt: key_mgmt + ] ++ validate_advanced(settings) + end + + def validate_advanced(%{ipv4_address: "static"} = settings) do + [ + ipv4_address_method: :static, + ipv4_address: Map.fetch!(settings, :ipv4_address), + ipv4_gateway: Map.fetch!(settings, :ipv4_gateway), + ipv4_subnet_mask: Map.fetch!(settings, :ipv4_subnet_mask) + ] + end + + def validate_advanced(%{ipv4_method: _} = _settings) do + [] + end + + def list_interfaces do + ifnames() + |> List.delete("lo") + |> Enum.map(fn ifname -> + IO.puts("Looking up #{ifname}") + {:ok, settings} = Nerves.NetworkInterface.settings(ifname) + {ifname, settings} + end) + end + + @doc "List all ifnames the Network Manager knows about." + def ifnames do + GenServer.call(__MODULE__, :ifnames) + end + + @doc "Bring down hostap, bring up networking. Will reset if not `validate/0`d in time." + def setup(ifname, settings) do + GenServer.cast(__MODULE__, {:setup, ifname, settings}) + end + + @doc "Validate the config given to the Network manager" + def validate do + GenServer.cast(__MODULE__, :validate) + end + + @doc "Bring down networking, bring up hostap" + def hostap_up do + GenServer.cast(__MODULE__, :hostap_up) + end + + @doc "Bring down hostap, Bring up networking" + def hostap_down do + GenServer.cast(__MODULE__, :hostap_down) + end + + @doc "Start the Network manager." + def start_link(args) do + GenServer.start_link(__MODULE__, args, name: __MODULE__) + end + + def init(args) do + config = Keyword.get(args, :config, %{}) + {:ok, %State{config: config}, 0} + end + + def terminate(_, state) do + state = try_stop_dhcp(state) + + for ifname <- state.ifnames do + Nerves.Network.teardown(ifname) + Nerves.NetworkInterface.ifdown(ifname) + end + end + + def handle_call(:ifnames, _from, state) do + {:reply, state.ifnames, state} + end + + def handle_cast({:setup, ifname, opts}, state) do + {:noreply, %{state | config: Map.put(state.config, ifname, opts)}, 0} + end + + def handle_cast(:hostap_up, state) do + setup_hostap(state) + end + + def handle_cast(:hostap_down, state) do + {:noreply, stop_hostap(state), 0} + end + + def handle_cast(:validate, state) do + Logger.info("Config validated") + {:noreply, %{state | hostap: :validated}} + end + + def handle_info(:timeout, %{ifnames: []} = state) do + Logger.info("Detecting ifnames") + ifnames = NetworkInterface.interfaces() + {:noreply, %{state | ifnames: ifnames}, 0} + end + + def handle_info(:timeout, %{config: c} = state) when map_size(c) == 0 do + setup_hostap(state) + end + + def handle_info(:timeout, %{hostap: :pending_validation} = state) do + Logger.warn("Config not validated in time.") + setup_hostap(%{state | config: %{}}) + end + + def handle_info(:timeout, state) do + Logger.info("Waiting #{@validation_ms} ms for config to be validated.") + state = stop_hostap(state) + + for {ifname, conf} <- state.config do + Logger.debug("Nerves.Network.setup(#{inspect(ifname)}, #{inspect(conf)}") + Nerves.Network.setup(ifname, conf) + end + + {:noreply, %{state | hostap: :pending_validation}, @validation_ms} + end + + def stop_hostap(%{hostap: :up} = state) do + state = try_stop_dhcp(state) + + for {ifname, _conf} <- state.config do + Nerves.Network.teardown(ifname) + Nerves.Network.setup(ifname, []) + Nerves.NetworkInterface.setup(ifname, []) + end + + %{state | hostap: :down, hostap_wpa_supplicant_pid: nil} + end + + # hostap down or not available. + def stop_hostap(state), do: state + + def setup_hostap(%{hostap: :up} = state), do: {:noreply, state} + + def setup_hostap(state) do + hostap_opts = [ + ssid: build_hostap_ssid(), + key_mgmt: :NONE, + mode: 2 + ] + + ip_opts = [ + ipv4_address_method: :static, + ipv4_address: "192.168.24.1", + ipv4_gateway: "192.168.24.1", + ipv4_subnet_mask: "255.255.0.0", + nameservers: ["192.168.24.1"] + ] + + dhcp_opts = [ + gateway: "192.168.24.1", + netmask: "255.255.255.0", + range: {"192.168.24.2", "192.168.24.10"}, + domain_servers: ["192.168.24.1"] + ] + + Nerves.Network.teardown("wlan0") + Nerves.Network.setup("wlan0", hostap_opts) + Nerves.NetworkInterface.setup("wlan0", ip_opts) + {:ok, hostap_dhcp_server_pid} = DHCPServer.start_link("wlan0", dhcp_opts) + {:ok, hostap_wpa_supplicant_pid} = wait_for_wpa("wlan0") + + {:noreply, + %{ + state + | hostap: :up, + hostap_dhcp_server_pid: hostap_dhcp_server_pid, + hostap_wpa_supplicant_pid: hostap_wpa_supplicant_pid + }} + end + + defp wait_for_wpa(ifname) do + # Logger.debug("waiting for #{ifname} wpa_supplicant") + name = :"Nerves.WpaSupplicant.#{ifname}" + + case GenServer.whereis(name) do + nil -> wait_for_wpa(ifname) + pid -> {:ok, pid} + end + end + + defp try_stop_dhcp(state) do + if state.hostap_dhcp_server_pid && Process.alive?(state.hostap_dhcp_server_pid) do + Logger.debug("Stopping DHCP Server") + GenServer.stop(state.hostap_dhcp_server_pid, :normal) + end + + %{state | hostap_dhcp_server_pid: nil} + end +end diff --git a/farmbot_os/platform/target/network/distribution.ex b/farmbot_os/platform/target/network/distribution.ex new file mode 100644 index 00000000..68677e8d --- /dev/null +++ b/farmbot_os/platform/target/network/distribution.ex @@ -0,0 +1,175 @@ +defmodule Farmbot.Target.Network.Distribution do + @moduledoc false + + use GenServer + require Logger + + defmodule State do + @moduledoc false + defstruct ip: nil, is_up: nil, opts: nil + end + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts) + end + + def init(opts) do + # Register for updates from system registry + SystemRegistry.register() + + state = + %State{opts: opts} + |> init_mdns(opts) + |> init_net_kernel(opts) + + {:ok, state} + end + + def handle_info({:system_registry, :global, registry}, state) do + {changed, new_state} = update_state(state, registry) + + case changed do + :up -> handle_if_up(new_state) + :down -> handle_if_down(new_state) + _ -> :ok + end + + {:noreply, new_state} + end + + defp update_state(state, registry) do + new_ip = get_in(registry, [:state, :network_interface, state.opts.ifname, :ipv4_address]) + + case {new_ip == state.ip, new_ip} do + {true, _} -> {:unchanged, state} + {false, nil} -> {:down, %{state | ip: nil, is_up: false}} + {false, _ip} -> {:up, %{state | ip: new_ip, is_up: true}} + end + end + + defp handle_if_up(state) do + Logger.debug("#{state.opts.ifname} is up. IP is now #{state.ip}") + + update_mdns(state.ip, state.opts.mdns_domain) + update_net_kernel(state.ip, state.opts) + end + + defp handle_if_down(_state), do: :ok + + defp init_mdns(state, %{mdns_domain: nil}), do: state + + defp init_mdns(state, opts) do + Mdns.Server.add_service(%Mdns.Server.Service{ + domain: resolve_mdns_name(opts.mdns_domain), + data: :ip, + ttl: 120, + type: :a + }) + + state + end + + defp resolve_mdns_name(nil), do: nil + + defp resolve_mdns_name(:hostname) do + {:ok, hostname} = :inet.gethostname() + + to_dot_local_name(hostname) + end + + defp resolve_mdns_name(mdns_name), do: mdns_name + + defp to_dot_local_name(name) do + # Use the first part of the domain name and concatenate '.local' + name + |> to_string() + |> String.split(".") + |> hd() + |> Kernel.<>(".local") + end + + defp update_mdns(_ip, nil), do: :ok + + defp update_mdns(ip, _mdns_domain) do + ip_tuple = string_to_ip(ip) + Mdns.Server.stop() + + # Give the interface time to settle to fix an issue where mDNS's multicast + # membership is not registered. This occurs on wireless interfaces and + # needs to be revisited. + :timer.sleep(100) + + Mdns.Server.start(interface: ip_tuple) + Mdns.Server.set_ip(ip_tuple) + end + + defp init_net_kernel(state, opts) do + if erlang_distribution_enabled?(opts) do + :os.cmd('epmd -daemon') + end + + state + end + + defp update_net_kernel(ip, opts) do + new_name = make_node_name(opts, ip) + + if new_name do + :net_kernel.stop() + + case :net_kernel.start([new_name]) do + {:ok, _} -> + :ok = Farmbot.BotState.set_node_name(to_string(new_name)) + Logger.debug("Restarted Erlang distribution as node #{inspect(new_name)}") + + {:error, reason} -> + Logger.error("Erlang distribution failed to start: #{inspect(reason)}") + end + end + end + + defp string_to_ip(s) do + {:ok, ip} = :inet.parse_address(to_charlist(s)) + ip + end + + defp erlang_distribution_enabled?(opts) do + make_node_name(opts, "fake.ip") != nil + end + + defp resolve_dhcp_name(fallback) do + with {:ok, hostname} <- :inet.gethostname(), + {:ok, {:hostent, dhcp_name, _, _, _, _}} <- :inet.gethostbyname(hostname) do + dhcp_name + else + _ -> fallback + end + end + + defp make_node_name(%{node_name: name, node_host: :ip}, ip) do + to_node_name(name, ip) + end + + defp make_node_name(%{node_name: name, node_host: :dhcp}, ip) do + to_node_name(name, resolve_dhcp_name(ip)) + end + + defp make_node_name(%{node_name: name, node_host: :mdns_domain, mdns_domain: host}, _ip) + when host != nil do + to_node_name(name, resolve_mdns_name(host)) + end + + defp make_node_name(%{node_name: name, node_host: :mdns_domain, mdns_domain: host}, ip) + when host == nil do + # revert to IP address if no mdns domain + to_node_name(name, ip) + end + + defp make_node_name(%{node_name: name, node_host: host}, _ip) do + to_node_name(name, host) + end + + defp to_node_name(nil, _host), do: nil + defp to_node_name(_name, nil), do: nil + defp to_node_name(name, host), do: :"#{name}@#{host}" +end diff --git a/farmbot_os/platform/target/network/info_supervisor.ex b/farmbot_os/platform/target/network/info_supervisor.ex deleted file mode 100644 index b6b21917..00000000 --- a/farmbot_os/platform/target/network/info_supervisor.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Farmbot.Target.Network.InfoSupervisor do - use GenServer - alias Farmbot.Config - - def start_link(args) do - Supervisor.start_link(__MODULE__, args, name: __MODULE__) - end - - def init([]) do - configs = Config.get_all_network_configs() - - children = - Enum.map(configs, fn config -> - {Farmbot.Target.Network.InfoWorker, [config]} - end) - - Supervisor.init(children, strategy: :one_for_one) - end -end diff --git a/farmbot_os/platform/target/network/info_worker.ex b/farmbot_os/platform/target/network/info_worker.ex deleted file mode 100644 index 0a2e8b08..00000000 --- a/farmbot_os/platform/target/network/info_worker.ex +++ /dev/null @@ -1,31 +0,0 @@ -defmodule Farmbot.Target.Network.InfoWorker do - @moduledoc false - use GenServer - @default_timeout_ms 60_000 - - def start_link(args) do - GenServer.start_link(__MODULE__, args) - end - - def init([config]) do - {:ok, config, 0} - end - - def handle_info(:timeout, %{type: "wireless"} = config) do - if level = Farmbot.Target.Network.get_level(config.name, config.ssid) do - report_wifi_level(level) - end - - {:noreply, config, @default_timeout_ms} - end - - def handle_info(:timeout, config) do - {:noreply, config, :hibernate} - end - - def report_wifi_level(level) do - if GenServer.whereis(Farmbot.BotState) do - Farmbot.BotState.report_wifi_level(level) - end - end -end diff --git a/farmbot_os/platform/target/network/network_not_found_timer.ex b/farmbot_os/platform/target/network/network_not_found_timer.ex deleted file mode 100644 index 505137d6..00000000 --- a/farmbot_os/platform/target/network/network_not_found_timer.ex +++ /dev/null @@ -1,100 +0,0 @@ -defmodule Farmbot.Target.Network.NotFoundTimer do - use GenServer - import Farmbot.Config, only: [get_config_value: 3] - require Farmbot.Logger - - def query do - GenServer.call(__MODULE__, :query) - end - - def start do - GenServer.call(__MODULE__, :start) - end - - def stop do - GenServer.call(__MODULE__, :stop) - end - - def start_link(args) do - GenServer.start_link(__MODULE__, args, name: __MODULE__) - end - - def init([]) do - {:ok, %{timer: nil}} - end - - def handle_call(:query, _, state) do - if state.timer do - r = Process.read_timer(state.timer) - {:reply, r, state} - else - {:reply, nil, state} - end - end - - def handle_call(:start, _from, %{timer: nil} = state) do - minutes = get_config_value(:float, "settings", "network_not_found_timer") || 1 - ms = (minutes * 60_000) |> round() - timer = Process.send_after(self(), :timer, ms) - Farmbot.Logger.debug(1, "Starting network not found timer: #{minutes} minute(s)") - {:reply, :ok, %{state | timer: timer}} - end - - # Timer already started - def handle_call(:start, _from, state) do - {:reply, :ok, state} - end - - def handle_call(:stop, _from, state) do - if state.timer do - Process.cancel_timer(state.timer) - end - - {:reply, :ok, %{state | timer: nil}} - end - - def handle_info(:timer, state) do - delay_minutes = get_config_value(:float, "settings", "network_not_found_timer") || 1 - disable_factory_reset? = get_config_value(:bool, "settings", "disable_factory_reset") - first_boot? = get_config_value(:bool, "settings", "first_boot") - - cond do - disable_factory_reset? -> - Farmbot.Logger.warn(1, "Factory reset is disabled. Not resetting.") - {:noreply, %{state | timer: nil}} - - first_boot? -> - msg = """ - Network not found after #{delay_minutes} minute(s). - possible causes of this include: - - 1) A typo if you manually inputted the SSID. - - 2) The access point is out of range - - 3) There is too much radio interference around Farmbot. - - 5) There is a hardware issue. - """ - - Farmbot.Logger.error(1, msg) - Farmbot.System.factory_reset(msg) - {:stop, :normal, %{state | timer: nil}} - - true -> - Farmbot.Logger.error(1, "Network not found after timer. Farmbot is disconnected.") - - msg = """ - Network not found after #{delay_minutes} minute(s). - This can happen if your wireless access point is no longer available, - out of range, or there is too much radio interference around Farmbot. - If you see this message intermittently you should disable \"automatic - factory reset\" or tune the \"network not found - timer\" value in the Farmbot Web Application. - """ - - Farmbot.System.factory_reset(msg) - {:stop, :normal, %{state | timer: nil}} - end - end -end diff --git a/farmbot_os/platform/target/network/supervisor.ex b/farmbot_os/platform/target/network/supervisor.ex new file mode 100644 index 00000000..10e80dfc --- /dev/null +++ b/farmbot_os/platform/target/network/supervisor.ex @@ -0,0 +1,35 @@ +defmodule Farmbot.Target.Network.Supervisor do + use Supervisor + + def start_link(args) do + Supervisor.start_link(__MODULE__, args, name: __MODULE__) + end + + def init([]) do + {:ok, hostname} = :inet.gethostname() + confs = Farmbot.Config.Repo.all(Farmbot.Config.NetworkInterface) + + config = + Map.new(confs, fn %{name: ifname} = settings -> + {ifname, Farmbot.Target.Network.validate_settings(settings)} + end) + + distribution_children = + Enum.map(confs, fn %{name: ifname} -> + opts = %{ + ifname: ifname, + mdns_domain: "#{hostname}.local", + node_name: "farmbot", + node_host: :mdns_domain + } + + {Farmbot.Target.Network.Distribution, opts} + end) + + children = [ + {Farmbot.Target.Network, config: config} | distribution_children + ] + + Supervisor.init(children, strategy: :one_for_one) + end +end diff --git a/farmbot_os/platform/target/network/utils.ex b/farmbot_os/platform/target/network/utils.ex new file mode 100644 index 00000000..ceae57d6 --- /dev/null +++ b/farmbot_os/platform/target/network/utils.ex @@ -0,0 +1,141 @@ +defmodule Farmbot.Target.Network.Utils do + import Farmbot.Config, only: [get_config_value: 3] + require Farmbot.Logger + alias Farmbot.Target.Network.ScanResult + + def build_hostap_ssid do + {:ok, hostname} = :inet.gethostname() + + if String.starts_with?(to_string(hostname), "farmbot-") do + to_string('farmbot-' ++ Enum.take(hostname, -4)) + else + to_string(hostname) + end + end + + @doc "Scan on an interface." + def scan(iface) do + do_scan(iface) + |> ScanResult.decode() + |> ScanResult.sort_results() + |> ScanResult.decode_security() + |> Enum.filter(&Map.get(&1, :ssid)) + |> Enum.map(&Map.update(&1, :ssid, nil, fn ssid -> to_string(ssid) end)) + |> Enum.reject(&String.contains?(&1.ssid, "\\x00")) + |> Enum.uniq_by(fn %{ssid: ssid} -> ssid end) + end + + defp wait_for_results(pid) do + Nerves.WpaSupplicant.request(pid, :SCAN_RESULTS) + |> String.trim() + |> String.split("\n") + |> tl() + |> Enum.map(&String.split(&1, "\t")) + |> reduce_decode() + |> case do + [] -> + Process.sleep(500) + wait_for_results(pid) + + res -> + res + end + end + + defp reduce_decode(results, acc \\ []) + defp reduce_decode([], acc), do: Enum.reverse(acc) + + defp reduce_decode([[bssid, freq, signal, flags, ssid] | rest], acc) do + decoded = %{ + bssid: bssid, + frequency: String.to_integer(freq), + flags: flags, + level: String.to_integer(signal), + ssid: ssid + } + + reduce_decode(rest, [decoded | acc]) + end + + defp reduce_decode([[bssid, freq, signal, flags] | rest], acc) do + decoded = %{ + bssid: bssid, + frequency: String.to_integer(freq), + flags: flags, + level: String.to_integer(signal), + ssid: nil + } + + reduce_decode(rest, [decoded | acc]) + end + + defp reduce_decode([_ | rest], acc) do + reduce_decode(rest, acc) + end + + def do_scan(iface) do + pid = :"Nerves.WpaSupplicant.#{iface}" + + if Process.whereis(pid) do + Nerves.WpaSupplicant.request(pid, :SCAN) + wait_for_results(pid) + else + [] + end + end + + def get_level(ifname, ssid) do + r = scan(ifname) + + if res = Enum.find(r, &(Map.get(&1, :ssid) == ssid)) do + res.level + end + end + + @doc "Tests if we can make dns queries." + def test_dns(hostname \\ nil) + + def test_dns(nil) do + case get_config_value(:string, "authorization", "server") do + nil -> + test_dns(get_config_value(:string, "settings", "default_dns_name")) + + url when is_binary(url) -> + %URI{host: hostname} = URI.parse(url) + test_dns(hostname) + end + end + + def test_dns(hostname) when is_binary(hostname) do + test_dns(to_charlist(hostname)) + end + + def test_dns(hostname) do + :ok = :inet_db.clear_cache() + # IO.puts "testing dns: #{hostname}" + case :inet.parse_ipv4_address(hostname) do + {:ok, addr} -> {:ok, {:hostent, hostname, [], :inet, 4, [addr]}} + _ -> :inet_res.gethostbyname(hostname) + end + end + + @fb_data_dir Farmbot.OS.FileSystem.data_path() + @tzdata_dir Application.app_dir(:tzdata, "priv") + def maybe_hack_tzdata do + case Tzdata.Util.data_dir() do + @fb_data_dir -> + :ok + + _ -> + Farmbot.Logger.debug(3, "Hacking tzdata.") + objs_to_cp = Path.wildcard(Path.join(@tzdata_dir, "*")) + + for obj <- objs_to_cp do + File.cp_r(obj, @fb_data_dir) + end + + Application.put_env(:tzdata, :data_dir, @fb_data_dir) + :ok + end + end +end diff --git a/farmbot_os/platform/target/network/wait_for_time.ex b/farmbot_os/platform/target/network/wait_for_time.ex deleted file mode 100644 index b71f44c4..00000000 --- a/farmbot_os/platform/target/network/wait_for_time.ex +++ /dev/null @@ -1,38 +0,0 @@ -defmodule Farmbot.Target.Network.WaitForTime do - @moduledoc "Blocks until time is synced." - require Farmbot.Logger - - nerves_time = - case Nerves.Time.FileTime.time() do - {:error, _} -> NaiveDateTime.utc_now() - ndt -> ndt - end - - @nerves_time nerves_time - - def start_link(_args) do - :ok = wait_for_time() - Farmbot.Logger.success(3, "Time seems to be set. Moving on.") - IO.puts("Check: #{inspect(@nerves_time)}") - IO.puts("Current: #{inspect(NaiveDateTime.utc_now())}") - :ignore - end - - # • -1 -- the first date comes before the second one - # • 0 -- both arguments represent the same date when coalesced to the same - # timezone. - # • 1 -- the first date comes after the second one - - defp wait_for_time do - case Timex.compare(NaiveDateTime.utc_now(), get_file_time()) do - 1 -> - :ok - - _ -> - Process.sleep(1000) - wait_for_time() - end - end - - def get_file_time, do: @nerves_time -end diff --git a/farmbot_os/platform/target/network/tzdata_task.ex b/farmbot_os/platform/target/tzdata_task.ex similarity index 91% rename from farmbot_os/platform/target/network/tzdata_task.ex rename to farmbot_os/platform/target/tzdata_task.ex index e8681d5d..382c13fe 100644 --- a/farmbot_os/platform/target/network/tzdata_task.ex +++ b/farmbot_os/platform/target/tzdata_task.ex @@ -1,4 +1,4 @@ -defmodule Farmbot.Target.Network.TzdataTask do +defmodule Farmbot.Target.TzdataTask do use GenServer @data_path Farmbot.OS.FileSystem.data_path() diff --git a/farmbot_os/rel/vm.args b/farmbot_os/rel/vm.args index 4345ce1c..891d2d03 100644 --- a/farmbot_os/rel/vm.args +++ b/farmbot_os/rel/vm.args @@ -1,10 +1,43 @@ +## Add custom options here + +## Distributed Erlang Options +## The cookie needs to be configured prior to vm boot for +## for read only filesystem. + +# -name farmbot@0.0.0.0 -setcookie democookie + +## Use Ctrl-C to interrupt the current shell rather than invoking the emulator's +## break handler and possibly exiting the VM. +Bc + +# Allow time warps so that the Erlang system time can more closely match the +# OS system time. ++C multi_time_warp + +## Load code at system startup +## See http://erlang.org/doc/system_principles/system_principles.html#code-loading-strategy +# -mode embedded + +## Save the shell history between reboots +## See http://erlang.org/doc/man/kernel_app.html for additional options -kernel shell_history enabled --mode embedded --sname farmbot + +## Enable heartbeat monitoring of the Erlang runtime system +-heart -env HEART_BEAT_TIMEOUT 30 + +## Start the Elixir shell + -noshell -user Elixir.IEx.CLI --heart -+C multi_time_warp + +## Enable colors in the shell +-elixir ansi_enabled true + +## Options added after -extra are interpreted as plain arguments and can be +## retrieved using :init.get_plain_arguments(). Options before the "--" are +## interpreted by Elixir and anything afterwards is left around for other IEx +## and user applications. -extra --no-halt +-- +--dot-iex /etc/iex.exs diff --git a/farmbot_os/rootfs_overlay/etc/iex.exs b/farmbot_os/rootfs_overlay/etc/iex.exs index 07fc0ba2..a4229cd7 100644 --- a/farmbot_os/rootfs_overlay/etc/iex.exs +++ b/farmbot_os/rootfs_overlay/etc/iex.exs @@ -1,22 +1 @@ -# Pull in Nerves-specific helpers to the IEx session -use Nerves.Runtime.Helpers - -# Be careful when adding to this file. Nearly any error can crash the VM and -# cause a reboot. -alias Farmbot.System.{ - ConfigStorage -} - -alias Farmbot.Asset -alias Farmbot.Asset.{ - Device, - FarmEvent, - GenericPointer, - Peripheral, - Point, - Regimen, - Sensor, - Sequence, - ToolSlot, - Tool -} +use Toolshed