Changes profile import endpoint to use standard API token authentication: API Token Changes: - Add user_id to api_tokens table (tracks who created the token) - Update ApiTokens.verify_token/1 to return user along with org_id - Update ApiAuth plug to assign current_user from token Profile Import Changes: - Move endpoint from /api/v1/admin/profiles/import to /api/v1/profiles/import - Check user.is_superuser in controller instead of using RequireSuperuser plug - Use api_v1 pipeline (Bearer token auth) instead of browser session - Update documentation to show API token usage Security: - Only API tokens created by superusers can import profiles - Returns 403 Forbidden if token user is not a superuser - Logs import attempts with user email for audit trail This provides a consistent API experience using Bearer tokens instead of requiring browser session cookies.
179 lines
5.2 KiB
Elixir
179 lines
5.2 KiB
Elixir
defmodule Mix.Tasks.ExportProfiles do
|
|
@moduledoc """
|
|
Exports device SNMP profiles to JSON format for uploading to production.
|
|
|
|
## Usage
|
|
|
|
# Export all profiles to JSON
|
|
mix export_profiles --librenms-path ~/dev/librenms --output profiles.json
|
|
|
|
# Export specific profiles
|
|
mix export_profiles --librenms-path ~/dev/librenms --profiles epmp,airos-af --output profiles.json
|
|
|
|
## Options
|
|
|
|
--librenms-path PATH - Path to external installation (required)
|
|
--output PATH - Output JSON file path (default: profiles.json)
|
|
--profiles LIST - Export only specific profiles (comma-separated, optional)
|
|
|
|
## Output Format
|
|
|
|
The exported JSON file contains all profile data in a format that can be uploaded
|
|
to the production server via API:
|
|
|
|
POST /api/v1/admin/profiles/import
|
|
Content-Type: application/json
|
|
Authorization: Bearer YOUR_TOKEN
|
|
|
|
"""
|
|
use Mix.Task
|
|
|
|
@requirements ["app.start"]
|
|
|
|
@impl Mix.Task
|
|
def run(args) do
|
|
{opts, _, _} =
|
|
OptionParser.parse(args,
|
|
strict: [
|
|
librenms_path: :string,
|
|
output: :string,
|
|
profiles: :string
|
|
]
|
|
)
|
|
|
|
librenms_path = opts[:librenms_path] || guess_librenms_path()
|
|
output_file = opts[:output] || "profiles.json"
|
|
profile_filter = parse_profile_filter(opts[:profiles])
|
|
|
|
if librenms_path do
|
|
Mix.shell().info("Exporting profiles from: #{librenms_path}")
|
|
export_profiles(librenms_path, output_file, profile_filter)
|
|
else
|
|
Mix.shell().error("Please specify --librenms-path option")
|
|
print_usage()
|
|
end
|
|
end
|
|
|
|
defp export_profiles(librenms_path, output_file, profile_filter) do
|
|
detection_dir = Path.join(librenms_path, "resources/definitions/os_detection")
|
|
discovery_dir = Path.join(librenms_path, "resources/definitions/os_discovery")
|
|
|
|
detection_files = Path.wildcard(Path.join(detection_dir, "*.yaml"))
|
|
|
|
# Filter profiles if specified
|
|
detection_files =
|
|
if profile_filter do
|
|
Enum.filter(detection_files, fn file ->
|
|
os_name = Path.basename(file, ".yaml")
|
|
os_name in profile_filter
|
|
end)
|
|
else
|
|
detection_files
|
|
end
|
|
|
|
Mix.shell().info("Found #{length(detection_files)} profiles to export")
|
|
Mix.shell().info("Reading YAML files...")
|
|
|
|
# Read and parse all profiles
|
|
profiles =
|
|
detection_files
|
|
|> Enum.map(fn detection_file ->
|
|
os_name = Path.basename(detection_file, ".yaml")
|
|
discovery_file = Path.join(discovery_dir, "#{os_name}.yaml")
|
|
|
|
Mix.shell().info(" Reading: #{os_name}")
|
|
|
|
detection_data = read_yaml_file(detection_file)
|
|
|
|
discovery_data =
|
|
if File.exists?(discovery_file) do
|
|
read_yaml_file(discovery_file)
|
|
end
|
|
|
|
%{
|
|
os: os_name,
|
|
detection: detection_data,
|
|
discovery: discovery_data
|
|
}
|
|
end)
|
|
|> Enum.reject(&is_nil(&1.detection))
|
|
|
|
Mix.shell().info("\nWriting to: #{output_file}")
|
|
|
|
# Write JSON file
|
|
json_data = Jason.encode!(profiles, pretty: true)
|
|
File.write!(output_file, json_data)
|
|
|
|
file_size = format_bytes(File.stat!(output_file).size)
|
|
Mix.shell().info("✓ Exported #{length(profiles)} profiles (#{file_size})")
|
|
Mix.shell().info("\nUpload to production (requires superuser API token):")
|
|
|
|
Mix.shell().info("""
|
|
|
|
curl -X POST https://towerops.net/api/v1/profiles/import \\
|
|
-H "Authorization: Bearer YOUR_SUPERUSER_API_TOKEN" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d @#{output_file}
|
|
|
|
Note: You must create an API token using a superuser account.
|
|
Only tokens created by superusers can import device profiles.
|
|
""")
|
|
end
|
|
|
|
defp read_yaml_file(file_path) do
|
|
case YamlElixir.read_from_file(file_path) do
|
|
{:ok, data} ->
|
|
data
|
|
|
|
{:error, reason} ->
|
|
Mix.shell().error(" Failed to read #{file_path}: #{inspect(reason)}")
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp parse_profile_filter(nil), do: nil
|
|
|
|
defp parse_profile_filter(profiles_string) do
|
|
profiles_string
|
|
|> String.split(",", trim: true)
|
|
|> Enum.map(&String.trim/1)
|
|
end
|
|
|
|
defp guess_librenms_path do
|
|
home = System.get_env("HOME")
|
|
possible_paths = ["~/dev/librenms", "#{home}/dev/librenms", "/opt/librenms"]
|
|
|
|
possible_paths
|
|
|> Enum.find(fn path ->
|
|
expanded = Path.expand(path)
|
|
File.dir?(expanded)
|
|
end)
|
|
|> case do
|
|
nil -> nil
|
|
path -> Path.expand(path)
|
|
end
|
|
end
|
|
|
|
defp format_bytes(bytes) when bytes < 1024, do: "#{bytes} B"
|
|
defp format_bytes(bytes) when bytes < 1024 * 1024, do: "#{Float.round(bytes / 1024, 1)} KB"
|
|
defp format_bytes(bytes), do: "#{Float.round(bytes / (1024 * 1024), 1)} MB"
|
|
|
|
defp print_usage do
|
|
Mix.shell().info("""
|
|
Usage:
|
|
mix export_profiles [options]
|
|
|
|
Options:
|
|
--librenms-path PATH Path to external installation (required)
|
|
--output PATH Output JSON file (default: profiles.json)
|
|
--profiles LIST Export specific profiles (comma-separated)
|
|
|
|
Examples:
|
|
# Export all profiles
|
|
mix export_profiles --librenms-path ~/dev/librenms --output profiles.json
|
|
|
|
# Export specific profiles
|
|
mix export_profiles --librenms-path ~/dev/librenms --profiles epmp,unifi,airos-af
|
|
""")
|
|
end
|
|
end
|