Removes unreachable catch-all clauses, drops unused `require Logger` lines, adds pin operators to bitstring size patterns, reorders function heads for correct dispatch, and replaces deprecated LoggerBackends calls.
365 lines
10 KiB
Elixir
365 lines
10 KiB
Elixir
defmodule Mix.Tasks.ImportProfiles do
|
|
@shortdoc "Import device profiles from LibreNMS YAML definitions"
|
|
|
|
@moduledoc """
|
|
Imports device profiles from YAML definition files.
|
|
|
|
## Usage
|
|
|
|
# Import all profiles from priv/profiles (default)
|
|
mix import_profiles
|
|
|
|
# Import from a custom path
|
|
mix import_profiles --source-path ~/librenms/resources/definitions
|
|
|
|
# Import specific profiles
|
|
mix import_profiles --profiles mikrotik,cisco,epmp
|
|
|
|
## Options
|
|
|
|
* `--source-path` - Path to definitions directory (default: priv/profiles)
|
|
* `--profiles` - Comma-separated list of profiles to import (default: all)
|
|
|
|
## Notes
|
|
|
|
* Reads YAML files from os_detection/ and os_discovery/ subdirectories
|
|
* Creates database profiles with MIB symbolic names
|
|
* Updates existing profiles if they already exist
|
|
* Run ./scripts/sync-librenms.sh to sync profiles from LibreNMS
|
|
"""
|
|
|
|
use Mix.Task
|
|
|
|
import Ecto.Query
|
|
|
|
alias Towerops.Profiles
|
|
alias Towerops.Profiles.DeviceOid
|
|
alias Towerops.Profiles.SensorOid
|
|
alias Towerops.Repo
|
|
|
|
@requirements ["app.start"]
|
|
|
|
def run(args) do
|
|
{opts, _} =
|
|
OptionParser.parse!(args,
|
|
strict: [
|
|
source_path: :string,
|
|
profiles: :string
|
|
]
|
|
)
|
|
|
|
source_path = opts[:source_path] || default_source_path()
|
|
profiles_filter = parse_profiles(opts[:profiles])
|
|
|
|
if !File.exists?(source_path) do
|
|
raise "Source directory not found: #{source_path}"
|
|
end
|
|
|
|
case profiles_filter do
|
|
:all ->
|
|
import_all_profiles(source_path)
|
|
|
|
profile_list ->
|
|
import_specific_profiles(source_path, profile_list)
|
|
end
|
|
end
|
|
|
|
defp default_source_path do
|
|
Path.join(:code.priv_dir(:towerops), "profiles")
|
|
end
|
|
|
|
defp parse_profiles(nil), do: :all
|
|
defp parse_profiles(""), do: :all
|
|
|
|
defp parse_profiles(profiles_string) do
|
|
profiles_string
|
|
|> String.split(",")
|
|
|> Enum.map(&String.trim/1)
|
|
|> Enum.reject(&(&1 == ""))
|
|
end
|
|
|
|
defp import_all_profiles(source_path) do
|
|
Mix.shell().info("Discovering YAML files in #{source_path}...")
|
|
|
|
detection_path = Path.join(source_path, "os_detection")
|
|
|
|
yaml_files =
|
|
detection_path
|
|
|> Path.join("*.yaml")
|
|
|> Path.wildcard()
|
|
|> Enum.sort()
|
|
|
|
Mix.shell().info("Found #{length(yaml_files)} YAML files")
|
|
|
|
profile_names = Enum.map(yaml_files, &Path.basename(&1, ".yaml"))
|
|
import_profiles_by_name(source_path, profile_names)
|
|
end
|
|
|
|
defp import_specific_profiles(source_path, profiles) do
|
|
Mix.shell().info("Importing #{length(profiles)} specified profiles")
|
|
import_profiles_by_name(source_path, profiles)
|
|
end
|
|
|
|
defp import_profiles_by_name(source_path, profile_names) do
|
|
results =
|
|
Enum.map(profile_names, fn profile_name ->
|
|
Mix.shell().info("Importing profile: #{profile_name}")
|
|
|
|
case import_profile(source_path, profile_name) do
|
|
{:ok, _profile} ->
|
|
Mix.shell().info("✓ #{profile_name}: created/updated successfully")
|
|
{:ok, profile_name}
|
|
|
|
{:error, reason} ->
|
|
Mix.shell().error("✗ #{profile_name}: #{inspect(reason)}")
|
|
{:error, profile_name, reason}
|
|
end
|
|
end)
|
|
|
|
successes = Enum.count(results, &match?({:ok, _}, &1))
|
|
failures = Enum.count(results, &match?({:error, _, _}, &1))
|
|
|
|
Mix.shell().info("Import complete: #{successes} succeeded, #{failures} failed")
|
|
end
|
|
|
|
defp import_profile(source_path, profile_name) do
|
|
detection_file = Path.join([source_path, "os_detection", "#{profile_name}.yaml"])
|
|
discovery_file = Path.join([source_path, "os_discovery", "#{profile_name}.yaml"])
|
|
|
|
with {:ok, detection_data} <- read_yaml_file(detection_file),
|
|
# Discovery file is optional - use empty map if not found
|
|
discovery_data = read_yaml_file_optional(discovery_file),
|
|
{:ok, _profile} <- create_or_update_profile(detection_data, discovery_data, profile_name),
|
|
# Reload profile to get correct binary ID
|
|
profile when not is_nil(profile) <- Profiles.get_profile(profile_name),
|
|
:ok <- import_device_oids(profile, discovery_data),
|
|
:ok <- import_sensor_oids(profile, discovery_data) do
|
|
{:ok, profile}
|
|
end
|
|
end
|
|
|
|
defp read_yaml_file_optional(file_path) do
|
|
case read_yaml_file(file_path) do
|
|
{:ok, data} -> data
|
|
{:error, _} -> %{}
|
|
end
|
|
end
|
|
|
|
defp read_yaml_file(file_path) do
|
|
with {:ok, yaml_content} <- File.read(file_path),
|
|
{:ok, yaml_data} <- YamlElixir.read_from_string(yaml_content) do
|
|
# YamlElixir can return either a map or a list with one map
|
|
yaml_data =
|
|
case yaml_data do
|
|
[data] -> data
|
|
data when is_map(data) -> data
|
|
_ -> yaml_data
|
|
end
|
|
|
|
{:ok, yaml_data}
|
|
else
|
|
{:error, :enoent} ->
|
|
{:error, "File not found: #{file_path}"}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp create_or_update_profile(detection_data, _discovery_data, profile_name) do
|
|
# Extract detection patterns from os_detection file
|
|
detection_pattern = extract_detection_pattern(detection_data)
|
|
detection_oid = extract_detection_oid(detection_data)
|
|
vendor = Map.get(detection_data, "text") || String.capitalize(profile_name)
|
|
|
|
attrs = %{
|
|
name: profile_name,
|
|
vendor: vendor,
|
|
detection_pattern: detection_pattern,
|
|
detection_oid: detection_oid,
|
|
priority: Map.get(detection_data, "priority", 100),
|
|
enabled: true
|
|
}
|
|
|
|
# Check if profile exists
|
|
case Profiles.get_profile(profile_name) do
|
|
nil ->
|
|
Profiles.create_profile(attrs)
|
|
|
|
existing_profile ->
|
|
Profiles.update_profile(existing_profile, attrs)
|
|
end
|
|
end
|
|
|
|
defp extract_detection_pattern(yaml_data) do
|
|
# Look for sysDescr patterns in discovery section
|
|
case get_in(yaml_data, ["discovery"]) do
|
|
discovery when is_list(discovery) ->
|
|
sys_descr_patterns = Enum.flat_map(discovery, &extract_sys_descr_patterns/1)
|
|
|
|
List.first(sys_descr_patterns)
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp extract_sys_descr_patterns(%{"sysDescr" => patterns}) when is_list(patterns), do: patterns
|
|
defp extract_sys_descr_patterns(_), do: []
|
|
|
|
defp extract_detection_oid(yaml_data) do
|
|
# Look for sysObjectID patterns in discovery section
|
|
case get_in(yaml_data, ["discovery"]) do
|
|
discovery when is_list(discovery) ->
|
|
oid_patterns = Enum.flat_map(discovery, &extract_sys_object_id_patterns/1)
|
|
|
|
List.first(oid_patterns)
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp extract_sys_object_id_patterns(%{"sysObjectID" => patterns}) when is_list(patterns), do: patterns
|
|
|
|
defp extract_sys_object_id_patterns(_), do: []
|
|
|
|
defp import_device_oids(profile, yaml_data) do
|
|
# Look for device info in various places
|
|
device_oids = extract_device_oids(yaml_data)
|
|
|
|
# Delete existing device OIDs
|
|
Repo.delete_all(
|
|
from o in DeviceOid,
|
|
where: o.profile_id == ^profile.id
|
|
)
|
|
|
|
# Insert new device OIDs
|
|
Enum.each(device_oids, fn {field, mib_name} ->
|
|
Profiles.add_device_oid(profile.id, field, mib_name)
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
defp extract_device_oids(yaml_data) do
|
|
# Extract from modules.os section in os_discovery YAML
|
|
# Get the modules.os section
|
|
os_module = get_in(yaml_data, ["modules", "os"]) || %{}
|
|
|
|
# Map of YAML field names to our field names
|
|
field_mappings = [
|
|
{"serial", "serial_number"},
|
|
{"version", "firmware_version"},
|
|
{"hardware", "hardware"},
|
|
{"lat", "latitude"},
|
|
{"long", "longitude"},
|
|
{"features", "features"},
|
|
{"sysName", "sys_name_oid"}
|
|
]
|
|
|
|
Enum.flat_map(field_mappings, fn {yaml_field, db_field} ->
|
|
case Map.get(os_module, yaml_field) do
|
|
nil -> []
|
|
# Handle list of OIDs (fallback order)
|
|
mib_names when is_list(mib_names) -> [{db_field, List.first(mib_names)}]
|
|
mib_name when is_binary(mib_name) -> [{db_field, mib_name}]
|
|
_ -> []
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp import_sensor_oids(profile, yaml_data) do
|
|
# Look for sensor definitions
|
|
sensors = extract_sensor_oids(yaml_data)
|
|
|
|
# Delete existing sensor OIDs
|
|
Repo.delete_all(
|
|
from o in SensorOid,
|
|
where: o.profile_id == ^profile.id
|
|
)
|
|
|
|
# Insert new sensor OIDs
|
|
Enum.each(sensors, fn sensor_attrs ->
|
|
Profiles.add_sensor_oid(profile.id, sensor_attrs)
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
defp extract_sensor_oids(yaml_data) do
|
|
# Extract from modules.sensors section in os_discovery YAML
|
|
sensors_module = get_in(yaml_data, ["modules", "sensors"]) || %{}
|
|
|
|
# Sensor types to extract
|
|
sensor_types = ["temperature", "voltage", "current", "power", "fanspeed"]
|
|
|
|
Enum.flat_map(sensor_types, fn sensor_type ->
|
|
case get_in(sensors_module, [sensor_type, "data"]) do
|
|
sensor_data when is_list(sensor_data) ->
|
|
sensor_data
|
|
|> Enum.map(&parse_sensor(&1, sensor_type))
|
|
|> Enum.reject(&is_nil/1)
|
|
|
|
_ ->
|
|
[]
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp parse_sensor(sensor_def, sensor_type) do
|
|
mib_name = Map.get(sensor_def, "oid") || Map.get(sensor_def, "value")
|
|
num_oid = Map.get(sensor_def, "num_oid")
|
|
|
|
cond do
|
|
table_sensor?(num_oid) -> nil
|
|
scalar_sensor?(mib_name) -> build_sensor_map(sensor_def, sensor_type, mib_name)
|
|
true -> nil
|
|
end
|
|
end
|
|
|
|
defp table_sensor?(num_oid) when is_binary(num_oid) do
|
|
String.contains?(num_oid, "{{ $index }}")
|
|
end
|
|
|
|
defp table_sensor?(_), do: false
|
|
|
|
defp scalar_sensor?(mib_name) when is_binary(mib_name) do
|
|
String.ends_with?(mib_name, ".0")
|
|
end
|
|
|
|
defp scalar_sensor?(_), do: false
|
|
|
|
defp build_sensor_map(sensor_def, sensor_type, mib_name) do
|
|
%{
|
|
sensor_type: sensor_type,
|
|
mib_name: mib_name,
|
|
sensor_descr: extract_sensor_description(sensor_def, sensor_type),
|
|
sensor_unit: Map.get(sensor_def, "unit"),
|
|
sensor_divisor: Map.get(sensor_def, "divisor", 1)
|
|
}
|
|
end
|
|
|
|
defp extract_sensor_description(sensor_def, default_type) do
|
|
case Map.get(sensor_def, "descr") do
|
|
nil ->
|
|
default_type
|
|
|
|
descr when is_binary(descr) ->
|
|
clean_description_template(descr, default_type)
|
|
|
|
_ ->
|
|
default_type
|
|
end
|
|
end
|
|
|
|
defp clean_description_template(descr, default_type) do
|
|
descr
|
|
|> String.replace(~r/\{\{ [^}]+ \}\}/, "")
|
|
|> String.trim()
|
|
|> case do
|
|
"" -> default_type
|
|
cleaned -> cleaned
|
|
end
|
|
end
|
|
end
|