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, &upload_vendor_mibs(&1, base_url, token)) report_upload_results(results) end end defp upload_vendor_mibs(vendor_dir, base_url, token) do vendor = Path.basename(vendor_dir) Logger.info("Uploading MIBs for vendor: #{vendor}") vendor_dir |> find_mib_files() |> Enum.map(&upload_single_mib(&1, vendor, base_url, token)) |> Enum.reject(&is_nil/1) end defp find_mib_files(vendor_dir) do vendor_dir |> Path.join("*") |> Path.wildcard() |> Enum.filter(&File.regular?/1) |> Enum.sort() end defp upload_single_mib(file_path, vendor, base_url, token) do if File.regular?(file_path) do 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 else Logger.debug(" Skipping non-file: #{Path.basename(file_path)}") nil end end defp report_upload_results(results) do successes = Enum.count(results, &match?({:ok, _}, &1)) failures = Enum.count(results, &match?({:error, _, _}, &1)) Logger.info("MIB upload complete: #{successes} succeeded, #{failures} failed") 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