-
Notifications
You must be signed in to change notification settings - Fork 69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add device extensions support #1479
base: main
Are you sure you want to change the base?
Conversation
61b9720
to
e14326c
Compare
e14326c
to
2963f8d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a good approach to me. Want me to flesh out the allow mechanisms as a branch off of this branch or something?
Or we merge this once we tidy up the dbg and TODO notes?
Fixing the rebase. But ready for review @joshk and I've cut anything that would cause legacy for the yet-unreleased health and geo features. So two env vars have changed to match. Messaging internally has changed. Message to the link-side features have been aligned to all do similar stuff. |
8ea0780
to
1db8da2
Compare
I need to manually test this locally to make sure everything works correctly. But would love review @joshk so when I've tried it properly we can move this and the matching nerves_hub_link along. |
b8e62e9
to
5de29ff
Compare
@jjcarstens was this just a rebase? |
Running this and nerves_hub_link locally, catching a bunch of trickiness :) |
Marking as draft while I churn through renaming and so on. |
410d841
to
e2c441d
Compare
Allows for specialized extensions on device to report data and interactions safely outside the without affecting the firmware update mechanism. - Geo and Health adapted to the new mechanism. - Product-level settings to enable/disable - Device-level settings to mostly disable
e2c441d
to
ede38df
Compare
Re-wrote Jon's words above to be all about EXTENSIONS! 🤘🏻 👀 🤘🏻 I've renamed all the things. The sibling nerves_hub_link PR seems healthy and solid. I put one test on pending because it is just messy to update for now. I think everything is good to go. Or at least ready to be picked over :D |
So paging @joshk and @jjcarstens for reviewing :) |
@@ -1383,4 +1383,33 @@ defmodule NervesHub.Devices do | |||
"pending" | |||
end | |||
end | |||
|
|||
def enable_extension_setting(%Device{} = device, extension_string) do | |||
device = Repo.get(Device, device.id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we fetching the device if we are requiring it as an arg?
|
||
Device.changeset(device, %{"extensions" => %{extension_string => true}}) | ||
|> Repo.update() | ||
|> tap(fn _ -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we will need to check that the update was successful before broadcasting, otherwise we will be out of sync with the setting
def enable_extension_setting(%Device{} = device, extension_string) do | ||
device = Repo.get(Device, device.id) | ||
|
||
Device.changeset(device, %{"extensions" => %{extension_string => true}}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this actually work if I have multiple extensions? To me it looks like it will completely overwrite all extension settings with just this one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not, embeds_one
raises by default on replace. I think an on_replace: :update
will work here.
@supported_extensions %{ | ||
health: """ | ||
Reporting of fundamental device metrics, metadata, alarms and more. | ||
Also supports custom metrics. Alarms require an alarm handler to be set. | ||
""", | ||
geo: """ | ||
Reporting of GeoIP information or custom geo-location information sources | ||
you've set up for your device. | ||
""" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a behavior module, I feel like we should keep as much of this in the extension as possible. Maybe we need a description
callback for the module to define it?
case result do | ||
{:ok, _pf} -> | ||
# reload extensions | ||
assign(socket, :extensions, Extensions.list()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extensions.list()
is static. Do we need to reload it? In fact, we probably don't even need to hold it in the assigns and just render with the view. Save some memory
@@ -94,7 +95,7 @@ defmodule NervesHubWeb.DeviceSocket do | |||
|> Devices.get_device_certificate_by_x509() | |||
|> case do | |||
{:ok, %{device: %Device{} = device}} -> | |||
socket_and_assigns(socket, device) | |||
socket_and_assigns(socket, Devices.preload_product(device)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to include this preload in a more effect way? Like with getting the device certificate? I know Josh did a lot of work optimizing preloads so I'm hesitant to put such a small function in without thinking through
@@ -34,6 +34,7 @@ defmodule NervesHubWeb.Live.Devices.DeviceHealth do | |||
|
|||
if connected?(socket) do | |||
socket.endpoint.subscribe("device:#{device.identifier}:internal") | |||
socket.endpoint.subscribe("device:#{device.identifier}:extensions") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are we doing with this subscription here?
product_extensions | ||
|> Map.from_struct() | ||
|> Enum.filter(fn {extension, enabled?} -> | ||
enabled? == true and Map.get(device_extensions, extension) != false | ||
end) | ||
|> Enum.map(&elem(&1, 0)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
product_extensions | |
|> Map.from_struct() | |
|> Enum.filter(fn {extension, enabled?} -> | |
enabled? == true and Map.get(device_extensions, extension) != false | |
end) | |
|> Enum.map(&elem(&1, 0)) | |
for {extension, true} <- Map.from_struct(product_extensions), | |
{^extension, device_enabled?} <- Map.from_struct(device_extensions, | |
device_enabled? != false, | |
do: extension |
Slight optimization. Less looping, more direct filtering. Gets you list of extensions only allowed at product and device
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good so far; I left some comments, questions, and concerns.
def enable_extension_setting(%Device{} = device, extension_string) do | ||
device = Repo.get(Device, device.id) | ||
|
||
Device.changeset(device, %{"extensions" => %{extension_string => true}}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not, embeds_one
raises by default on replace. I think an on_replace: :update
will work here.
@@ -87,6 +87,8 @@ config :nerves_hub, NervesHub.SwooshMailer, adapter: Swoosh.Adapters.Test | |||
|
|||
config :nerves_hub, NervesHub.RateLimit, limit: 100 | |||
|
|||
config :sentry, environment_name: :test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this do? Were we reporting errors to Sentry from tests before? 😨
# @impl NervesHub.Extensions | ||
def init(socket) do | ||
# Allow DB settings? | ||
socket | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cruft?
|
||
health_interval = | ||
case get_in(extension_config, [:health, :interval_minutes]) do | ||
i when is_integer(i) -> i |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this needed if we're calling String.to_integer()
in runtime.exs
?
def enable_extension_setting(%Product{} = product, extension_string) do | ||
product = Repo.get(Product, product.id) | ||
|
||
Product.changeset(product, %{"extensions" => %{extension_string => true}}) | ||
|> Repo.update() | ||
|> tap(fn _ -> | ||
topic = "product:#{product.id}:extensions" | ||
|
||
NervesHubWeb.DeviceEndpoint.broadcast(topic, "attach", %{"extensions" => [extension_string]}) | ||
end) | ||
end | ||
|
||
def disable_extension_setting(%Product{} = product, extension_string) do | ||
product = Repo.get(Product, product.id) | ||
|
||
Product.changeset(product, %{"extensions" => %{extension_string => false}}) | ||
|> Repo.update() | ||
|> tap(fn _ -> | ||
topic = "product:#{product.id}:extensions" | ||
|
||
NervesHubWeb.DeviceEndpoint.broadcast(topic, "detach", %{"extensions" => [extension_string]}) | ||
end) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on refactoring this into a toggle_extension_setting
vs handling each condition explicitly?
end | ||
|
||
describe "device location" do | ||
# This test needs rework due to the extensions mechanism | ||
@tag :pending |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you did some work on this test, is anything more needed?
|> assign(:reply, reply) | ||
|
||
{:ok, socket} | ||
end | ||
|
||
@impl true | ||
def handle_message("device", "extensions:get", _message, socket) do | ||
{:ok, socket} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we do nothing here?
Start of device extensions which allow for specialized functionality on device to report data and interactions safely outside the update mechanism.
Requires nerves-hub/nerves_hub_link#228
Based on conversations the last while, these are some of the core design decisions so far:
With that in mind, this is the basic communication cycle for supporting extensions on device.
TODO: All the bits to selectively decide what extensions can be enabled for a device