README.md

# MikrotikApi

An Elixir wrapper for MikroTik RouterOS REST API. Auth is established once and passed per call alongside a simple target IP (IPv4/IPv6). We bias toward programmatic usage with POST for create/command-style operations while supporting standard REST verbs.

Reference: MikroTik RouterOS REST API — https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API

## Goals
- Keep the surface area small and pragmatic; don’t overcomplicate.
- Stateless by default: establish Auth once and pass it per call with a target IP; do not embed credentials in the target.
- Prefer HTTPS with proper certificate verification; allow opt-out for lab setups.
- Use Logger for all output (no IO.puts/IO.inspect), and redact credentials.

See rest_api.md for the full specification and plan.

## Installation

Add to your deps (library not yet published to Hex):

```elixir
# mix.exs (in your host application)

def deps do
  [
    # When published on Hex:
    {:mikrotik_api, "~> 0.1"}

    # For local development prior to Hex, use a path or VCS dep instead:
    # {:mikrotik_api, path: "/absolute/path/to/mikrotik_api"}
  ]
end
```

## Quick Start

Transport configuration
- Default scheme is configurable via your host app config.
- Over WireGuard, HTTP is acceptable and simpler to operate; HTTPS remains supported if you prefer certificates.

Examples:

```elixir
# config/runtime.exs (in your host application)
import Config
config :mikrotik_api, default_scheme: :http
# For HTTPS by default instead:
# config :mikrotik_api, default_scheme: :https
```

```elixir
# Establish auth once; target is just an IP
auth = MikrotikApi.Auth.new(
  username: System.get_env("MT_USER"),
  password: System.get_env("MT_PASS"),
  verify: :verify_peer
)

ip = "10.0.0.1"

# GET system resource over WireGuard (HTTP inside private network)
# If you want HTTPS, set default_scheme: :https or pass scheme: :https per call.
case MikrotikApi.get(auth, ip, "/system/resource", scheme: :http) do
  {:ok, data} -> Logger.info("system resource ok")
  {:error, err} -> Logger.error("system resource failed: #{inspect(err)}")
end

# POST to create an IP address (programmatic workflow)
attrs = %{"address" => "192.168.88.2/24", "interface" => "bridge"}
# For HTTPS with self-signed certs in lab, you can use verify: :verify_none (accepting the risk):
# auth = MikrotikApi.Auth.new(username: ..., password: ..., verify: :verify_none)
# For HTTPS with real certs, prefer verify: :verify_peer and provide CA info if needed:
# auth = MikrotikApi.Auth.new(username: ..., password: ..., verify: :verify_peer, ssl_opts: [cacertfile: '/etc/ssl/certs/ca-bundle.crt'])
case MikrotikApi.post(auth, ip, "/ip/address", attrs, scheme: :http) do
  {:ok, created} -> Logger.info("added ip address")
  {:error, err} -> Logger.error("add ip failed: #{inspect(err)}")
end
```

## Security Notes
- Prefer HTTPS (www-ssl) as advised by MikroTik; avoid HTTP except for isolated testing.
- For self-signed routers in lab environments, you may set verify: :verify_none, but understand the risks.

## API Overview

Telemetry helpers (Phase 1–4)
- System/operations
  - system_health/3 — GET /system/health
  - system_packages/3 — GET /system/package
  - firewall_connection_list/3 — GET /ip/firewall/connection
  - dns_config/3 — GET /ip/dns
  - dns_cache_list/3 — GET /ip/dns/cache
  - ip_pool_list/3 — GET /ip/pool
  - firewall_address_list/3 — GET /ip/firewall/address-list
- IPv6
  - ipv6_route_list/3 — GET /ipv6/route
  - ipv6_pool_list/3 — GET /ipv6/pool
  - ipv6_firewall_filter_list/3 — GET /ipv6/firewall/filter
  - ipv6_neighbor_list/3 — GET /ipv6/neighbor
- ipv6_firewall_address_list/3 — GET /ipv6/firewall/address-list

- Wireless/WiFi and CAPsMAN (Phase 3)
  - wireless_registration_table/3 — GET /interface/wireless/registration-table
  - wireless_interface_list/3, wireless_interface_add/4, wireless_interface_update/5, wireless_interface_delete/4, wireless_interface_ensure/5
  - wireless_security_profile_list/3, wireless_security_profile_add/4, wireless_security_profile_update/5, wireless_security_profile_delete/4, wireless_security_profile_ensure/5
  - wifi_interface_list/3, wifi_interface_update/5
  - wifi_ssid_list/3, wifi_ssid_add/4, wifi_ssid_update/5, wifi_ssid_delete/4, wifi_ssid_ensure/5
  - wifi_security_list/3, wifi_security_add/4, wifi_security_update/5, wifi_security_delete/4, wifi_security_ensure/5
  - capsman_interface_list/3, capsman_registration_table/3, capsman_security_list/3, capsman_security_add/4, capsman_security_ensure/5, capsman_provisioning_list/3, capsman_provisioning_add/4, capsman_provisioning_ensure/4

- Extended telemetry (Phase 4)
  - ethernet_poe_list/3 — GET /interface/ethernet/poe
  - interface_ethernet_monitor/4 — GET /interface/ethernet/monitor/{ident}
  - tool_netwatch_list/3 — GET /tool/netwatch
  - ip_cloud_info/3 — GET /ip/cloud
  - eoip_list/3 — GET /interface/eoip
  - gre_list/3 — GET /interface/gre
  - ipip_list/3 — GET /interface/ipip
  - ethernet_switch_port_list/3 — GET /interface/ethernet/switch/port
  - user_active_list/3 — GET /user/active
  - queue_simple_list/3 — GET /queue/simple
  - queue_tree_list/3 — GET /queue/tree
  - routing_bfd_list/3 — GET /routing/bfd/session
  - routing_bgp_list/3 — GET /routing/bgp/session
  - routing_stats/3 — GET /routing/stats
  - certificate_list/3 — GET /certificate
  - container_list/3 — GET /container

Core functions (generic verbs)
- get(auth, ip, path, opts \\ [])
- post(auth, ip, path, body \\ nil, opts \\ [])
- put(auth, ip, path, body, opts \\ [])
- patch(auth, ip, path, body, opts \\ [])
- delete(auth, ip, path, opts \\ [])

Common opts
- scheme: :http | :https (default from config :mikrotik_api, :default_scheme)
- params: map for query params
- headers: list of {binary(), binary()}
- decode: true | false (default true). When true, responses are decoded via Elixir's built-in JSON; false returns raw body strings.

Helper functions (selected)
- Probe: probe_device/3 — summarize system info and counts for key tables (interfaces, IP addresses, ARP, neighbors)
- Probe: probe_wireless/3 — summarize availability for wireless/wifi endpoints
- Probe: probe_wireless/3 — summarize availability for wireless/wifi endpoints
- Probe: probe_device/3 — summarize system info and counts for key tables (interfaces, IP addresses, ARP, neighbors)
- System: system_resource/2
- Interfaces: interface_list/2, interface_update/4, interface_enable/3, interface_disable/3, interface_ensure/5
- IP addresses: ip_address_list/2, ip_address_add/3, ip_address_update/4, ip_address_delete/3, ip_address_ensure/4
- DHCP leases: dhcp_lease_list/2, dhcp_lease_add/3, dhcp_lease_update/4, dhcp_lease_delete/3, dhcp_lease_ensure/4
- Firewall filter: firewall_filter_list/2, firewall_filter_add/3, firewall_filter_delete/3, firewall_filter_ensure/4
- Firewall NAT: firewall_nat_list/2, firewall_nat_add/3, firewall_nat_delete/3, firewall_nat_ensure/4
- Routes: route_list/2, route_add/3, route_delete/3, route_ensure/4
- Bridges: bridge_list/2, bridge_add/3, bridge_update/4, bridge_delete/3, bridge_ensure/5
- Bridge ports: bridge_port_list/2, bridge_port_add/3, bridge_port_update/4, bridge_port_delete/3
- Bridge VLANs: bridge_vlan_list/2, bridge_vlan_add/3, bridge_vlan_update/4, bridge_vlan_delete/3, bridge_vlan_ensure/6
- Wireless (legacy): wireless_interface_list/2, wireless_interface_add/3, wireless_interface_update/4, wireless_interface_delete/3, wireless_interface_ensure/5, wireless_registration_table/2, wireless_security_profile_list/2, wireless_security_profile_add/3, wireless_security_profile_update/4, wireless_security_profile_delete/3, wireless_security_profile_ensure/5
- WiFi (wifiwave2): wifi_interface_list/2, wifi_interface_update/4, wifi_ssid_list/2, wifi_ssid_add/3, wifi_ssid_update/4, wifi_ssid_delete/3, wifi_security_list/2, wifi_security_add/3, wifi_security_update/4, wifi_security_delete/3, wifi_security_ensure/5, wifi_ssid_ensure/5

HTTP over WireGuard (decode: true)

ARP and neighbors
```elixir
auth = MikrotikApi.Auth.new(username: System.get_env("MT_USER"), password: System.get_env("MT_PASS"), verify: :verify_none)
ip = System.get_env("MT_IP")
{:ok, arp} = MikrotikApi.arp_list(auth, ip, scheme: :http)
{:ok, neighbors} = MikrotikApi.neighbor_list(auth, ip, scheme: :http)
```
```elixir
auth = MikrotikApi.Auth.new(
  username: System.get_env("MT_USER"),
  password: System.get_env("MT_PASS"),
  verify: :verify_none
)

ip = System.get_env("MT_IP")

{:ok, sys} = MikrotikApi.system_resource(auth, ip, scheme: :http)
{:ok, ip_addrs} = MikrotikApi.ip_address_list(auth, ip, scheme: :http)
```

WiFi notes
- Ensure helpers are provided to idempotently create or reuse entries:
  - wifi_security_ensure/5 (name, attrs)
  - wifi_ssid_ensure/5 (name, attrs)

CAPsMAN examples
- Ensure helpers are provided:
  - capsman_security_ensure/5
  - capsman_provisioning_ensure/4
```elixir
auth = MikrotikApi.Auth.new(username: System.get_env("MT_USER"), password: System.get_env("MT_PASS"), verify: :verify_none)
ip = System.get_env("MT_IP")
{:ok, caps_if} = MikrotikApi.capsman_interface_list(auth, ip, scheme: :http)
{:ok, caps_sec} = MikrotikApi.capsman_security_list(auth, ip, scheme: :http)
{:ok, caps_reg} = MikrotikApi.capsman_registration_table(auth, ip, scheme: :http)
```

- Some wifiwave2 subresources (e.g., /interface/wifi/ssid) may return 500 on devices without WiFi configured or when the package/version doesn’t expose SSIDs yet. The library will return {:error, %MikrotikApi.Error{reason: :wifi_ssid_unavailable}} in this case.
- Ensure helpers are provided to idempotently create or reuse entries:
  - wifi_security_ensure/5 (name, attrs)
  - wifi_ssid_ensure/5 (name, attrs)
- Some wifiwave2 subresources (e.g., /interface/wifi/ssid) may return 500 on devices without WiFi configured or when the package/version doesn’t expose SSIDs yet. The library will return {:error, %MikrotikApi.Error{reason: :wifi_ssid_unavailable}} in this case.
- Some wifiwave2 subresources (e.g., /interface/wifi/ssid) may return 500 on devices without WiFi configured or when the package/version doesn’t expose SSIDs yet. The library will return {:error, %MikrotikApi.Error{reason: :wifi_ssid_unavailable}} in this case.

Probe examples
```elixir
auth = MikrotikApi.Auth.new(
  username: System.get_env("MT_USER"),
  password: System.get_env("MT_PASS"),
  verify: :verify_none
)

ip = System.get_env("MT_IP")
{:ok, summary} = MikrotikApi.probe_wireless(auth, ip, scheme: :http)
# summary => %{wireless: %{...}, wifi: %{...}}
```

```elixir
auth = MikrotikApi.Auth.new(
  username: System.get_env("MT_USER"),
  password: System.get_env("MT_PASS"),
  verify: :verify_none
)

ip = System.get_env("MT_IP")
{:ok, summary} = MikrotikApi.probe_device(auth, ip, scheme: :http)
# summary => %{system: {:ok, %{...}}, counts: %{interfaces: n, ip_addresses: n, arp: n, neighbors: n}}

## Batch reads (multi)
To fetch the same path across multiple devices concurrently, use multi/6.

Example:
```elixir
auth = MikrotikApi.Auth.new(username: System.get_env("MT_USER"), password: System.get_env("MT_PASS"), verify: :verify_none)
ips = ["192.168.88.1", "192.168.88.2"]
results = MikrotikApi.multi(auth, ips, :get, "/system/resource", [scheme: :http], max_concurrency: 5, timeout: 10_000)
# [%{ip: "192.168.88.1", result: {:ok, %{...}}}, ...]
```

## Developer guardrails
To prevent regressions, run:

```bash
mix guardrails
```
This task scans for disallowed patterns (IO.puts/IO.inspect and the legacy MikrotikApi.JSON).

## Normalization helpers (optional)
The library includes optional utilities for exporters to normalize string fields commonly found in RouterOS responses.

Examples:
```elixir
# Normalize wireless registration-table entries (legacy wireless)
{:ok, regs} = MikrotikApi.wireless_registration_table(auth, ip, scheme: :http)
normalized =
  Enum.map(regs, fn e ->
    e
    |> Map.update("rx-signal", nil, &MikrotikApi.Normalize.to_int/1)
    |> Map.update("tx-rate", nil, &MikrotikApi.Normalize.parse_rate_mbps/1)
    |> Map.update("rx-rate", nil, &MikrotikApi.Normalize.parse_rate_mbps/1)
  end)

# Normalize booleans
val = MikrotikApi.Normalize.normalize_bool("enabled") # => true
```
```

Multi (concurrent) examples
```elixir
auth = MikrotikApi.Auth.new(username: System.get_env("MT_USER"), password: System.get_env("MT_PASS"), verify: :verify_none)
ips = ["192.168.88.1", "192.168.88.2", "192.168.88.3"]
results = MikrotikApi.multi(auth, ips, :get, "/system/resource", [scheme: :http], max_concurrency: 5, timeout: 10_000)
# results => [%{ip: "192.168.88.1", result: {:ok, %{...}}}, ...]
```

Ensure examples
```elixir
# Route ensure
attrs = %{"dst-address" => "10.10.0.0/16", "gateway" => "192.168.88.1"}
{:ok, %{dst: _, gw: _}} = MikrotikApi.route_ensure(auth, ip, attrs, scheme: :http)

# Bridge and ports/VLANs ensure
{:ok, "bridgeLocal"} = MikrotikApi.bridge_ensure(auth, ip, "bridgeLocal", %{}, scheme: :http)
{:ok, {"bridgeLocal", "ether2"}} = MikrotikApi.bridge_port_ensure(auth, ip, "bridgeLocal", "ether2", %{}, scheme: :http)
{:ok, {"bridgeLocal", "10"}} = MikrotikApi.bridge_vlan_ensure(auth, ip, "bridgeLocal", "10", %{"tagged" => "sfp-sfpplus1", "untagged" => "ether2"}, scheme: :http)

# Interface ensure (patch only changed keys)
{:ok, %{changed: _}} = MikrotikApi.interface_ensure(auth, ip, "ether1", %{"mtu" => "1500", "disabled" => "false"}, scheme: :http)

# IP address ensure
{:ok, _addr} = MikrotikApi.ip_address_ensure(auth, ip, %{"address" => "192.168.88.2/24", "interface" => "bridgeLocal"}, scheme: :http)

# DHCP lease ensure
{:ok, _lease} = MikrotikApi.dhcp_lease_ensure(auth, ip, %{"address" => "192.168.88.100", "mac-address" => "AA:BB:CC:DD:EE:FF"}, scheme: :http)

# Firewall filter ensure
rule = %{"chain" => "forward", "action" => "accept", "comment" => "allow"}
{:ok, _} = MikrotikApi.firewall_filter_ensure(auth, ip, rule, scheme: :http)

# Firewall NAT ensure (default match by chain+action)
nat_rule = %{"chain" => "dstnat", "action" => "dst-nat", "to-addresses" => "192.168.88.2"}
{:ok, _} = MikrotikApi.firewall_nat_ensure(auth, ip, nat_rule, scheme: :http)
```

WiFi ensure workflow (wifiwave2)
```elixir
# Ensure a wifi security profile, then ensure an SSID using it
{:ok, _} = MikrotikApi.wifi_security_ensure(auth, ip, "SEC-PSK", %{"wpa2-pre-shared-key" => "supersecret"}, scheme: :http)
case MikrotikApi.wifi_ssid_ensure(auth, ip, "WG-LAB", %{"security" => "SEC-PSK"}, scheme: :http) do
  {:ok, _} -> :ok
  {:error, %MikrotikApi.Error{reason: :wifi_ssid_unavailable}} -> :ok # device may not expose SSIDs
  other -> other
end

# Optionally update a wifi interface to reference a configuration (if used in your setup)
# MikrotikApi.wifi_interface_update(auth, ip, "wifi1", %{"disabled" => "false"}, scheme: :http)
```

Legacy wireless ensure (wireless package)
```elixir
{:ok, _} = MikrotikApi.wireless_security_profile_ensure(auth, ip, "LEGACY-SEC", %{"mode" => "dynamic-keys"}, scheme: :http)
{:ok, _} = MikrotikApi.wireless_interface_ensure(auth, ip, "wlan1", %{"disabled" => "false"}, scheme: :http)
```

CAPsMAN ensure examples
```elixir
{:ok, _} = MikrotikApi.capsman_security_ensure(auth, ip, "CAPS-SEC", %{"authentication-types" => "wpa2-psk"}, scheme: :http)
{:ok, _} = MikrotikApi.capsman_provisioning_ensure(auth, ip, %{"action" => "create-enabled", "master-configuration" => "MASTER"}, scheme: :http)
```

HTTPS with verify_peer and CA
```elixir
auth = MikrotikApi.Auth.new(
  username: System.get_env("MT_USER"),
  password: System.get_env("MT_PASS"),
  verify: :verify_peer,
  ssl_opts: [cacertfile: "/path/to/ca.pem"]
)

ip = System.get_env("MT_IP")

{:ok, sys} = MikrotikApi.system_resource(auth, ip, scheme: :https)
```

## Reference
- MikroTik RouterOS REST API: https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API
- See rest_api.md for the complete plan and API surface.