towerops/lib/mix/tasks/upload_librenms.ex

185 lines
5.3 KiB
Elixir

defmodule Mix.Tasks.UploadLibrenms do
@shortdoc "Upload MIBs and import profiles from LibreNMS installation"
@moduledoc """
Uploads MIB files and imports device profiles from a LibreNMS installation.
## Usage
# Upload from local LibreNMS installation
mix upload_librenms --librenms-path ~/dev/librenms
# Upload to specific server
mix upload_librenms --librenms-path ~/dev/librenms --url https://towerops.net
# Upload specific profiles only
mix upload_librenms --librenms-path ~/dev/librenms --profiles mikrotik,cisco
## Options
* `--librenms-path` - Path to LibreNMS installation directory (required)
* `--token` - Superuser API token for authentication (required for MIB upload)
* `--url` - Server URL (default: http://localhost:4000)
* `--profiles` - Comma-separated list of profiles to import (default: all)
## What it does
1. Uploads all MIB files from `{librenms-path}/mibs` to the server
2. Imports device profiles from `{librenms-path}/resources/definitions`
## Requirements
* LibreNMS installation with mibs/ and resources/definitions/ directories
* Server must be running and accessible
"""
use Mix.Task
require Logger
@requirements ["app.start"]
def run(args) do
{opts, _} =
OptionParser.parse!(args,
strict: [
librenms_path: :string,
url: :string,
token: :string,
profiles: :string
]
)
librenms_path = opts[:librenms_path] || raise "Missing --librenms-path argument"
base_url = opts[:url] || "http://localhost:4000"
token = opts[:token]
profiles = opts[:profiles]
if !File.exists?(librenms_path) do
raise "LibreNMS directory not found: #{librenms_path}"
end
# Step 1: Upload MIBs
mibs_path = Path.join(librenms_path, "mibs")
if File.exists?(mibs_path) do
if token do
Logger.info("Step 1/2: Uploading MIB files from #{mibs_path}...")
upload_mibs(mibs_path, base_url, token)
else
Logger.warning("Skipping MIB upload: --token is required for MIB uploads (superuser API token needed)")
end
else
Logger.warning("MIBs directory not found: #{mibs_path}, skipping MIB upload")
end
# Step 2: Import profiles
definitions_path = Path.join(librenms_path, "resources/definitions")
if File.exists?(definitions_path) do
Logger.info("Step 2/2: Importing device profiles from #{definitions_path}...")
import_profiles(definitions_path, profiles)
else
Logger.warning("Definitions directory not found: #{definitions_path}, skipping profile import")
end
Logger.info("Upload complete!")
end
defp upload_mibs(mibs_path, base_url, token) do
# Find all vendor directories in mibs/
vendor_dirs =
mibs_path
|> Path.join("*")
|> Path.wildcard()
|> Enum.filter(&File.dir?/1)
|> Enum.sort()
if Enum.empty?(vendor_dirs) do
Logger.warning("No vendor directories found in #{mibs_path}")
else
Logger.info("Found #{length(vendor_dirs)} vendor directories")
# Upload each vendor's MIBs
results =
Enum.flat_map(vendor_dirs, fn vendor_dir ->
vendor = Path.basename(vendor_dir)
Logger.info("Uploading MIBs for vendor: #{vendor}")
# Find all MIB files in this vendor directory
mib_files =
vendor_dir
|> Path.join("*")
|> Path.wildcard()
|> Enum.filter(&File.regular?/1)
|> Enum.sort()
Enum.map(mib_files, fn file_path ->
filename = Path.basename(file_path)
Logger.debug(" Uploading: #{filename}")
case upload_mib_file(file_path, vendor, base_url, token) do
:ok ->
{:ok, filename}
{:error, reason} ->
Logger.error(" ✗ Failed to upload #{filename}: #{inspect(reason)}")
{:error, filename, reason}
end
end)
end)
successes = Enum.count(results, &match?({:ok, _}, &1))
failures = Enum.count(results, &match?({:error, _, _}, &1))
Logger.info("MIB upload complete: #{successes} succeeded, #{failures} failed")
end
end
defp upload_mib_file(file_path, vendor, base_url, token) do
url = "#{base_url}/api/v1/mibs"
filename = Path.basename(file_path)
# Build base request
req = Req.new(url: url)
# Add authorization header if token is provided
req =
if token do
Req.Request.put_header(req, "authorization", "Bearer #{token}")
else
req
end
# Add multipart form data
form_data = [
file: {file_path, filename: filename},
vendor: vendor
]
case Req.post(req, form_multipart: form_data) do
{:ok, %{status: status}} when status in 200..299 ->
:ok
{:ok, %{status: status, body: body}} ->
{:error, "HTTP #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
defp import_profiles(definitions_path, profiles_filter) do
# Call the existing import_profiles task
args =
case profiles_filter do
nil ->
["--source-path", definitions_path]
profiles ->
["--source-path", definitions_path, "--profiles", profiles]
end
Mix.Task.run("import_profiles", args)
end
end