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 language server and tooling
|
||||||
.elixir-tools/
|
.elixir-tools/
|
||||||
.expert/
|
.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
|
# Set environment identifier for runtime checks
|
||||||
config :towerops, :env, :dev
|
config :towerops, :env, :dev
|
||||||
|
|
||||||
|
# Configure MIB directory for development
|
||||||
|
config :towerops, :mib_dir, Path.expand("mibs", Path.dirname(__DIR__))
|
||||||
|
|
||||||
# Configure Redis/Valkey for development
|
# Configure Redis/Valkey for development
|
||||||
config :towerops, :redis,
|
config :towerops, :redis,
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,9 @@ if config_env() == :prod do
|
||||||
# Set default sender for all emails
|
# Set default sender for all emails
|
||||||
config :towerops, :mailer_from, {"Towerops", "hi@towerops.net"}
|
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,
|
config :towerops, :redis,
|
||||||
host: redis_host,
|
host: redis_host,
|
||||||
port: redis_port
|
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