228 lines
6.8 KiB
Elixir
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
|