374 lines
10 KiB
Elixir
374 lines
10 KiB
Elixir
defmodule Mix.Tasks.GenVendorModules do
|
|
@shortdoc "Generate vendor module skeletons from YAML profiles"
|
|
|
|
@moduledoc """
|
|
Generates vendor module skeletons from YAML profiles.
|
|
|
|
## Usage
|
|
|
|
mix gen_vendor_modules [--dry-run] [--group GROUP]
|
|
|
|
## Options
|
|
|
|
* `--dry-run` - Show what would be generated without creating files
|
|
* `--group GROUP` - Only generate for a specific group/vendor
|
|
|
|
## Examples
|
|
|
|
# Generate all missing vendor modules
|
|
mix gen_vendor_modules
|
|
|
|
# Preview what would be generated
|
|
mix gen_vendor_modules --dry-run
|
|
|
|
# Generate only for a specific vendor
|
|
mix gen_vendor_modules --group cisco
|
|
"""
|
|
|
|
use Mix.Task
|
|
|
|
@profiles_dir "priv/profiles/os_detection"
|
|
@vendors_dir "lib/towerops/snmp/profiles/vendors"
|
|
|
|
# Filename prefix to vendor mapping for profiles without group field
|
|
@filename_vendor_map %{
|
|
"cisco" => "cisco",
|
|
"juniper" => "juniper",
|
|
"junos" => "juniper",
|
|
"hp" => "hp",
|
|
"dell" => "dell",
|
|
"ibm" => "ibm",
|
|
"netgear" => "netgear",
|
|
"dlink" => "dlink",
|
|
"d-link" => "dlink",
|
|
"tp-link" => "tplink",
|
|
"tplink" => "tplink",
|
|
"netapp" => "netapp",
|
|
"synology" => "synology",
|
|
"qnap" => "qnap",
|
|
"vmware" => "vmware",
|
|
"palo" => "paloalto",
|
|
"panos" => "paloalto",
|
|
"fortinet" => "fortinet",
|
|
"forti" => "fortinet",
|
|
"checkpoint" => "checkpoint",
|
|
"sonic" => "sonicwall",
|
|
"sonicwall" => "sonicwall",
|
|
"opengear" => "opengear",
|
|
"raritan" => "raritan",
|
|
"eaton" => "eaton",
|
|
"liebert" => "liebert",
|
|
"cyberpower" => "cyberpower",
|
|
"tripp" => "tripplite",
|
|
"axis" => "axis",
|
|
"hikvision" => "hikvision",
|
|
"avaya" => "avaya",
|
|
"polycom" => "polycom",
|
|
"yealink" => "yealink",
|
|
"mitel" => "mitel",
|
|
"riverbed" => "riverbed",
|
|
"citrix" => "citrix",
|
|
"f5" => "f5",
|
|
"a10" => "a10",
|
|
"infoblox" => "infoblox",
|
|
"bluecoat" => "bluecoat",
|
|
"barracuda" => "barracuda",
|
|
"aruba" => "aruba",
|
|
"ruckus" => "ruckus"
|
|
}
|
|
|
|
@impl Mix.Task
|
|
def run(args) do
|
|
{opts, _, _} = OptionParser.parse(args, switches: [dry_run: :boolean, group: :string])
|
|
dry_run = Keyword.get(opts, :dry_run, false)
|
|
filter_group = Keyword.get(opts, :group)
|
|
|
|
# Get existing vendors and covered profiles
|
|
existing_vendors = get_existing_vendors()
|
|
covered_profiles = get_covered_profiles()
|
|
IO.puts("Found #{length(existing_vendors)} existing vendor modules")
|
|
IO.puts("Found #{length(covered_profiles)} profiles already covered")
|
|
|
|
# Parse all YAML profiles
|
|
profiles = parse_yaml_profiles()
|
|
IO.puts("Found #{length(profiles)} YAML profiles")
|
|
|
|
# Group profiles by vendor
|
|
grouped = group_profiles(profiles)
|
|
IO.puts("Grouped into #{map_size(grouped)} vendors")
|
|
|
|
# Filter if requested
|
|
grouped =
|
|
if filter_group do
|
|
Map.take(grouped, [filter_group])
|
|
else
|
|
grouped
|
|
end
|
|
|
|
# Filter out existing vendors and groups where all profiles are already covered
|
|
new_vendors =
|
|
grouped
|
|
|> Enum.reject(fn {vendor, vendor_profiles} ->
|
|
module_name = vendor_to_module_name(vendor)
|
|
module_exists = module_name in existing_vendors
|
|
|
|
# Check if all profiles in this group are already covered
|
|
profile_names = Enum.map(vendor_profiles, & &1.os)
|
|
all_covered = Enum.all?(profile_names, &(&1 in covered_profiles))
|
|
|
|
module_exists or all_covered
|
|
end)
|
|
|> Enum.map(fn {vendor, vendor_profiles} ->
|
|
# Filter out profiles that are already covered
|
|
uncovered = Enum.reject(vendor_profiles, &(&1.os in covered_profiles))
|
|
{vendor, uncovered}
|
|
end)
|
|
|> Enum.reject(fn {_vendor, profiles} -> Enum.empty?(profiles) end)
|
|
|> Map.new()
|
|
|
|
IO.puts("#{map_size(new_vendors)} new vendors to generate")
|
|
|
|
if map_size(new_vendors) == 0 do
|
|
IO.puts("\nNo new vendors to generate!")
|
|
:ok
|
|
else
|
|
Enum.each(new_vendors, fn {vendor, vendor_profiles} ->
|
|
generate_vendor_module(vendor, vendor_profiles, dry_run)
|
|
end)
|
|
|
|
print_instructions(new_vendors, dry_run)
|
|
end
|
|
end
|
|
|
|
defp print_instructions(new_vendors, dry_run) do
|
|
if !dry_run do
|
|
IO.puts("\n\nGenerated #{map_size(new_vendors)} vendor modules.")
|
|
IO.puts("Run `mix compile` to check for errors.")
|
|
IO.puts("Run `mix test` to verify.")
|
|
IO.puts("\nDon't forget to:")
|
|
IO.puts(" 1. Add aliases to registry.ex")
|
|
IO.puts(" 2. Add vendors to @vendors list in registry.ex")
|
|
IO.puts(" 3. Write tests for each vendor")
|
|
IO.puts(" 4. Add vendor-specific OIDs if available")
|
|
end
|
|
end
|
|
|
|
defp get_existing_vendors do
|
|
"#{@vendors_dir}/*.ex"
|
|
|> Path.wildcard()
|
|
|> Enum.map(&Path.basename(&1, ".ex"))
|
|
|> Enum.reject(&(&1 in ["registry", "vendor"]))
|
|
|> Enum.map(&Macro.camelize/1)
|
|
end
|
|
|
|
defp get_covered_profiles do
|
|
# Get all profile names already handled by existing vendor modules
|
|
# This requires the application to be started
|
|
_ = Application.ensure_all_started(:towerops)
|
|
|
|
Towerops.Snmp.Profiles.Vendors.Registry.list_profile_names()
|
|
rescue
|
|
_ -> []
|
|
end
|
|
|
|
defp parse_yaml_profiles do
|
|
"#{@profiles_dir}/*.yaml"
|
|
|> Path.wildcard()
|
|
|> Enum.map(&parse_yaml_file/1)
|
|
|> Enum.reject(&is_nil/1)
|
|
end
|
|
|
|
defp parse_yaml_file(path) do
|
|
case YamlElixir.read_from_file(path) do
|
|
{:ok, data} when is_map(data) ->
|
|
build_profile_map(data, path)
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp build_profile_map(data, path) do
|
|
os = Map.get(data, "os")
|
|
|
|
if os do
|
|
%{
|
|
os: os,
|
|
group: Map.get(data, "group"),
|
|
text: Map.get(data, "text"),
|
|
sys_object_ids: extract_sys_object_ids(data),
|
|
filename: Path.basename(path, ".yaml")
|
|
}
|
|
end
|
|
end
|
|
|
|
defp extract_sys_object_ids(data) do
|
|
data
|
|
|> Map.get("discovery", [])
|
|
|> List.wrap()
|
|
|> Enum.flat_map(&get_sys_object_id_list/1)
|
|
end
|
|
|
|
defp get_sys_object_id_list(%{"sysObjectID" => oids}) when is_list(oids), do: oids
|
|
defp get_sys_object_id_list(_), do: []
|
|
|
|
defp group_profiles(profiles) do
|
|
profiles
|
|
|> Enum.group_by(&determine_vendor/1)
|
|
|> Enum.reject(fn {vendor, _} -> vendor == nil end)
|
|
|> Map.new()
|
|
end
|
|
|
|
defp determine_vendor(%{group: group}) when is_binary(group) and group != "" do
|
|
String.downcase(group)
|
|
end
|
|
|
|
defp determine_vendor(%{filename: filename}) do
|
|
infer_vendor_from_filename(filename)
|
|
end
|
|
|
|
defp infer_vendor_from_filename(filename) do
|
|
Enum.find_value(@filename_vendor_map, fn {prefix, vendor} ->
|
|
if String.starts_with?(filename, prefix) or String.contains?(filename, prefix) do
|
|
vendor
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp vendor_to_module_name(vendor) do
|
|
name =
|
|
vendor
|
|
|> String.replace(~r/[^a-z0-9]+/i, "_")
|
|
|> String.trim("_")
|
|
|> Macro.camelize()
|
|
|
|
# Prefix with "N" if starts with a number (e.g., "4rf" -> "N4rf")
|
|
if String.match?(name, ~r/^[0-9]/) do
|
|
"N#{name}"
|
|
else
|
|
name
|
|
end
|
|
end
|
|
|
|
defp vendor_to_filename(vendor) do
|
|
name =
|
|
vendor
|
|
|> String.replace(~r/[^a-z0-9]+/i, "_")
|
|
|> String.downcase()
|
|
|> String.trim("_")
|
|
|
|
# Prefix with "n" if starts with a number
|
|
if String.match?(name, ~r/^[0-9]/) do
|
|
"n#{name}"
|
|
else
|
|
name
|
|
end
|
|
end
|
|
|
|
defp generate_vendor_module(vendor, profiles, dry_run) do
|
|
module_name = vendor_to_module_name(vendor)
|
|
filename = vendor_to_filename(vendor)
|
|
profile_names = profiles |> Enum.map(& &1.os) |> Enum.sort() |> Enum.uniq()
|
|
|
|
# Get enterprise OID from first sysObjectID if available
|
|
enterprise_oid = extract_enterprise_oid(profiles)
|
|
|
|
content = generate_module_content(module_name, profile_names, enterprise_oid, vendor)
|
|
|
|
if dry_run do
|
|
IO.puts("\n--- #{filename}.ex (#{module_name}) ---")
|
|
IO.puts(" Profiles: #{Enum.join(profile_names, ", ")}")
|
|
IO.puts(" Enterprise OID: #{enterprise_oid || "none"}")
|
|
else
|
|
path = "#{@vendors_dir}/#{filename}.ex"
|
|
File.write!(path, content)
|
|
IO.puts("Generated: #{path}")
|
|
end
|
|
end
|
|
|
|
defp extract_enterprise_oid(profiles) do
|
|
profiles
|
|
|> Enum.flat_map(& &1.sys_object_ids)
|
|
|> Enum.find_value(fn oid ->
|
|
# Parse enterprise OID from sysObjectID pattern like ".1.3.6.1.4.1.9."
|
|
case Regex.run(~r/\.?1\.3\.6\.1\.4\.1\.(\d+)/, oid) do
|
|
[_, enterprise] -> "1.3.6.1.4.1.#{enterprise}"
|
|
_ -> nil
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp generate_module_content(module_name, profile_names, enterprise_oid, vendor) do
|
|
profiles_list = inspect(profile_names, pretty: true, limit: :infinity)
|
|
|
|
enterprise_comment =
|
|
if enterprise_oid do
|
|
" # Enterprise OID: #{enterprise_oid}"
|
|
else
|
|
" # Enterprise OID: unknown - uses standard MIB OIDs"
|
|
end
|
|
|
|
"""
|
|
defmodule Towerops.Snmp.Profiles.Vendors.#{module_name} do
|
|
@moduledoc \"\"\"
|
|
SNMP profile for #{String.capitalize(vendor)} devices.
|
|
|
|
Auto-generated from YAML profiles. Add vendor-specific OIDs as needed.
|
|
\"\"\"
|
|
|
|
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
|
|
|
|
alias Towerops.Snmp.Client
|
|
alias Towerops.Snmp.Profiles.Vendors.Vendor
|
|
|
|
#{enterprise_comment}
|
|
|
|
@impl true
|
|
def profile_names, do: #{profiles_list}
|
|
|
|
@impl true
|
|
def detect_hardware(client_opts) do
|
|
case Client.get(client_opts, "1.3.6.1.2.1.1.1.0") do
|
|
{:ok, descr} when is_binary(descr) ->
|
|
parse_hardware_from_descr(descr)
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp parse_hardware_from_descr(descr) do
|
|
# TODO: Add vendor-specific hardware detection
|
|
# For now, return first line or word from sysDescr
|
|
descr
|
|
|> String.split(~r/[\\r\\n]/)
|
|
|> List.first()
|
|
|> case do
|
|
nil -> nil
|
|
line -> String.slice(line, 0, 64)
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def wireless_oid_defs do
|
|
[
|
|
# Standard HOST-RESOURCES-MIB CPU
|
|
%{
|
|
oid: "1.3.6.1.2.1.25.3.3.1.2.1",
|
|
sensor_descr: "CPU Load",
|
|
sensor_type: "load",
|
|
sensor_unit: "%",
|
|
sensor_divisor: 1
|
|
}
|
|
# TODO: Add vendor-specific OIDs
|
|
]
|
|
end
|
|
|
|
@impl true
|
|
def discover_wireless_sensors(client_opts) do
|
|
Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
|
|
end
|
|
end
|
|
"""
|
|
end
|
|
end
|