towerops/lib/mix/tasks/upload_mibs.ex

228 lines
6.8 KiB
Elixir

defmodule Mix.Tasks.UploadMibs do
@shortdoc "Upload MIB files to production API"
@moduledoc """
Uploads MIB files from a source directory to the production API.
## Usage
# Upload all vendor MIB directories
mix upload_mibs --source-path ~/librenms/mibs --token YOUR_SUPERUSER_TOKEN
# Upload to specific environment
mix upload_mibs --source-path ~/librenms/mibs --token TOKEN --api-url https://towerops.net
# Upload specific vendors only
mix upload_mibs --source-path ~/librenms/mibs --token TOKEN --vendors mikrotik,cisco,ubiquiti
## Options
* `--source-path` - Path to the directory containing MIB files (required)
* `--token` - Superuser API token for authentication (required)
* `--api-url` - API base URL (default: https://towerops.net)
* `--vendors` - Comma-separated list of vendor directories to upload (default: all)
## Notes
* Creates temporary tar.gz archives for each vendor directory
* Uploads via multipart/form-data to POST /api/v1/mibs
* Requires a superuser API token
* Cleans up temporary archives after upload
"""
use Mix.Task
require Logger
@default_api_url "https://towerops.net"
def run(args) do
# Start required applications
{:ok, _} = Application.ensure_all_started(:req)
{opts, _} =
OptionParser.parse!(args,
strict: [
source_path: :string,
token: :string,
api_url: :string,
vendors: :string
]
)
source_path = opts[:source_path] || raise "Missing --source-path argument"
token = opts[:token] || raise "Missing --token argument"
api_url = opts[:api_url] || @default_api_url
vendors = parse_vendors(opts[:vendors])
if !File.exists?(source_path) do
raise "Source directory not found: #{source_path}"
end
case vendors do
:all ->
upload_all_vendors(source_path, api_url, token)
vendor_list ->
upload_specific_vendors(source_path, api_url, token, vendor_list)
end
end
defp parse_vendors(nil), do: :all
defp parse_vendors(""), do: :all
defp parse_vendors(vendors_string) do
vendors_string
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
end
defp upload_all_vendors(source_path, api_url, token) do
Logger.info("Discovering vendor directories in #{source_path}...")
vendors =
source_path
|> File.ls!()
|> Enum.filter(fn name ->
path = Path.join(source_path, name)
File.dir?(path) and name != "lost+found" and !String.starts_with?(name, ".")
end)
|> Enum.sort()
Logger.info("Found #{length(vendors)} vendor directories")
upload_vendor_list(source_path, api_url, token, vendors)
end
defp upload_specific_vendors(source_path, api_url, token, vendors) do
Logger.info("Uploading #{length(vendors)} specified vendors")
upload_vendor_list(source_path, api_url, token, vendors)
end
defp upload_vendor_list(source_path, api_url, token, vendors) do
results =
Enum.map(vendors, fn vendor ->
vendor_path = Path.join(source_path, vendor)
if File.exists?(vendor_path) do
upload_vendor(api_url, token, vendor, vendor_path)
else
Logger.warning("Vendor directory not found: #{vendor}")
{:error, vendor, "Directory not found"}
end
end)
# Summary
successes = Enum.count(results, &match?({:ok, _, _}, &1))
failures = Enum.count(results, &match?({:error, _, _}, &1))
Logger.info("Upload complete: #{successes} succeeded, #{failures} failed")
if failures > 0 do
Logger.error("Failed uploads:")
Enum.each(results, fn
{:error, vendor, reason} ->
Logger.error(" - #{vendor}: #{reason}")
_ ->
:ok
end)
end
end
defp upload_vendor(api_url, token, vendor, vendor_path) do
Logger.info("Uploading #{vendor}...")
# Create temporary tar.gz archive
temp_dir = System.tmp_dir!()
archive_name = "#{vendor}.tar.gz"
archive_path = Path.join(temp_dir, archive_name)
try do
# Create tar.gz archive
case create_tarball(vendor_path, archive_path) do
:ok ->
# Upload to API
upload_tarball(api_url, token, vendor, archive_path, archive_name)
{:error, reason} ->
Logger.error("Failed to create archive for #{vendor}: #{inspect(reason)}")
{:error, vendor, "Archive creation failed"}
end
after
# Clean up temporary archive
File.rm(archive_path)
end
end
defp create_tarball(source_dir, archive_path) do
parent_dir = Path.dirname(source_dir)
vendor_name = Path.basename(source_dir)
case System.cmd(
"tar",
["-czf", archive_path, "-C", parent_dir, vendor_name],
stderr_to_stdout: true
) do
{_output, 0} ->
:ok
{error, exit_code} ->
Logger.error("tar failed (exit #{exit_code}): #{error}")
{:error, :tar_failed}
end
rescue
e in ErlangError ->
Logger.error("tar command not found: #{inspect(e)}")
{:error, :tar_not_found}
end
defp upload_tarball(api_url, token, vendor, archive_path, archive_name) do
url = "#{api_url}/api/v1/mibs"
Logger.debug("Uploading #{archive_name} to #{url}")
# Create multipart form with file
file_content = File.read!(archive_path)
form = [
{"vendor", vendor},
{"file", {file_content, filename: archive_name, content_type: "application/gzip"}}
]
case Req.post(url,
auth: {:bearer, token},
form_multipart: form,
receive_timeout: 120_000
) do
{:ok, %{status: 201, body: body}} when is_map(body) ->
files_count = Map.get(body, "files_count", "unknown")
Logger.info("#{vendor}: uploaded #{files_count} files")
{:ok, vendor, files_count}
{:ok, %{status: status, body: body}} when is_map(body) ->
error = Map.get(body, "error", "Unknown error")
# For 403, log the full error message since it's likely a permission issue
if status == 403 do
Logger.error("#{vendor}: HTTP #{status} - #{error}")
Logger.error(" Make sure your API token has superuser permissions!")
else
Logger.error("#{vendor}: HTTP #{status} - #{error}")
end
{:error, vendor, "HTTP #{status}: #{error}"}
{:ok, %{status: status, body: body}} ->
# Non-JSON response (likely HTML error page)
error_preview = body |> to_string() |> String.slice(0, 100)
Logger.error("#{vendor}: HTTP #{status} - #{error_preview}...")
{:error, vendor, "HTTP #{status}: Non-JSON response"}
{:error, reason} ->
Logger.error("#{vendor}: Request failed - #{inspect(reason)}")
{:error, vendor, "Request failed: #{inspect(reason)}"}
end
end
end