towerops/lib/towerops_web/live/device_live/form.ex
Graham McIntire 3ca0834ef0 tests: raise coverage to 70% via helper promotion + new unit/property tests
Promoted pure presentation and utility helpers from `defp` to `def @doc false`
across ~20 LiveViews, Oban workers, and sync modules so they're reachable from
unit tests. Refactored several `cond` blocks into idiomatic function heads with
guards. Added ~250 new test cases in new files under test/towerops and
test/towerops_web, including DB-backed tests for CnMaestro.Sync and
AlertNotificationWorker, and removed dead LiveView tab components and
CapacityLive (no callers anywhere in lib/test).

Configured mix.exs test_coverage.ignore_modules to exclude vendored third-party
code (SnmpKit, protobuf-generated Towerops.Agent.*, Absinthe GraphQL types,
Phoenix HTML modules, Inspect protocol impls) from coverage calculations —
these are not our project code.

Coverage: 66.93% → 70.09%. Full suite: 10,127 tests, 0 failures.
2026-04-24 09:49:06 -05:00

861 lines
28 KiB
Elixir

defmodule ToweropsWeb.DeviceLive.Form do
@moduledoc false
use ToweropsWeb, :live_view
alias Towerops.Admin.AuditLogger
alias Towerops.Agents
alias Towerops.Devices
alias Towerops.Devices.Device, as: DeviceSchema
alias Towerops.OnCall
alias Towerops.Organizations.SubscriptionLimits
alias Towerops.Sites
alias Towerops.Workers.DiscoveryWorker
alias ToweropsWeb.Live.Helpers.AccessControl
@impl true
def mount(params, _session, socket) do
organization = socket.assigns.current_scope.organization
sites = Sites.list_organization_sites(organization.id)
agents = Agents.list_organization_agent_tokens(organization.id)
# Include cloud pollers in the available agents list
cloud_pollers = Agents.list_cloud_pollers()
all_agents = agents ++ cloud_pollers
# Load escalation policies for this organization
escalation_policies = OnCall.list_escalation_policies(organization.id)
# Load device quota
{current_devices, device_limit} =
SubscriptionLimits.device_quota(organization)
is_at_limit =
case device_limit do
:unlimited -> false
limit -> current_devices >= limit
end
# Sites are now optional - devices can belong directly to organization
{:ok,
socket
|> assign(:organization, organization)
|> assign(:available_sites, sites)
|> assign(:available_agents, all_agents)
|> assign(:escalation_policies, escalation_policies)
|> assign(:preselected_site_id, params["site_id"])
|> assign(:snmp_test_result, nil)
|> assign(:duplicate_device, nil)
|> assign(:non_routable_ip_error, false)
|> assign(:device_quota, %{
current: current_devices,
limit: device_limit,
at_limit: is_at_limit
})}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :new, params) do
equipment_attrs = %{
monitoring_enabled: true,
check_interval_seconds: 300,
snmp_enabled: true,
snmp_version: "2c",
snmp_port: 161
}
# Pre-fill from query params if available (e.g., from discovered devices)
equipment_attrs =
equipment_attrs
|> merge_param(:name, params["name"])
|> merge_param(:ip_address, params["ip_address"])
|> merge_bool_param(:snmp_enabled, params["snmp_enabled"])
equipment_attrs =
maybe_preselect_site(
equipment_attrs,
socket.assigns.preselected_site_id,
socket.assigns.available_sites
)
changeset = Devices.change_device(%DeviceSchema{}, equipment_attrs)
socket
|> assign(:page_title, t("New Device"))
|> assign(:device, %DeviceSchema{})
|> assign(:form, to_form(changeset))
|> assign(:monitoring_mode, "snmp_and_icmp")
end
defp apply_action(socket, :edit, %{"id" => id}) do
organization = socket.assigns.current_scope.organization
case AccessControl.verify_device_access(id, organization.id) do
{:ok, device} ->
# Preload necessary associations for agent resolution
device = Devices.get_device_with_agent_info(device)
apply_edit_action(socket, device)
{:error, :not_found} ->
socket
|> put_flash(:error, t_equipment("Device not found"))
|> push_navigate(to: ~p"/devices")
{:error, :unauthorized} ->
socket
|> put_flash(:error, t_equipment("You don't have access to this device"))
|> push_navigate(to: ~p"/devices")
end
end
defp apply_edit_action(socket, device) do
# Get current agent assignment if any
current_assignment = Agents.get_device_assignment(device.id)
# Get the effective agent and where it comes from
{effective_agent_id, agent_source} = Agents.get_effective_agent_token_with_source(device)
# For the form, we only want the device-specific assignment
agent_token_id =
if current_assignment do
current_assignment.agent_token_id
end
# Get agent name for display
effective_agent_name =
if effective_agent_id do
agent = Enum.find(socket.assigns.available_agents, &(&1.id == effective_agent_id))
agent && agent.name
end
# Add agent_token_id to the changeset data
device_with_agent = Map.put(device, :agent_token_id, agent_token_id)
# Create changeset and validate to populate form.params for conditional rendering
changeset =
device_with_agent
|> Devices.change_device(%{})
|> Map.put(:action, :validate)
# Get effective SNMP configuration and source
snmp_config = Devices.get_snmp_config(device)
# Get effective MikroTik configuration and source
mikrotik_config = Devices.get_mikrotik_config(device)
# Check if device is MikroTik (based on SNMP discovery)
is_mikrotik = Devices.mikrotik_device?(device)
# Determine monitoring mode based on current snmp_enabled value
monitoring_mode = if device.snmp_enabled, do: "snmp_and_icmp", else: "icmp_only"
socket
|> assign(:page_title, t("Edit Device"))
|> assign(:device, device_with_agent)
|> assign(:form, to_form(changeset))
|> assign(:agent_source, agent_source)
|> assign(:effective_agent_name, effective_agent_name)
|> assign(:snmp_config_source, snmp_config.source)
|> assign(:effective_snmp_version, snmp_config.version)
|> assign(:effective_snmp_community, snmp_config.community || "(not set)")
|> assign(:mikrotik_config_source, mikrotik_config.source)
|> assign(:effective_mikrotik_username, mikrotik_config.username || "(not set)")
|> assign(:is_mikrotik_device, is_mikrotik)
|> assign(:mikrotik_test_result, nil)
|> assign(:monitoring_mode, monitoring_mode)
end
@impl true
def handle_event("switch_monitoring_mode", %{"mode" => mode}, socket) do
# Update snmp_enabled based on the selected mode
snmp_enabled = mode == "snmp_and_icmp"
# Get current form params
current_params = socket.assigns.form.params
# Update params with new snmp_enabled value
updated_params = Map.put(current_params, "snmp_enabled", snmp_enabled)
# Create new changeset with updated params
changeset =
socket.assigns.device
|> Devices.change_device(updated_params)
|> Map.put(:action, :validate)
socket
|> assign(:monitoring_mode, mode)
|> assign(:form, to_form(changeset))
|> then(&{:noreply, &1})
end
@impl true
def handle_event("validate", %{"device" => device_params}, socket) do
require Logger
device_params = sanitize_device_params(device_params)
changeset =
socket.assigns.device
|> Devices.change_device(device_params)
|> Map.put(:action, :validate)
# Check for duplicate IP address within the same site
ip_address = device_params["ip_address"]
site_id = if device_params["site_id"] == "", do: nil, else: device_params["site_id"]
exclude_id = if socket.assigns.live_action == :edit, do: socket.assigns.device.id
duplicate_device =
if ip_address && String.trim(ip_address) != "" && site_id do
Devices.get_device_by_ip(String.trim(ip_address), site_id, exclude_id)
end
# Check for non-routable IP with cloud poller
non_routable_ip_error = check_non_routable_ip_cloud_error(device_params, socket.assigns)
{:noreply,
socket
|> assign(:form, to_form(changeset))
|> assign(:duplicate_device, duplicate_device)
|> assign(:non_routable_ip_error, non_routable_ip_error)}
end
@impl true
def handle_event("save", %{"device" => device_params}, socket) do
device_params = sanitize_device_params(device_params)
# Inject organization_id from current context
organization_id = socket.assigns.organization.id
device_params = Map.put(device_params, "organization_id", organization_id)
save_device(socket, socket.assigns.live_action, device_params)
end
@impl true
def handle_event("delete", _params, socket) do
device = socket.assigns.device
case Devices.delete_device(device) do
{:ok, _} ->
# Log device deletion
user = socket.assigns.current_scope.user
AuditLogger.log_device_deleted(nil, user.id, device.id, device.name)
{:noreply,
socket
|> put_flash(:info, t_equipment("Device deleted successfully"))
|> push_navigate(to: ~p"/devices")}
{:error, _} ->
{:noreply, put_flash(socket, :error, t_equipment("Unable to delete device"))}
end
end
@impl true
def handle_event("test_snmp", _params, socket) do
require Logger
changeset = socket.assigns.form.source
form_data = Ecto.Changeset.apply_changes(changeset)
# Validate IP address is present
if is_nil(form_data.ip_address) || to_string(form_data.ip_address) == "" do
{:noreply,
assign(socket, :snmp_test_result, %{
success: false,
message: "IP address is required to test SNMP connection"
})}
else
# Resolve credentials with inheritance from site/org
resolved_credentials = resolve_snmp_credentials_for_test(form_data, changeset, socket.assigns)
device_map = build_device_map_with_credentials(form_data, resolved_credentials)
effective_agent_id = determine_effective_agent_id(device_map, socket.assigns)
# Debug logging for SNMPv3 credential testing
Logger.debug(
"SNMPv3 test credentials: version=#{device_map["snmp_version"]}, " <>
"security_level=#{inspect(device_map["snmpv3_security_level"])}, " <>
"username=#{inspect(device_map["snmpv3_username"])}, " <>
"auth_protocol=#{inspect(device_map["snmpv3_auth_protocol"])}, " <>
"auth_password_present=#{!is_nil(device_map["snmpv3_auth_password"])}, " <>
"priv_protocol=#{inspect(device_map["snmpv3_priv_protocol"])}, " <>
"priv_password_present=#{!is_nil(device_map["snmpv3_priv_password"])}"
)
handle_snmp_test(socket, device_map, effective_agent_id)
end
end
@impl true
def handle_event("trigger_discovery", _params, socket) do
device = socket.assigns.device
if device.snmp_enabled do
_ = enqueue_discovery(device.id)
{:noreply,
socket
|> put_flash(:info, t_equipment("Discovery started..."))
|> push_navigate(to: ~p"/devices/#{device.id}")}
else
{:noreply, put_flash(socket, :error, t_equipment("SNMP is not enabled for this device"))}
end
end
# Resolve all SNMP credentials with inheritance from site/org
defp resolve_snmp_credentials_for_test(form_data, changeset, assigns) do
snmp_version = form_data.snmp_version || "2c"
if snmp_version == "3" do
resolve_snmpv3_credentials_for_test(form_data, changeset, assigns)
else
resolve_snmp_community_for_test(form_data, changeset, assigns)
end
end
# Resolve SNMPv3 credentials with site/org inheritance
defp resolve_snmpv3_credentials_for_test(form_data, changeset, assigns) do
cond do
# Use device-level credentials if available
form_data.snmpv3_username && form_data.snmpv3_username != "" ->
build_device_snmpv3_creds(form_data, changeset.changes)
# Inherit from site if available
site_has_snmpv3_creds?(form_data, assigns) ->
site = find_site(form_data.site_id, assigns.available_sites)
build_snmpv3_creds(site)
# Fall back to organization credentials
assigns.organization.snmpv3_username ->
build_snmpv3_creds(assigns.organization)
# No credentials available
true ->
%{version: "3"}
end
end
defp site_has_snmpv3_creds?(form_data, assigns) do
site = find_site(form_data.site_id, assigns.available_sites)
site && site.snmpv3_username
end
defp find_site(nil, _sites), do: nil
defp find_site(site_id, sites), do: Enum.find(sites, &(&1.id == site_id))
defp build_device_snmpv3_creds(form_data, changes) do
%{
version: "3",
security_level: form_data.snmpv3_security_level,
username: form_data.snmpv3_username,
auth_protocol: form_data.snmpv3_auth_protocol,
auth_password: Map.get(changes, :snmpv3_auth_password) || form_data.snmpv3_auth_password,
priv_protocol: form_data.snmpv3_priv_protocol,
priv_password: Map.get(changes, :snmpv3_priv_password) || form_data.snmpv3_priv_password
}
end
defp build_snmpv3_creds(source) do
%{
version: "3",
security_level: source.snmpv3_security_level,
username: source.snmpv3_username,
auth_protocol: source.snmpv3_auth_protocol,
auth_password: source.snmpv3_auth_password,
priv_protocol: source.snmpv3_priv_protocol,
priv_password: source.snmpv3_priv_password
}
end
# Resolve SNMP community with site/org inheritance
defp resolve_snmp_community_for_test(form_data, changeset, assigns) do
version = form_data.snmp_version || "2c"
community = get_inherited_community(form_data, changeset, assigns)
if community do
%{version: version, community: community}
else
%{version: version}
end
end
defp get_inherited_community(form_data, changeset, assigns) do
changes = changeset.changes
# Check if community is explicitly set in form
if Map.has_key?(changes, :snmp_community) && form_data.snmp_community do
form_data.snmp_community
else
# Try site-level, then org-level community
site = form_data.site_id && Enum.find(assigns.available_sites, &(&1.id == form_data.site_id))
if site && site.snmp_community do
site.snmp_community
else
assigns.organization.snmp_community
end
end
end
defp build_device_map_with_credentials(form_data, credentials) do
base_map = %{
"agent_token_id" => Map.get(form_data, :agent_token_id),
"site_id" => form_data.site_id,
"ip_address" => form_data.ip_address,
"snmp_port" => form_data.snmp_port || 161,
"snmp_version" => credentials.version
}
if credentials.version == "3" do
Map.merge(base_map, %{
"snmpv3_security_level" => credentials[:security_level],
"snmpv3_username" => credentials[:username],
"snmpv3_auth_protocol" => credentials[:auth_protocol],
"snmpv3_auth_password" => credentials[:auth_password],
"snmpv3_priv_protocol" => credentials[:priv_protocol],
"snmpv3_priv_password" => credentials[:priv_password]
})
else
Map.put(base_map, "snmp_community", credentials[:community])
end
end
defp handle_snmp_test(socket, _device_map, nil) do
{:noreply,
assign(socket, :snmp_test_result, %{
success: false,
message: "No agent available for testing. Please assign an agent or configure organization defaults."
})}
end
defp handle_snmp_test(socket, device_map, effective_agent_id) do
test_id = Ecto.UUID.generate()
case send_credential_test_to_agent(effective_agent_id, device_map, test_id) do
:ok ->
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "credential_test:#{test_id}")
{:noreply,
socket
|> assign(:snmp_test_result, %{testing: true})
|> assign(:test_id, test_id)}
{:error, reason} ->
{:noreply,
assign(socket, :snmp_test_result, %{
success: false,
message: "Failed to send test: #{reason}"
})}
end
end
defp save_device(socket, :new, device_params) do
# Extract agent_token_id before creating device
agent_token_id = Map.get(device_params, "agent_token_id")
device_params = Map.delete(device_params, "agent_token_id")
# Add snmp_enabled based on monitoring mode
snmp_enabled = socket.assigns.monitoring_mode == "snmp_and_icmp"
device_params = Map.put(device_params, "snmp_enabled", snmp_enabled)
# Check if user is superuser and bypass limits if so
is_superuser = socket.assigns.current_scope.user.is_superuser
opts = if is_superuser, do: [bypass_limits: true], else: []
case Devices.create_device(device_params, opts) do
{:ok, device} ->
# Log device creation
user = socket.assigns.current_scope.user
AuditLogger.log_device_created(nil, user.id, device.id, device.name)
# Handle agent assignment after device creation
_ = handle_agent_assignment(device.id, agent_token_id)
flash_message = handle_device_creation(device)
# Reload quota after device creation
organization = socket.assigns.organization
{current, limit} = SubscriptionLimits.device_quota(organization)
is_at_limit =
case limit do
:unlimited -> false
l -> current >= l
end
# Reset form for next device while staying on the add page
fresh_attrs = %{
monitoring_enabled: true,
check_interval_seconds: 300,
snmp_enabled: snmp_enabled,
snmp_version: "2c",
snmp_port: 161,
# Keep the same site selected
site_id: device.site_id
}
fresh_changeset = Devices.change_device(%DeviceSchema{}, fresh_attrs)
{:noreply,
socket
|> put_flash(:info, flash_message)
|> assign(:form, to_form(fresh_changeset))
|> assign(:device_quota, %{current: current, limit: limit, at_limit: is_at_limit})
|> assign(:snmp_test_result, nil)
|> push_event("scroll_to_top", %{})}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}
end
end
defp save_device(socket, :edit, device_params) do
old_device = socket.assigns.device
# Extract agent_token_id before updating device
agent_token_id = Map.get(device_params, "agent_token_id")
device_params = Map.delete(device_params, "agent_token_id")
# Add snmp_enabled based on monitoring mode
snmp_enabled = socket.assigns.monitoring_mode == "snmp_and_icmp"
device_params = Map.put(device_params, "snmp_enabled", snmp_enabled)
case Devices.update_device(old_device, device_params) do
{:ok, device} ->
# Log device update
user = socket.assigns.current_scope.user
fields_changed = Map.keys(device_params)
AuditLogger.log_device_updated(nil, user.id, device.id, fields_changed)
# device update
_ = handle_agent_assignment(device.id, agent_token_id)
flash_message = handle_device_update(old_device, device)
{:noreply,
socket
|> put_flash(:info, flash_message)
|> push_navigate(to: ~p"/devices/#{device.id}")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}
end
end
defp handle_device_creation(device) do
device_label = device.name || device.ip_address
base_message =
if device.snmp_enabled do
t_equipment("Device created. SNMP discovery started. ")
else
t_equipment("Device created. ")
end
_ =
if device.snmp_enabled do
enqueue_discovery(device.id)
end
%{
text: base_message,
link_text: device_label,
link_url: "/devices/#{device.id}"
}
end
defp handle_device_update(old_device, device) do
if should_trigger_snmp_discovery?(old_device, device) do
_ = enqueue_discovery(device.id)
t_equipment("Device updated successfully. SNMP discovery started in background.")
else
t_equipment("Device updated successfully")
end
end
# Enqueue discovery job - safe to call in test environment
defp enqueue_discovery(device_id) do
if Application.get_env(:towerops, :env) == :test do
# In test, skip discovery (tests can call Snmp.discover_device directly if needed)
:ok
else
# In dev/prod, enqueue to Oban
DiscoveryWorker.enqueue(device_id)
end
end
defp should_trigger_snmp_discovery?(old_device, device) do
device.snmp_enabled and
(!old_device.snmp_enabled or
snmp_config_changed?(old_device, device))
end
defp snmp_config_changed?(old_device, device) do
device.snmp_community != old_device.snmp_community or
device.snmp_version != old_device.snmp_version or
device.snmp_port != old_device.snmp_port or
device.ip_address != old_device.ip_address
end
defp handle_agent_assignment(device_id, agent_token_id) when is_binary(agent_token_id) do
# Only assign if agent_token_id is not empty
if agent_token_id == "" do
Agents.update_device_assignment(device_id, nil)
else
Agents.update_device_assignment(device_id, agent_token_id)
end
end
defp handle_agent_assignment(device_id, nil) do
# No agent selected, remove any assignment
Agents.update_device_assignment(device_id, nil)
end
defp handle_agent_assignment(_device_id, _), do: :ok
# Sanitize device params - trim whitespace from IP address and handle device role source
@doc false
def sanitize_device_params(params) do
params
|> sanitize_ip_address()
|> sanitize_device_role()
end
@doc false
def sanitize_ip_address(params) do
case Map.get(params, "ip_address") do
ip when is_binary(ip) ->
Map.put(params, "ip_address", String.trim(ip))
_ ->
params
end
end
@doc false
def sanitize_device_role(params) do
case Map.get(params, "device_role") do
role when is_binary(role) and role != "" ->
params
_ ->
params
end
end
@impl true
def handle_info({:credential_test_result, result}, socket) do
require Logger
Logger.info(
"LiveView received credential test result: test_id=#{result.test_id} success=#{result.success} system_description=#{inspect(result.system_description)} error=#{inspect(result.error_message)}"
)
# Unsubscribe from this specific test topic since we got the result
if test_id = socket.assigns[:test_id] do
Phoenix.PubSub.unsubscribe(Towerops.PubSub, "credential_test:#{test_id}")
end
test_result =
if result.success do
%{
success: true,
message: "Connection successful! System: #{result.system_description}"
}
else
%{
success: false,
message: result.error_message
}
end
Logger.info("Setting snmp_test_result assign: #{inspect(test_result)}")
{:noreply, socket |> assign(:snmp_test_result, test_result) |> assign(:test_id, nil)}
end
# Catch-all for unexpected messages
@impl true
def handle_info(_msg, socket), do: {:noreply, socket}
# Determine effective agent ID considering inheritance chain
defp determine_effective_agent_id(device_params, assigns) do
with nil <- get_device_agent(device_params),
nil <- get_site_agent(device_params, assigns),
nil <- get_org_agent(assigns) do
nil
else
agent_id when is_binary(agent_id) and agent_id != "" -> agent_id
_ -> nil
end
end
@doc false
def get_device_agent(%{"agent_token_id" => id}) when is_binary(id) and id != "", do: id
def get_device_agent(_), do: nil
@doc false
def get_site_agent(%{"site_id" => site_id}, %{available_sites: sites}) when is_binary(site_id) do
case Enum.find(sites, &(&1.id == site_id)) do
%{agent_token_id: id} when not is_nil(id) -> id
_ -> nil
end
end
def get_site_agent(_, _), do: nil
@doc false
def get_org_agent(%{organization: %{default_agent_token_id: id}}) when not is_nil(id), do: id
def get_org_agent(_), do: nil
defp send_credential_test_to_agent(agent_token_id, device_map, test_id) do
require Logger
snmp_config = build_snmp_test_config(device_map)
Logger.info("Broadcasting credential test request",
agent_token_id: agent_token_id,
test_id: test_id,
topic: "agent:#{agent_token_id}:credential_test",
ip: snmp_config[:ip],
version: snmp_config[:version]
)
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"agent:#{agent_token_id}:credential_test",
{:credential_test_requested, test_id, snmp_config}
)
end
defp build_snmp_test_config(device_map) do
%{
ip: get_value(device_map, "ip_address", ""),
port: normalize_port(device_map["snmp_port"]),
version: get_value(device_map, "snmp_version", "2c"),
community: get_value(device_map, "snmp_community", ""),
v3_security_level: get_value(device_map, "snmpv3_security_level", ""),
v3_username: get_value(device_map, "snmpv3_username", ""),
v3_auth_protocol: get_value(device_map, "snmpv3_auth_protocol", ""),
v3_auth_password: get_value(device_map, "snmpv3_auth_password", ""),
v3_priv_protocol: get_value(device_map, "snmpv3_priv_protocol", ""),
v3_priv_password: get_value(device_map, "snmpv3_priv_password", "")
}
end
@doc false
def get_value(map, key, default), do: map[key] || default
@doc false
def normalize_port(port) when is_binary(port), do: String.to_integer(port)
def normalize_port(port) when is_integer(port), do: port
def normalize_port(_), do: 161
# Check if a non-routable IP is being used with cloud poller
# Skip this check in dev mode or for specific users
defp check_non_routable_ip_cloud_error(device_params, assigns) do
# Skip check in dev mode or for specific users
if skip_non_routable_check?(assigns) do
false
else
ip_address = device_params["ip_address"]
# Only check if we have a valid IP address
with true <- is_binary(ip_address) and String.trim(ip_address) != "",
trimmed_ip = String.trim(ip_address),
true <- non_routable_ip?(trimmed_ip),
true <- using_cloud_poller?(device_params, assigns) do
true
else
_ -> false
end
end
end
defp skip_non_routable_check?(assigns) do
# Skip in dev mode or for specific users
Application.get_env(:towerops, :env) == :dev ||
(assigns[:current_scope] && assigns.current_scope.user &&
assigns.current_scope.user.email == "graham@mcintire.me")
end
# Determine if the device would use cloud poller (no agent at any level)
defp using_cloud_poller?(device_params, assigns) do
# Check if agent is directly assigned in form
form_agent_id = device_params["agent_token_id"]
if form_agent_id && form_agent_id != "" do
false
else
# Check if site has a default agent
site_id = device_params["site_id"]
site = site_id && Enum.find(assigns.available_sites, &(&1.id == site_id))
site_agent_id = site && site.agent_token_id
if site_agent_id do
false
else
# Check if organization has a default agent
org = assigns.organization
org_agent_id = org && org.default_agent_token_id
# Cloud poller if no agent at any level
is_nil(org_agent_id)
end
end
end
# Merge a query param into attrs if present and non-empty
defp merge_param(attrs, _key, nil), do: attrs
defp merge_param(attrs, _key, ""), do: attrs
defp merge_param(attrs, key, value) when is_binary(value) do
Map.put(attrs, key, value)
end
# Merge a boolean query param into attrs if present
defp merge_bool_param(attrs, _key, nil), do: attrs
defp merge_bool_param(attrs, key, "true"), do: Map.put(attrs, key, true)
defp merge_bool_param(attrs, key, "false"), do: Map.put(attrs, key, false)
defp merge_bool_param(attrs, _key, _), do: attrs
# Check if IP is non-routable (RFC1918 private or RFC6598 CGNAT)
defp non_routable_ip?(ip_string) do
case ip_string |> String.to_charlist() |> :inet.parse_address() do
{:ok, {a, b, _c, _d}} -> non_routable_ipv4_range?(a, b)
# IPv6 or invalid - don't flag as non-routable
_ -> false
end
end
# 10.0.0.0/8 (RFC1918)
defp non_routable_ipv4_range?(10, _b), do: true
# 172.16.0.0/12 (RFC1918)
defp non_routable_ipv4_range?(172, b) when b >= 16 and b <= 31, do: true
# 192.168.0.0/16 (RFC1918)
defp non_routable_ipv4_range?(192, 168), do: true
# 100.64.0.0/10 (RFC6598 CGNAT)
defp non_routable_ipv4_range?(100, b) when b >= 64 and b <= 127, do: true
defp non_routable_ipv4_range?(_a, _b), do: false
# Auto-select site if preselected or if there's only one option
defp maybe_preselect_site(attrs, preselected_id, _sites) when not is_nil(preselected_id),
do: Map.put(attrs, :site_id, preselected_id)
defp maybe_preselect_site(attrs, nil, [site]), do: Map.put(attrs, :site_id, site.id)
defp maybe_preselect_site(attrs, nil, _sites), do: attrs
@impl true
def terminate(_reason, socket) do
# Unsubscribe from credential test topic if we're still subscribed
if test_id = socket.assigns[:test_id] do
Phoenix.PubSub.unsubscribe(Towerops.PubSub, "credential_test:#{test_id}")
end
:ok
end
end