handle mib uploading
This commit is contained in:
parent
1f123bbeb9
commit
50d8b27447
5 changed files with 238 additions and 0 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -53,3 +53,7 @@ profiles.json
|
|||
# Elixir language server and tooling
|
||||
.elixir-tools/
|
||||
.expert/
|
||||
|
||||
# MIB files directory (keep directory in git, ignore contents)
|
||||
/mibs/*
|
||||
!/mibs/.gitkeep
|
||||
|
|
|
|||
|
|
@ -95,6 +95,9 @@ config :towerops, ToweropsWeb.Endpoint,
|
|||
# Set environment identifier for runtime checks
|
||||
config :towerops, :env, :dev
|
||||
|
||||
# Configure MIB directory for development
|
||||
config :towerops, :mib_dir, Path.expand("mibs", Path.dirname(__DIR__))
|
||||
|
||||
# Configure Redis/Valkey for development
|
||||
config :towerops, :redis,
|
||||
host: "localhost",
|
||||
|
|
|
|||
|
|
@ -154,6 +154,9 @@ if config_env() == :prod do
|
|||
# Set default sender for all emails
|
||||
config :towerops, :mailer_from, {"Towerops", "hi@towerops.net"}
|
||||
|
||||
# Configure MIB directory for production (persistent volume mount)
|
||||
config :towerops, :mib_dir, "/app/mibs"
|
||||
|
||||
config :towerops, :redis,
|
||||
host: redis_host,
|
||||
port: redis_port
|
||||
|
|
|
|||
228
lib/mix/tasks/upload_mibs.ex
Normal file
228
lib/mix/tasks/upload_mibs.ex
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
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
|
||||
0
mibs/.gitkeep
Normal file
0
mibs/.gitkeep
Normal file
Loading…
Add table
Reference in a new issue