towerops/lib/mix/tasks/export_profiles.ex
Graham McIntire 56093bb493
refactor: use API token auth for profile imports instead of session cookies
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.
2026-01-18 09:30:21 -06:00

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