464 lines
15 KiB
Elixir
464 lines
15 KiB
Elixir
defmodule Mix.Tasks.ImportProfilesTest do
|
|
use Towerops.DataCase, async: true
|
|
|
|
import ExUnit.CaptureIO
|
|
|
|
alias Mix.Tasks.ImportProfiles
|
|
alias Towerops.Profiles
|
|
alias Towerops.Profiles.DeviceOid
|
|
alias Towerops.Profiles.Profile
|
|
alias Towerops.Profiles.SensorOid
|
|
alias Towerops.Repo
|
|
|
|
setup do
|
|
# Create temporary directory for test YAML files
|
|
temp_dir = Path.join(System.tmp_dir!(), "profiles_test_#{System.unique_integer([:positive])}")
|
|
detection_dir = Path.join([temp_dir, "os_detection"])
|
|
discovery_dir = Path.join([temp_dir, "os_discovery"])
|
|
File.mkdir_p!(detection_dir)
|
|
File.mkdir_p!(discovery_dir)
|
|
|
|
on_exit(fn ->
|
|
File.rm_rf!(temp_dir)
|
|
end)
|
|
|
|
{:ok, temp_dir: temp_dir, detection_dir: detection_dir, discovery_dir: discovery_dir}
|
|
end
|
|
|
|
describe "run/1 with valid profiles" do
|
|
test "imports a basic profile from YAML", %{
|
|
detection_dir: detection_dir,
|
|
discovery_dir: discovery_dir,
|
|
temp_dir: temp_dir
|
|
} do
|
|
# Create a simple detection file
|
|
detection_yaml = Path.join(detection_dir, "mikrotik.yaml")
|
|
|
|
File.write!(detection_yaml, """
|
|
os: mikrotik
|
|
text: MikroTik
|
|
priority: 100
|
|
discovery:
|
|
- sysDescr:
|
|
- RouterOS
|
|
- sysObjectID:
|
|
- .1.3.6.1.4.1.14988.
|
|
""")
|
|
|
|
# Create a simple discovery file
|
|
discovery_yaml = Path.join(discovery_dir, "mikrotik.yaml")
|
|
|
|
File.write!(discovery_yaml, """
|
|
modules:
|
|
os:
|
|
serial: SNMPv2-MIB::sysContact.0
|
|
version: SNMPv2-MIB::sysName.0
|
|
sensors:
|
|
temperature:
|
|
data:
|
|
- oid: MIKROTIK-MIB::mtxrHlTemperature.0
|
|
descr: System Temperature
|
|
unit: C
|
|
divisor: 10
|
|
""")
|
|
|
|
output =
|
|
capture_io(fn ->
|
|
ImportProfiles.run(["--source-path", temp_dir])
|
|
end)
|
|
|
|
assert output =~ "✓ mikrotik: created/updated successfully"
|
|
assert output =~ "Import complete: 1 succeeded, 0 failed"
|
|
|
|
# Verify profile was created
|
|
profile = Profiles.get_profile("mikrotik")
|
|
assert %Profile{} = profile
|
|
assert profile.vendor == "MikroTik"
|
|
assert profile.priority == 100
|
|
assert profile.enabled == true
|
|
|
|
# Verify device OIDs were created
|
|
device_oids = Repo.all(from o in DeviceOid, where: o.profile_id == ^profile.id)
|
|
assert length(device_oids) == 2
|
|
|
|
serial_oid = Enum.find(device_oids, &(&1.field == "serial_number"))
|
|
assert serial_oid.mib_name == "SNMPv2-MIB::sysContact.0"
|
|
|
|
version_oid = Enum.find(device_oids, &(&1.field == "firmware_version"))
|
|
assert version_oid.mib_name == "SNMPv2-MIB::sysName.0"
|
|
|
|
# Verify sensor OIDs were created
|
|
sensor_oids = Repo.all(from o in SensorOid, where: o.profile_id == ^profile.id)
|
|
assert length(sensor_oids) == 1
|
|
|
|
temp_sensor = List.first(sensor_oids)
|
|
assert temp_sensor.sensor_type == "temperature"
|
|
assert temp_sensor.mib_name == "MIKROTIK-MIB::mtxrHlTemperature.0"
|
|
assert temp_sensor.sensor_descr == "System Temperature"
|
|
assert temp_sensor.sensor_unit == "C"
|
|
assert temp_sensor.sensor_divisor == 10
|
|
end
|
|
|
|
test "updates existing profile", %{detection_dir: detection_dir, temp_dir: temp_dir} do
|
|
# Create initial profile
|
|
{:ok, profile} =
|
|
Profiles.create_profile(%{
|
|
name: "cisco",
|
|
vendor: "Cisco Old",
|
|
detection_pattern: "old pattern",
|
|
priority: 50,
|
|
enabled: true
|
|
})
|
|
|
|
# Create YAML file with updated info
|
|
detection_yaml = Path.join(detection_dir, "cisco.yaml")
|
|
|
|
File.write!(detection_yaml, """
|
|
os: cisco
|
|
text: Cisco Updated
|
|
priority: 200
|
|
discovery:
|
|
- sysDescr:
|
|
- Cisco IOS
|
|
""")
|
|
|
|
output =
|
|
capture_io(fn ->
|
|
ImportProfiles.run(["--source-path", temp_dir])
|
|
end)
|
|
|
|
assert output =~ "✓ cisco: created/updated successfully"
|
|
|
|
# Verify profile was updated
|
|
updated_profile = Profiles.get_profile("cisco")
|
|
assert updated_profile.id == profile.id
|
|
assert updated_profile.vendor == "Cisco Updated"
|
|
assert updated_profile.priority == 200
|
|
end
|
|
|
|
test "imports specific profiles only", %{detection_dir: detection_dir, temp_dir: temp_dir} do
|
|
# Create multiple detection files
|
|
File.write!(Path.join(detection_dir, "mikrotik.yaml"), """
|
|
os: mikrotik
|
|
text: MikroTik
|
|
priority: 100
|
|
""")
|
|
|
|
File.write!(Path.join(detection_dir, "cisco.yaml"), """
|
|
os: cisco
|
|
text: Cisco
|
|
priority: 100
|
|
""")
|
|
|
|
File.write!(Path.join(detection_dir, "ubiquiti.yaml"), """
|
|
os: ubiquiti
|
|
text: Ubiquiti
|
|
priority: 100
|
|
""")
|
|
|
|
output =
|
|
capture_io(fn ->
|
|
ImportProfiles.run(["--source-path", temp_dir, "--profiles", "mikrotik,cisco"])
|
|
end)
|
|
|
|
assert output =~ "Importing 2 specified profiles"
|
|
assert output =~ "✓ mikrotik"
|
|
assert output =~ "✓ cisco"
|
|
refute output =~ "ubiquiti"
|
|
|
|
# Verify only specified profiles were created
|
|
assert Profiles.get_profile("mikrotik")
|
|
assert Profiles.get_profile("cisco")
|
|
assert Profiles.get_profile("ubiquiti") == nil
|
|
end
|
|
|
|
test "handles profile without discovery file", %{detection_dir: detection_dir, temp_dir: temp_dir} do
|
|
detection_yaml = Path.join(detection_dir, "generic.yaml")
|
|
|
|
File.write!(detection_yaml, """
|
|
os: generic
|
|
text: Generic Device
|
|
priority: 1
|
|
""")
|
|
|
|
output =
|
|
capture_io(fn ->
|
|
ImportProfiles.run(["--source-path", temp_dir])
|
|
end)
|
|
|
|
assert output =~ "✓ generic: created/updated successfully"
|
|
|
|
profile = Profiles.get_profile("generic")
|
|
assert %Profile{} = profile
|
|
assert profile.vendor == "Generic Device"
|
|
|
|
# Should have no device OIDs or sensor OIDs
|
|
assert Repo.aggregate(from(o in DeviceOid, where: o.profile_id == ^profile.id), :count) == 0
|
|
assert Repo.aggregate(from(o in SensorOid, where: o.profile_id == ^profile.id), :count) == 0
|
|
end
|
|
|
|
test "extracts detection patterns from sysDescr", %{detection_dir: detection_dir, temp_dir: temp_dir} do
|
|
detection_yaml = Path.join(detection_dir, "test.yaml")
|
|
|
|
File.write!(detection_yaml, """
|
|
os: test
|
|
text: Test Device
|
|
discovery:
|
|
- sysDescr:
|
|
- "Test Pattern 1"
|
|
- "Test Pattern 2"
|
|
""")
|
|
|
|
capture_io(fn ->
|
|
ImportProfiles.run(["--source-path", temp_dir])
|
|
end)
|
|
|
|
profile = Profiles.get_profile("test")
|
|
assert profile.detection_pattern == "Test Pattern 1"
|
|
end
|
|
|
|
test "extracts detection OID from sysObjectID", %{detection_dir: detection_dir, temp_dir: temp_dir} do
|
|
detection_yaml = Path.join(detection_dir, "test.yaml")
|
|
|
|
File.write!(detection_yaml, """
|
|
os: test
|
|
text: Test Device
|
|
discovery:
|
|
- sysObjectID:
|
|
- ".1.3.6.1.4.1.9."
|
|
- ".1.3.6.1.4.1.10."
|
|
""")
|
|
|
|
capture_io(fn ->
|
|
ImportProfiles.run(["--source-path", temp_dir])
|
|
end)
|
|
|
|
profile = Profiles.get_profile("test")
|
|
assert profile.detection_oid == ".1.3.6.1.4.1.9."
|
|
end
|
|
|
|
test "imports all standard device OID fields", %{
|
|
detection_dir: detection_dir,
|
|
discovery_dir: discovery_dir,
|
|
temp_dir: temp_dir
|
|
} do
|
|
File.write!(Path.join(detection_dir, "test.yaml"), """
|
|
os: test
|
|
text: Test Device
|
|
""")
|
|
|
|
File.write!(Path.join(discovery_dir, "test.yaml"), """
|
|
modules:
|
|
os:
|
|
serial: DEVICE-MIB::serialNumber.0
|
|
version: DEVICE-MIB::firmwareVersion.0
|
|
hardware: DEVICE-MIB::hardwareModel.0
|
|
lat: DEVICE-MIB::latitude.0
|
|
long: DEVICE-MIB::longitude.0
|
|
features: DEVICE-MIB::features.0
|
|
sysName: SNMPv2-MIB::sysName.0
|
|
""")
|
|
|
|
capture_io(fn ->
|
|
ImportProfiles.run(["--source-path", temp_dir])
|
|
end)
|
|
|
|
profile = Profiles.get_profile("test")
|
|
device_oids = Repo.all(from o in DeviceOid, where: o.profile_id == ^profile.id)
|
|
|
|
assert length(device_oids) == 7
|
|
|
|
assert Enum.find(device_oids, &(&1.field == "serial_number")).mib_name == "DEVICE-MIB::serialNumber.0"
|
|
assert Enum.find(device_oids, &(&1.field == "firmware_version")).mib_name == "DEVICE-MIB::firmwareVersion.0"
|
|
assert Enum.find(device_oids, &(&1.field == "hardware")).mib_name == "DEVICE-MIB::hardwareModel.0"
|
|
assert Enum.find(device_oids, &(&1.field == "latitude")).mib_name == "DEVICE-MIB::latitude.0"
|
|
assert Enum.find(device_oids, &(&1.field == "longitude")).mib_name == "DEVICE-MIB::longitude.0"
|
|
assert Enum.find(device_oids, &(&1.field == "features")).mib_name == "DEVICE-MIB::features.0"
|
|
assert Enum.find(device_oids, &(&1.field == "sys_name_oid")).mib_name == "SNMPv2-MIB::sysName.0"
|
|
end
|
|
|
|
test "imports all sensor types", %{detection_dir: detection_dir, discovery_dir: discovery_dir, temp_dir: temp_dir} do
|
|
File.write!(Path.join(detection_dir, "test.yaml"), """
|
|
os: test
|
|
text: Test Device
|
|
""")
|
|
|
|
File.write!(Path.join(discovery_dir, "test.yaml"), """
|
|
modules:
|
|
sensors:
|
|
temperature:
|
|
data:
|
|
- oid: DEVICE-MIB::temp.0
|
|
descr: Temperature
|
|
unit: C
|
|
voltage:
|
|
data:
|
|
- oid: DEVICE-MIB::voltage.0
|
|
descr: Voltage
|
|
unit: V
|
|
current:
|
|
data:
|
|
- oid: DEVICE-MIB::current.0
|
|
descr: Current
|
|
unit: A
|
|
power:
|
|
data:
|
|
- oid: DEVICE-MIB::power.0
|
|
descr: Power
|
|
unit: W
|
|
fanspeed:
|
|
data:
|
|
- oid: DEVICE-MIB::fan.0
|
|
descr: Fan Speed
|
|
unit: RPM
|
|
""")
|
|
|
|
capture_io(fn ->
|
|
ImportProfiles.run(["--source-path", temp_dir])
|
|
end)
|
|
|
|
profile = Profiles.get_profile("test")
|
|
sensor_oids = Repo.all(from o in SensorOid, where: o.profile_id == ^profile.id)
|
|
|
|
assert length(sensor_oids) == 5
|
|
|
|
assert Enum.any?(sensor_oids, &(&1.sensor_type == "temperature"))
|
|
assert Enum.any?(sensor_oids, &(&1.sensor_type == "voltage"))
|
|
assert Enum.any?(sensor_oids, &(&1.sensor_type == "current"))
|
|
assert Enum.any?(sensor_oids, &(&1.sensor_type == "power"))
|
|
assert Enum.any?(sensor_oids, &(&1.sensor_type == "fanspeed"))
|
|
end
|
|
|
|
test "skips table sensors with {{ $index }}", %{
|
|
detection_dir: detection_dir,
|
|
discovery_dir: discovery_dir,
|
|
temp_dir: temp_dir
|
|
} do
|
|
File.write!(Path.join(detection_dir, "test.yaml"), """
|
|
os: test
|
|
text: Test Device
|
|
""")
|
|
|
|
File.write!(Path.join(discovery_dir, "test.yaml"), """
|
|
modules:
|
|
sensors:
|
|
temperature:
|
|
data:
|
|
- oid: DEVICE-MIB::temp.0
|
|
descr: Scalar Temperature
|
|
unit: C
|
|
- oid: DEVICE-MIB::tempTable
|
|
num_oid: .1.3.6.1.4.1.9999.1.2.{{ $index }}
|
|
descr: Table Temperature
|
|
unit: C
|
|
""")
|
|
|
|
capture_io(fn ->
|
|
ImportProfiles.run(["--source-path", temp_dir])
|
|
end)
|
|
|
|
profile = Profiles.get_profile("test")
|
|
sensor_oids = Repo.all(from o in SensorOid, where: o.profile_id == ^profile.id)
|
|
|
|
# Only scalar sensor should be imported
|
|
assert length(sensor_oids) == 1
|
|
assert List.first(sensor_oids).sensor_descr == "Scalar Temperature"
|
|
end
|
|
|
|
test "replaces existing OIDs when reimporting", %{
|
|
detection_dir: detection_dir,
|
|
discovery_dir: discovery_dir,
|
|
temp_dir: temp_dir
|
|
} do
|
|
# Create initial profile with OIDs
|
|
{:ok, profile} = Profiles.create_profile(%{name: "test", vendor: "Test", priority: 100, enabled: true})
|
|
{:ok, _} = Profiles.add_device_oid(profile.id, "serial_number", "OLD-MIB::serial.0")
|
|
|
|
{:ok, _} =
|
|
Profiles.add_sensor_oid(profile.id, %{
|
|
sensor_type: "temperature",
|
|
mib_name: "OLD-MIB::temp.0",
|
|
sensor_descr: "Old Temp",
|
|
sensor_unit: "C",
|
|
sensor_divisor: 1
|
|
})
|
|
|
|
# Create new YAML with different OIDs
|
|
File.write!(Path.join(detection_dir, "test.yaml"), """
|
|
os: test
|
|
text: Test Device
|
|
""")
|
|
|
|
File.write!(Path.join(discovery_dir, "test.yaml"), """
|
|
modules:
|
|
os:
|
|
serial: NEW-MIB::serialNumber.0
|
|
sensors:
|
|
voltage:
|
|
data:
|
|
- oid: NEW-MIB::voltage.0
|
|
descr: New Voltage
|
|
unit: V
|
|
""")
|
|
|
|
capture_io(fn ->
|
|
ImportProfiles.run(["--source-path", temp_dir])
|
|
end)
|
|
|
|
# Old OIDs should be replaced
|
|
device_oids = Repo.all(from o in DeviceOid, where: o.profile_id == ^profile.id)
|
|
assert length(device_oids) == 1
|
|
assert List.first(device_oids).mib_name == "NEW-MIB::serialNumber.0"
|
|
|
|
sensor_oids = Repo.all(from o in SensorOid, where: o.profile_id == ^profile.id)
|
|
assert length(sensor_oids) == 1
|
|
assert List.first(sensor_oids).mib_name == "NEW-MIB::voltage.0"
|
|
assert List.first(sensor_oids).sensor_type == "voltage"
|
|
end
|
|
end
|
|
|
|
describe "run/1 error handling" do
|
|
test "raises error when source directory doesn't exist" do
|
|
assert_raise RuntimeError, "Source directory not found: /nonexistent/path", fn ->
|
|
capture_io(fn ->
|
|
ImportProfiles.run(["--source-path", "/nonexistent/path"])
|
|
end)
|
|
end
|
|
end
|
|
|
|
test "reports failed imports", %{detection_dir: detection_dir, temp_dir: temp_dir} do
|
|
# Create invalid YAML
|
|
File.write!(Path.join(detection_dir, "invalid.yaml"), """
|
|
this is not: [valid yaml: that will parse
|
|
""")
|
|
|
|
output =
|
|
capture_io([capture_prompt: false], fn ->
|
|
ImportProfiles.run(["--source-path", temp_dir])
|
|
end)
|
|
|
|
assert output =~ ~r/✗ invalid|Import complete: 0 succeeded, 1 failed/
|
|
assert output =~ "Import complete: 0 succeeded, 1 failed"
|
|
end
|
|
|
|
test "uses default source path when not provided" do
|
|
# Use explicit --source-path with empty temp dir to avoid scanning 788+ real profile files
|
|
# Testing the no-args behavior would scan all real files which is slow
|
|
temp_dir = Path.join(System.tmp_dir!(), "import_profiles_test_#{:rand.uniform(999_999)}")
|
|
File.mkdir_p!(Path.join(temp_dir, "os_detection"))
|
|
|
|
try do
|
|
output =
|
|
capture_io(fn ->
|
|
# Use --source-path to avoid slow scan of real priv/profiles directory
|
|
ImportProfiles.run(["--source-path", temp_dir])
|
|
end)
|
|
|
|
# Should discover files and find none
|
|
assert output =~ ~r/Discovering YAML files|Found 0 YAML files/
|
|
after
|
|
File.rm_rf!(temp_dir)
|
|
end
|
|
end
|
|
end
|
|
end
|