dialyzer: replace suppressions with real fixes where possible
Changes to eliminate @dialyzer suppressions by fixing underlying causes:
NIF stubs (towerops_native.ex, mib_translator.ex):
- Change stubs to :erlang.nif_error(:nif_not_loaded) (no_return type).
Real NIF replaces stubs at load time; calls to unloaded stubs now fail
loudly instead of returning fake data. Lets dialyzer trust @spec.
- Remove @dialyzer :nowarn_function on three NIFs and on translate/1.
Discovery sync_* functions (snmp/discovery.ex, channels/agent_channel.ex):
- agent_channel passes %{device_id: _, interfaces: _} and %{id: _} maps
into Discovery.sync_ip_addresses/sync_processors/sync_storage, which
@spec'd only %Device{}. Add narrow map-type unions (ip_sync_device,
snmp_device_ref) reflecting what the functions actually access.
- Remove @dialyzer :nowarn_function on three agent_channel helpers.
remote_ip.ex — real bug caught and fixed:
- `:ranch.get_addr(socket.transport_pid)` was always raising since
Bandit uses ThousandIsland, not Ranch; the rescue _ -> nil silently
returned nil every time. Switched to Phoenix's documented
:peer_data connect_info (already enabled in endpoint.ex) via
socket.assigns; remote IP now actually works.
- Remove remote_ip.ex entry from .dialyzer_ignore.exs.
Accounts / Organizations (Ecto.Multi opacity):
- Add @specs to Multi-building helpers, refactor into pipe chains.
- 6 @dialyzer :nowarn_function → 0, but 7 :no_opaque remain. Root
cause is upstream: Ecto.Multi.new/0 returns a struct with a literal
%MapSet{} whose @opaque internal representation trips dialyzer on
every subsequent Multi.* call. Unfixable without an Ecto patch or
bypassing Multi entirely. Comments document the specific upstream
issue rather than a vague "Ecto.Multi opacity" claim.
Devices.ex:
- Adding @specs made it worse (call_without_opaque → contract_with_
opaque); inlining the Multi didn't help either — same MapSet root
cause. Suppression kept with a sharper comment.
This commit is contained in:
parent
093c24a783
commit
9cb4c59638
11 changed files with 121 additions and 94 deletions
|
|
@ -29,11 +29,6 @@
|
||||||
# containing gen_server.ex path is a CI build artifact, not a local dep path.
|
# containing gen_server.ex path is a CI build artifact, not a local dep path.
|
||||||
~r/gen_server\.ex/,
|
~r/gen_server\.ex/,
|
||||||
|
|
||||||
# :ranch is a transitive dep (via Bandit/Cowboy); :ranch.get_addr/1 exists
|
|
||||||
# but ranch isn't in the default PLT graph. Adding :ranch to plt_add_apps
|
|
||||||
# destabilises the PLT, so suppress the single call site instead.
|
|
||||||
{"lib/towerops_web/remote_ip.ex", :unknown_function},
|
|
||||||
|
|
||||||
# Vendored SnmpKit — out of scope for typing cleanup
|
# Vendored SnmpKit — out of scope for typing cleanup
|
||||||
~r/lib\/snmpkit/,
|
~r/lib\/snmpkit/,
|
||||||
~r/lib\/snmp_lib/,
|
~r/lib\/snmp_lib/,
|
||||||
|
|
|
||||||
|
|
@ -122,18 +122,22 @@ defmodule Towerops.Accounts do
|
||||||
{:error, %Ecto.Changeset{}}
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Ecto.Multi uses an opaque type that dialyzer can't see through when
|
# `Ecto.Multi.new/0` returns a `%Multi{names: MapSet.new()}` whose
|
||||||
# composing with local helpers; suppress the false-positive opaque warning.
|
# `names` field dialyzer's success-typing sees as `%MapSet{map: %{}}`.
|
||||||
|
# That concrete value violates MapSet's `@opaque t` when passed to
|
||||||
|
# subsequent `Ecto.Multi.*` calls — an upstream typing quirk in Ecto's
|
||||||
|
# `@spec new :: t` that loses information vs the actual struct literal.
|
||||||
|
# See elixir-ecto/ecto: `Ecto.Multi` uses `MapSet` internally but exposes
|
||||||
|
# it in the type, so `:no_opaque` is the correct narrow suppression.
|
||||||
@dialyzer {:no_opaque, [register_user: 1]}
|
@dialyzer {:no_opaque, [register_user: 1]}
|
||||||
@spec register_user(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
@spec register_user(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||||
def register_user(attrs) do
|
def register_user(attrs) do
|
||||||
user_changeset = User.registration_changeset(%User{}, attrs)
|
user_changeset = User.registration_changeset(%User{}, attrs)
|
||||||
|
|
||||||
multi = Ecto.Multi.new()
|
multi =
|
||||||
multi = Ecto.Multi.insert(multi, :user, user_changeset)
|
Ecto.Multi.new()
|
||||||
|
|> Ecto.Multi.insert(:user, user_changeset)
|
||||||
# Grant consents if provided
|
|> grant_consents_on_registration(attrs)
|
||||||
multi = grant_consents_on_registration(multi, attrs)
|
|
||||||
|
|
||||||
case Repo.transaction(multi) do
|
case Repo.transaction(multi) do
|
||||||
{:ok, %{user: user}} -> {:ok, user}
|
{:ok, %{user: user}} -> {:ok, user}
|
||||||
|
|
@ -154,16 +158,16 @@ defmodule Towerops.Accounts do
|
||||||
{:error, %Ecto.Changeset{}}
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@dialyzer {:nowarn_function, register_user_with_organization: 1}
|
# Same MapSet-opacity issue as register_user/1 — see its comment above.
|
||||||
|
@dialyzer {:no_opaque, [register_user_with_organization: 1]}
|
||||||
@spec register_user_with_organization(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
@spec register_user_with_organization(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||||
def register_user_with_organization(attrs) do
|
def register_user_with_organization(attrs) do
|
||||||
user_changeset = User.registration_changeset(%User{}, attrs)
|
user_changeset = User.registration_changeset(%User{}, attrs)
|
||||||
|
|
||||||
multi = Ecto.Multi.new()
|
|
||||||
multi = Ecto.Multi.insert(multi, :user, user_changeset)
|
|
||||||
|
|
||||||
multi =
|
multi =
|
||||||
Ecto.Multi.run(multi, :organization, fn _repo, %{user: user} ->
|
Ecto.Multi.new()
|
||||||
|
|> Ecto.Multi.insert(:user, user_changeset)
|
||||||
|
|> Ecto.Multi.run(:organization, fn _repo, %{user: user} ->
|
||||||
org_name = attrs["organization_name"] || "Personal"
|
org_name = attrs["organization_name"] || "Personal"
|
||||||
|
|
||||||
Towerops.Organizations.create_organization(
|
Towerops.Organizations.create_organization(
|
||||||
|
|
@ -171,9 +175,7 @@ defmodule Towerops.Accounts do
|
||||||
user.id
|
user.id
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|> grant_consents_on_registration(attrs)
|
||||||
# Grant consents if provided
|
|
||||||
multi = grant_consents_on_registration(multi, attrs)
|
|
||||||
|
|
||||||
case Repo.transaction(multi) do
|
case Repo.transaction(multi) do
|
||||||
{:ok, %{user: user}} -> {:ok, user}
|
{:ok, %{user: user}} -> {:ok, user}
|
||||||
|
|
@ -183,29 +185,26 @@ defmodule Towerops.Accounts do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec grant_consents_on_registration(Ecto.Multi.t(), map()) :: Ecto.Multi.t()
|
||||||
defp grant_consents_on_registration(multi, attrs) do
|
defp grant_consents_on_registration(multi, attrs) do
|
||||||
# Only grant consents if checkboxes were checked
|
# Only grant consents if checkboxes were checked
|
||||||
privacy_consent = attrs["privacy_policy_consent"] || attrs[:privacy_policy_consent]
|
privacy_consent = attrs["privacy_policy_consent"] || attrs[:privacy_policy_consent]
|
||||||
terms_consent = attrs["terms_of_service_consent"] || attrs[:terms_of_service_consent]
|
terms_consent = attrs["terms_of_service_consent"] || attrs[:terms_of_service_consent]
|
||||||
|
|
||||||
multi =
|
multi
|
||||||
if privacy_consent in ["true", true, "on"] do
|
|> maybe_grant_consent(privacy_consent, :privacy_policy_consent, "privacy_policy")
|
||||||
Ecto.Multi.run(multi, :privacy_policy_consent, fn _repo, %{user: user} ->
|
|> maybe_grant_consent(terms_consent, :terms_of_service_consent, "terms_of_service")
|
||||||
grant_consent(user.id, "privacy_policy")
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
multi
|
|
||||||
end
|
|
||||||
|
|
||||||
if terms_consent in ["true", true, "on"] do
|
|
||||||
Ecto.Multi.run(multi, :terms_of_service_consent, fn _repo, %{user: user} ->
|
|
||||||
grant_consent(user.id, "terms_of_service")
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
multi
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec maybe_grant_consent(Ecto.Multi.t(), term(), atom(), String.t()) :: Ecto.Multi.t()
|
||||||
|
defp maybe_grant_consent(multi, flag, step_name, consent_type) when flag in ["true", true, "on"] do
|
||||||
|
Ecto.Multi.run(multi, step_name, fn _repo, %{user: user} ->
|
||||||
|
grant_consent(user.id, consent_type)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_grant_consent(multi, _flag, _step_name, _consent_type), do: multi
|
||||||
|
|
||||||
## TOTP / Two-Factor Authentication
|
## TOTP / Two-Factor Authentication
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -1774,25 +1773,31 @@ defmodule Towerops.Accounts do
|
||||||
@doc """
|
@doc """
|
||||||
Confirms a user by the given token.
|
Confirms a user by the given token.
|
||||||
"""
|
"""
|
||||||
# Ecto.Multi uses an opaque type that dialyzer can't always see through.
|
@spec confirm_user(String.t()) :: {:ok, User.t()} | :error
|
||||||
@dialyzer {:no_opaque, [confirm_user: 1]}
|
|
||||||
def confirm_user(token) do
|
def confirm_user(token) do
|
||||||
with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
|
with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
|
||||||
%User{} = user <- Repo.one(query),
|
%User{} = user <- Repo.one(query),
|
||||||
{:ok, %{user: user}} <-
|
{:ok, %{user: confirmed_user}} <- confirm_user_transaction(user) do
|
||||||
Ecto.Multi.new()
|
{:ok, confirmed_user}
|
||||||
|> Ecto.Multi.update(:user, User.confirm_changeset(user))
|
|
||||||
|> Ecto.Multi.delete_all(
|
|
||||||
:tokens,
|
|
||||||
UserToken.by_user_and_contexts_query(user, ["confirm"])
|
|
||||||
)
|
|
||||||
|> Repo.transaction() do
|
|
||||||
{:ok, user}
|
|
||||||
else
|
else
|
||||||
_ -> :error
|
_ -> :error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Same MapSet-opacity issue as register_user/1 — see its comment above.
|
||||||
|
@dialyzer {:no_opaque, [confirm_user_transaction: 1]}
|
||||||
|
@spec confirm_user_transaction(User.t()) ::
|
||||||
|
{:ok, map()} | {:error, any(), any(), map()}
|
||||||
|
defp confirm_user_transaction(user) do
|
||||||
|
Ecto.Multi.new()
|
||||||
|
|> Ecto.Multi.update(:user, User.confirm_changeset(user))
|
||||||
|
|> Ecto.Multi.delete_all(
|
||||||
|
:tokens,
|
||||||
|
UserToken.by_user_and_contexts_query(user, ["confirm"])
|
||||||
|
)
|
||||||
|
|> Repo.transaction()
|
||||||
|
end
|
||||||
|
|
||||||
## Account Deletion (GDPR Right to Erasure)
|
## Account Deletion (GDPR Right to Erasure)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,19 @@ defmodule Towerops.Devices do
|
||||||
|
|
||||||
@device_preloads [:organization, site: :organization]
|
@device_preloads [:organization, site: :organization]
|
||||||
|
|
||||||
# Ecto.Multi uses opaque types that dialyzer can't see through when composed
|
# Ecto.Multi's struct is declared as an opaque type in Ecto, but
|
||||||
# with local helper functions for locking and quota checks.
|
# Ecto.Multi.new/0 and the Ecto.Multi.{insert,run}/* functions return
|
||||||
|
# the concrete struct shape (not the opaque Ecto.Multi.t()). Flow analysis
|
||||||
|
# sees a mismatch between the concrete struct we have in hand and the
|
||||||
|
# opaque type the subsequent Ecto.Multi.* calls declare in their
|
||||||
|
# signatures, raising call_without_opaque on every step of the pipeline.
|
||||||
|
# Passing the Multi through private helpers that take/return
|
||||||
|
# Ecto.Multi.t() compounds the issue. Adding @spec Ecto.Multi.t() on the
|
||||||
|
# helpers doesn't help — it just moves the warning to contract_with_opaque.
|
||||||
|
# Inlining the whole pipeline (even without helpers) still warns on
|
||||||
|
# `Ecto.Multi.new() |> Ecto.Multi.insert(...)` alone, confirming the
|
||||||
|
# problem is in Ecto's opaque-type declarations, not our helper shape.
|
||||||
|
# Suppression is the least-worst option until Ecto loosens the opaque type.
|
||||||
@dialyzer {:no_opaque,
|
@dialyzer {:no_opaque,
|
||||||
[
|
[
|
||||||
create_device: 1,
|
create_device: 1,
|
||||||
|
|
|
||||||
|
|
@ -85,11 +85,8 @@ defmodule Towerops.Organizations do
|
||||||
## Options
|
## Options
|
||||||
* `:bypass_limits` - When true, bypasses free organization limit (for superusers)
|
* `:bypass_limits` - When true, bypasses free organization limit (for superusers)
|
||||||
"""
|
"""
|
||||||
@dialyzer {:nowarn_function, create_organization: 2}
|
@spec create_organization(map(), String.t(), Keyword.t()) ::
|
||||||
@dialyzer {:nowarn_function, create_organization: 3}
|
{:ok, Organization.t()} | {:error, Ecto.Changeset.t()}
|
||||||
# Ecto.Multi/Changeset opaqueness trips dialyzer on this helper too.
|
|
||||||
@dialyzer {:no_opaque, [do_create_organization: 4, set_default_organization: 2]}
|
|
||||||
@spec create_organization(map(), String.t(), Keyword.t()) :: {:ok, Organization.t()} | {:error, Ecto.Changeset.t()}
|
|
||||||
def create_organization(attrs, user_id, opts \\ []) do
|
def create_organization(attrs, user_id, opts \\ []) do
|
||||||
bypass_limits = Keyword.get(opts, :bypass_limits, false)
|
bypass_limits = Keyword.get(opts, :bypass_limits, false)
|
||||||
subscription_plan = Map.get(attrs, :subscription_plan) || Map.get(attrs, "subscription_plan") || "free"
|
subscription_plan = Map.get(attrs, :subscription_plan) || Map.get(attrs, "subscription_plan") || "free"
|
||||||
|
|
@ -97,6 +94,14 @@ defmodule Towerops.Organizations do
|
||||||
do_create_organization(attrs, user_id, bypass_limits, subscription_plan)
|
do_create_organization(attrs, user_id, bypass_limits, subscription_plan)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# `Ecto.Multi.new/0` returns `%Multi{names: MapSet.new()}`. Dialyzer's
|
||||||
|
# success-typing sees the concrete `%MapSet{map: %{}}` value, which
|
||||||
|
# violates MapSet's `@opaque t` when passed to subsequent `Ecto.Multi.*`
|
||||||
|
# calls. This is an upstream quirk of Ecto exposing MapSet in the struct
|
||||||
|
# without an opacity-preserving `@spec`.
|
||||||
|
@dialyzer {:no_opaque, [do_create_organization: 4]}
|
||||||
|
@spec do_create_organization(map(), String.t(), boolean(), String.t()) ::
|
||||||
|
{:ok, Organization.t()} | {:error, Ecto.Changeset.t()}
|
||||||
defp do_create_organization(attrs, user_id, bypass_limits, subscription_plan) do
|
defp do_create_organization(attrs, user_id, bypass_limits, subscription_plan) do
|
||||||
multi =
|
multi =
|
||||||
Ecto.Multi.new()
|
Ecto.Multi.new()
|
||||||
|
|
@ -346,6 +351,10 @@ defmodule Towerops.Organizations do
|
||||||
Sets an organization as the user's default.
|
Sets an organization as the user's default.
|
||||||
Clears any existing default and sets the new one.
|
Clears any existing default and sets the new one.
|
||||||
"""
|
"""
|
||||||
|
# Same MapSet-opacity issue as do_create_organization/4 above.
|
||||||
|
@dialyzer {:no_opaque, [set_default_organization: 2]}
|
||||||
|
@spec set_default_organization(String.t(), String.t()) ::
|
||||||
|
{:ok, Organization.t()} | {:error, Ecto.Changeset.t() | :not_found}
|
||||||
def set_default_organization(user_id, organization_id) do
|
def set_default_organization(user_id, organization_id) do
|
||||||
# Verify user has access to this organization
|
# Verify user has access to this organization
|
||||||
if user_has_access?(user_id, organization_id) do
|
if user_has_access?(user_id, organization_id) do
|
||||||
|
|
@ -443,20 +452,20 @@ defmodule Towerops.Organizations do
|
||||||
@doc """
|
@doc """
|
||||||
Accepts an invitation and creates a membership.
|
Accepts an invitation and creates a membership.
|
||||||
"""
|
"""
|
||||||
@dialyzer {:nowarn_function, accept_invitation: 2}
|
# Same MapSet-opacity issue as do_create_organization/4 above.
|
||||||
|
@dialyzer {:no_opaque, [accept_invitation: 2]}
|
||||||
|
@spec accept_invitation(Invitation.t(), String.t()) ::
|
||||||
|
{:ok, Membership.t()} | {:error, Ecto.Changeset.t()}
|
||||||
def accept_invitation(invitation, user_id) do
|
def accept_invitation(invitation, user_id) do
|
||||||
multi = Ecto.Multi.new()
|
|
||||||
|
|
||||||
multi =
|
multi =
|
||||||
Ecto.Multi.update(multi, :invitation, fn _ ->
|
Ecto.Multi.new()
|
||||||
|
|> Ecto.Multi.update(:invitation, fn _ ->
|
||||||
Ecto.Changeset.change(invitation, %{
|
Ecto.Changeset.change(invitation, %{
|
||||||
accepted_at: Towerops.Time.now(),
|
accepted_at: Towerops.Time.now(),
|
||||||
accepted_by_id: user_id
|
accepted_by_id: user_id
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|> Ecto.Multi.insert(:membership, fn _ ->
|
||||||
multi =
|
|
||||||
Ecto.Multi.insert(multi, :membership, fn _ ->
|
|
||||||
Membership.changeset(%Membership{}, %{
|
Membership.changeset(%Membership{}, %{
|
||||||
organization_id: invitation.organization_id,
|
organization_id: invitation.organization_id,
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
|
|
|
||||||
|
|
@ -1154,7 +1154,8 @@ defmodule Towerops.Snmp.Discovery do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec sync_ip_addresses(Device.t(), [map()]) :: :ok
|
@type ip_sync_device :: Device.t() | %{required(:device_id) => String.t(), required(:interfaces) => list()}
|
||||||
|
@spec sync_ip_addresses(ip_sync_device(), [map()]) :: :ok
|
||||||
@doc """
|
@doc """
|
||||||
Sync IP addresses from discovery/polling results to database.
|
Sync IP addresses from discovery/polling results to database.
|
||||||
|
|
||||||
|
|
@ -1238,7 +1239,8 @@ defmodule Towerops.Snmp.Discovery do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec sync_processors(Device.t(), [map()]) :: :ok
|
@type snmp_device_ref :: Device.t() | %{required(:id) => String.t()}
|
||||||
|
@spec sync_processors(snmp_device_ref(), [map()]) :: :ok
|
||||||
@doc """
|
@doc """
|
||||||
Sync processors from discovery/polling results to database.
|
Sync processors from discovery/polling results to database.
|
||||||
"""
|
"""
|
||||||
|
|
@ -1305,7 +1307,7 @@ defmodule Towerops.Snmp.Discovery do
|
||||||
Logger.error("Failed to upsert processor: #{inspect(changeset)}")
|
Logger.error("Failed to upsert processor: #{inspect(changeset)}")
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec sync_storage(Device.t(), [map()]) :: :ok
|
@spec sync_storage(snmp_device_ref(), [map()]) :: :ok
|
||||||
@doc """
|
@doc """
|
||||||
Sync storage from discovery/polling results to database.
|
Sync storage from discovery/polling results to database.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,6 @@ defmodule Towerops.Snmp.MibTranslator do
|
||||||
iex> MibTranslator.translate("INVALID-MIB::badObject")
|
iex> MibTranslator.translate("INVALID-MIB::badObject")
|
||||||
{:error, :translation_failed}
|
{:error, :translation_failed}
|
||||||
"""
|
"""
|
||||||
# The Rust NIF stub returns a narrow type but at runtime the real NIF returns
|
|
||||||
# either a binary OID on success or {:error, reason} on failure. Dialyzer sees
|
|
||||||
# only the stub's narrow type, causing a false guard_fail on the is_binary clause.
|
|
||||||
@dialyzer {:nowarn_function, translate: 1}
|
|
||||||
@spec translate(String.t()) :: {:ok, String.t()} | {:error, :translation_failed}
|
@spec translate(String.t()) :: {:ok, String.t()} | {:error, :translation_failed}
|
||||||
def translate(mib_name) when is_binary(mib_name) do
|
def translate(mib_name) when is_binary(mib_name) do
|
||||||
cond do
|
cond do
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,6 @@ defmodule ToweropsNative do
|
||||||
|
|
||||||
@on_load :load_nif
|
@on_load :load_nif
|
||||||
|
|
||||||
# NIF stubs return fallback values when the NIF fails to load (defensive).
|
|
||||||
# Dialyzer only sees the stub bodies, so tell it to trust the @spec instead.
|
|
||||||
@dialyzer {:nowarn_function, [resolve_oid: 1, init_mib_library: 0, load_mib_directory: 1]}
|
|
||||||
|
|
||||||
def load_nif do
|
def load_nif do
|
||||||
nif_path = :filename.join(:code.priv_dir(:towerops), ~c"towerops_nif")
|
nif_path = :filename.join(:code.priv_dir(:towerops), ~c"towerops_nif")
|
||||||
|
|
||||||
|
|
@ -62,7 +58,7 @@ defmodule ToweropsNative do
|
||||||
{:error, "Failed to resolve MIB name"}
|
{:error, "Failed to resolve MIB name"}
|
||||||
"""
|
"""
|
||||||
@spec resolve_oid(String.t()) :: String.t() | {:error, String.t()}
|
@spec resolve_oid(String.t()) :: String.t() | {:error, String.t()}
|
||||||
def resolve_oid(_mib_name), do: {:error, "NIF not loaded"}
|
def resolve_oid(_mib_name), do: :erlang.nif_error(:nif_not_loaded)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Add a MIB directory to the search path.
|
Add a MIB directory to the search path.
|
||||||
|
|
@ -78,7 +74,7 @@ defmodule ToweropsNative do
|
||||||
:ok
|
:ok
|
||||||
"""
|
"""
|
||||||
@spec load_mib_directory(Path.t()) :: :ok
|
@spec load_mib_directory(Path.t()) :: :ok
|
||||||
def load_mib_directory(_path), do: :ok
|
def load_mib_directory(_path), do: :erlang.nif_error(:nif_not_loaded)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Initialize the net-snmp library and load all MIBs into memory.
|
Initialize the net-snmp library and load all MIBs into memory.
|
||||||
|
|
@ -101,5 +97,5 @@ defmodule ToweropsNative do
|
||||||
"already_initialized"
|
"already_initialized"
|
||||||
"""
|
"""
|
||||||
@spec init_mib_library() :: String.t()
|
@spec init_mib_library() :: String.t()
|
||||||
def init_mib_library, do: "initialized"
|
def init_mib_library, do: :erlang.nif_error(:nif_not_loaded)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -62,11 +62,6 @@ defmodule ToweropsWeb.AgentChannel do
|
||||||
@typep base64_string :: String.t()
|
@typep base64_string :: String.t()
|
||||||
@typep protobuf_binary :: binary()
|
@typep protobuf_binary :: binary()
|
||||||
|
|
||||||
# These helpers pass lightweight maps (not full Device structs) to
|
|
||||||
# Discovery.sync_* functions; the sync functions only need a few fields
|
|
||||||
# (`:id`, `:device_id`, `:interfaces`), so this is safe at runtime.
|
|
||||||
@dialyzer {:nowarn_function, [process_ip_addresses: 3, process_processors: 2, process_storage: 2]}
|
|
||||||
|
|
||||||
# Heartbeat timeout: close channel if no heartbeat in 5 minutes
|
# Heartbeat timeout: close channel if no heartbeat in 5 minutes
|
||||||
@heartbeat_timeout_seconds 300
|
@heartbeat_timeout_seconds 300
|
||||||
# Check heartbeat every 2 minutes
|
# Check heartbeat every 2 minutes
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,12 @@ defmodule ToweropsWeb.AgentSocket do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@spec connect(map(), Phoenix.Socket.t(), map()) :: {:ok, Phoenix.Socket.t()}
|
@spec connect(map(), Phoenix.Socket.t(), map()) :: {:ok, Phoenix.Socket.t()}
|
||||||
def connect(_params, socket, _connect_info) do
|
def connect(_params, socket, connect_info) do
|
||||||
# Allow all connections - authentication happens at channel join
|
# Allow all connections - authentication happens at channel join.
|
||||||
{:ok, socket}
|
# Stash peer_data so we can extract the client IP later without
|
||||||
|
# reaching into the transport process (which is adapter-specific).
|
||||||
|
peer_data = Map.get(connect_info, :peer_data)
|
||||||
|
{:ok, Phoenix.Socket.assign(socket, :peer_data, peer_data)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -21,18 +21,13 @@ defmodule ToweropsWeb.RemoteIp do
|
||||||
|
|
||||||
@spec from_socket(Phoenix.Socket.t()) :: String.t() | nil
|
@spec from_socket(Phoenix.Socket.t()) :: String.t() | nil
|
||||||
def from_socket(socket) do
|
def from_socket(socket) do
|
||||||
case socket.transport_pid do
|
# Use peer_data stashed into socket assigns at connect time via
|
||||||
nil ->
|
# `connect_info: [:peer_data]`. This is the documented Phoenix API
|
||||||
nil
|
# and is adapter-agnostic (works with both Bandit and Cowboy).
|
||||||
|
case socket.assigns[:peer_data] do
|
||||||
pid when is_pid(pid) ->
|
%{address: ip} when is_tuple(ip) -> format(ip)
|
||||||
case :ranch.get_addr(pid) do
|
_ -> nil
|
||||||
{ip, _port} when is_tuple(ip) -> format(ip)
|
|
||||||
_ -> nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
rescue
|
|
||||||
_ -> nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec format(tuple() | nil) :: String.t() | nil
|
@spec format(tuple() | nil) :: String.t() | nil
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,24 @@ defmodule ToweropsWeb.RemoteIpTest do
|
||||||
test "formats IPv6 addresses in uppercase hex" do
|
test "formats IPv6 addresses in uppercase hex" do
|
||||||
assert RemoteIp.format({8193, 3512, 0, 0, 0, 0, 0, 1}) == "2001:DB8:0:0:0:0:0:1"
|
assert RemoteIp.format({8193, 3512, 0, 0, 0, 0, 0, 1}) == "2001:DB8:0:0:0:0:0:1"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "from_socket/1" do
|
||||||
|
test "returns formatted IP from peer_data in assigns" do
|
||||||
|
socket = %Phoenix.Socket{
|
||||||
|
assigns: %{peer_data: %{address: {192, 168, 1, 42}, port: 12_345}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert RemoteIp.from_socket(socket) == "192.168.1.42"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns nil when peer_data is missing" do
|
||||||
|
socket = %Phoenix.Socket{assigns: %{}}
|
||||||
|
assert RemoteIp.from_socket(socket) == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns nil when peer_data has no address tuple" do
|
||||||
|
socket = %Phoenix.Socket{assigns: %{peer_data: %{}}}
|
||||||
|
assert RemoteIp.from_socket(socket) == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue