devices assigned directly to an org (no site) were excluded from apply_agent_to_all_equipment due to inner join through site table. also fix fallback agent resolution to check device.organization when device.site is nil.
822 lines
27 KiB
Elixir
822 lines
27 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.Organizations.SubscriptionLimits
|
|
alias Towerops.Repo
|
|
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 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(: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 =
|
|
cond do
|
|
# Use preselected site from URL params if available
|
|
socket.assigns.preselected_site_id ->
|
|
Map.put(equipment_attrs, :site_id, socket.assigns.preselected_site_id)
|
|
|
|
# Auto-select if there's only one site
|
|
length(socket.assigns.available_sites) == 1 ->
|
|
site = List.first(socket.assigns.available_sites)
|
|
Map.put(equipment_attrs, :site_id, site.id)
|
|
|
|
# Otherwise, leave it empty for user to select
|
|
true ->
|
|
equipment_attrs
|
|
end
|
|
|
|
changeset = Devices.change_device(%DeviceSchema{}, equipment_attrs)
|
|
|
|
socket
|
|
|> assign(:page_title, "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 = Repo.preload(device, [:organization, site: [organization: :default_agent_token]])
|
|
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)
|
|
device_with_snmp = Repo.preload(device, :snmp_device, force: true)
|
|
is_mikrotik = mikrotik_device?(device_with_snmp)
|
|
|
|
# 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, "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
|
|
|
|
defp mikrotik_device?(device) do
|
|
# Check if device is MikroTik based on SNMP discovery data
|
|
# Must return boolean (not nil) to work with 'and' operator in templates
|
|
not is_nil(device.snmp_device) and
|
|
(String.contains?(device.snmp_device.manufacturer || "", "MikroTik") ||
|
|
String.contains?(device.snmp_device.sys_descr || "", "RouterOS"))
|
|
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
|
|
defp sanitize_device_params(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
|
|
|
|
@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)}"
|
|
)
|
|
|
|
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, assign(socket, :snmp_test_result, test_result)}
|
|
end
|
|
|
|
# Determine effective agent ID considering inheritance chain
|
|
defp determine_effective_agent_id(device_params, assigns) do
|
|
# Check device-level agent
|
|
form_agent_id = device_params["agent_token_id"]
|
|
|
|
if form_agent_id && form_agent_id != "" do
|
|
form_agent_id
|
|
else
|
|
# Check site-level 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
|
|
site_agent_id
|
|
else
|
|
# Check organization-level agent
|
|
org = assigns.organization
|
|
org_agent_id = org && org.default_agent_token_id
|
|
|
|
org_agent_id
|
|
end
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
defp get_value(map, key, default), do: map[key] || default
|
|
|
|
defp normalize_port(port) when is_binary(port), do: String.to_integer(port)
|
|
defp normalize_port(port) when is_integer(port), do: port
|
|
defp 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
|
|
end
|