Class: Mt::Wall::Transport::RestApi
- Defined in:
- lib/mt/wall/transport/rest_api.rb
Overview
RouterOS v7+ REST API adapter – the primary transport.
Talks to https://<host>/rest/<path> over HTTP Basic auth. Verified RouterOS v7 REST mapping (Plan::Operation -> REST verb):
* fetch GET /rest/<path> (list rows)
* :create PUT /rest/<path> (place-before in body)
* :update PATCH /rest/<path>/<.id>
* :delete DELETE /rest/<path>/<.id>
* :move POST /rest/<path>/move (reorder by .id)
‘<path>` is the DesiredState key with the leading slash, e.g. `/ip/firewall/filter` -> `/rest/ip/firewall/filter`; IPv6 maps to `/rest/ipv6/firewall/…`. Requires RouterOS v7+ with the REST service enabled.
NORMALIZATION: RouterOS returns booleans/numbers as STRINGS (“true”/“yes”/“443”); fetch normalizes them so Plan.diff compares like for like, and rows flagged ‘dynamic` are excluded from the fetched state (never diffed or deleted).
── SECURITY DEFAULTS ──────────────────────────────────────────────────
-
Credentials are read from ENV, never from the DSL. Canonical, collision -checked derivation: a device named “edge-1” uses MT_WALL_EDGE_1_USER / MT_WALL_EDGE_1_PASSWORD (see RestApi.credentials_for); a missing expected var is a fail-fast TransportError.
-
PLAINTEXT HTTP IS REFUSED by default. Talking to a device requires TLS; downgrading needs a loud, explicit opt-in (‘insecure_http: true` option or MT_WALL_INSECURE_HTTP env) — absent that, http raises TransportError.
-
TLS verification is ON. Prefer pinning via ‘ca_file:` / `tls_fingerprint:` over a blanket `verify_tls: false`; disabling verification requires an explicit opt-out and is discouraged.
-
CREDENTIAL REDACTION is centralized: the password MUST NOT appear in any URL, log, exception message, or rendered .rsc. (A test asserts the password never surfaces in plan/error/.rsc output.)
Constant Summary collapse
- PLAINTEXT_PORT =
The standard cleartext HTTP port; targeting it requires an explicit plaintext opt-in (Basic-auth creds would otherwise travel in clear).
80- OPEN_TIMEOUT =
Connection timeouts (seconds).
10- READ_TIMEOUT =
30- REQUEST_CLASSES =
Net::HTTP request classes per logical verb.
{ get: Net::HTTP::Get, put: Net::HTTP::Put, patch: Net::HTTP::Patch, delete: Net::HTTP::Delete, post: Net::HTTP::Post }.freeze
- NETWORK_ERRORS =
Network-layer failures we translate into a (redacted) TransportError.
[ SocketError, IOError, SystemCallError, Timeout::Error, OpenSSL::SSL::SSLError, Net::OpenTimeout, Net::ReadTimeout, Net::ProtocolError ].freeze
- ID_KEY =
Plan::ID_KEY
Class Method Summary collapse
-
.assert_unique_env_prefixes!(device_names) ⇒ void
Guard: distinct device names must not collapse to the same ENV prefix, which would silently share one set of credentials.
-
.credentials_for(device_name) ⇒ Hash{Symbol=>String}
Derive the canonical ENV var names and read credentials for a device.
-
.env_prefix(device_name) ⇒ String
Canonical ENV var prefix for a device name: upcased, every run of non- collapsed to a single “_”.
-
.for_device(device, http_client: nil) ⇒ RestApi
Build a RestApi for a Model::Device, reading credentials from ENV and non-secret connection options from ‘device.options`.
-
.rest_key(symbol) ⇒ String
Ruby underscore symbol -> RouterOS hyphenated REST key.
-
.ruby_key(string) ⇒ Symbol
RouterOS hyphenated REST key -> Ruby underscore symbol.
Instance Method Summary collapse
- #apply(operations) ⇒ void
-
#arm_auto_revert(_snapshot, timeout:) ⇒ String
Back up the box and arm a device-side scheduler that restores the backup after ‘timeout`.
-
#confirm(handle) ⇒ void
Cancel an armed revert: delete the scheduler and its restore script.
- #fetch(paths, managed_list_names: []) ⇒ DesiredState
-
#initialize(host:, user: nil, password: nil, port: 443, verify_tls: true, ca_file: nil, tls_fingerprint: nil, insecure_http: false, http_client: nil) ⇒ RestApi
constructor
A new instance of RestApi.
-
#inspect ⇒ String
(also: #to_s)
Redacted representation — the password is NEVER rendered.
Constructor Details
#initialize(host:, user: nil, password: nil, port: 443, verify_tls: true, ca_file: nil, tls_fingerprint: nil, insecure_http: false, http_client: nil) ⇒ RestApi
Returns a new instance of RestApi.
77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/mt/wall/transport/rest_api.rb', line 77 def initialize(host:, user: nil, password: nil, port: 443, verify_tls: true, # rubocop:disable Metrics/ParameterLists ca_file: nil, tls_fingerprint: nil, insecure_http: false, http_client: nil) super() @host = host @user = user @password = password @port = port @verify_tls = verify_tls @ca_file = ca_file @tls_fingerprint = tls_fingerprint @insecure_http = insecure_http @http_client = http_client end |
Class Method Details
.assert_unique_env_prefixes!(device_names) ⇒ void
This method returns an undefined value.
Guard: distinct device names must not collapse to the same ENV prefix, which would silently share one set of credentials. Fails fast.
141 142 143 144 145 146 147 148 |
# File 'lib/mt/wall/transport/rest_api.rb', line 141 def self.assert_unique_env_prefixes!(device_names) by_prefix = device_names.group_by { |name| env_prefix(name) } collisions = by_prefix.select { |_prefix, names| names.uniq.size > 1 } return if collisions.empty? detail = collisions.map { |prefix, names| "#{names.uniq.inspect} -> #{prefix}" }.join("; ") raise TransportError, "device names collide on credential ENV prefix: #{detail}" end |
.credentials_for(device_name) ⇒ Hash{Symbol=>String}
Derive the canonical ENV var names and read credentials for a device. “edge-1” -> MT_WALL_EDGE_1_USER / MT_WALL_EDGE_1_PASSWORD. A missing or empty expected var fails fast with TransportError.
123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/mt/wall/transport/rest_api.rb', line 123 def self.credentials_for(device_name) prefix = env_prefix(device_name) user = ENV.fetch("#{prefix}_USER", nil) password = ENV.fetch("#{prefix}_PASSWORD", nil) missing = [] missing << "#{prefix}_USER" if user.nil? || user.empty? missing << "#{prefix}_PASSWORD" if password.nil? || password.empty? unless missing.empty? raise TransportError, "missing credentials for device #{device_name.inspect}: set #{missing.join(' and ')}" end { user: user, password: password } end |
.env_prefix(device_name) ⇒ String
Canonical ENV var prefix for a device name: upcased, every run of non- collapsed to a single “_”. “edge-1” -> “MT_WALL_EDGE_1”.
112 113 114 115 |
# File 'lib/mt/wall/transport/rest_api.rb', line 112 def self.env_prefix(device_name) slug = device_name.to_s.upcase.gsub(/[^A-Z0-9]+/, "_").gsub(/\A_+|_+\z/, "") "MT_WALL_#{slug}" end |
.for_device(device, http_client: nil) ⇒ RestApi
Build a RestApi for a Model::Device, reading credentials from ENV and non-secret connection options from ‘device.options`.
97 98 99 100 101 102 103 104 105 106 |
# File 'lib/mt/wall/transport/rest_api.rb', line 97 def self.for_device(device, http_client: nil) creds = credentials_for(device.name) opts = device. insecure = opts.fetch(:insecure_http, false) new(host: device.host, user: creds[:user], password: creds[:password], port: opts.fetch(:port) { insecure ? PLAINTEXT_PORT : 443 }, verify_tls: opts.fetch(:verify_tls, true), ca_file: opts[:ca_file], tls_fingerprint: opts[:tls_fingerprint], insecure_http: insecure, http_client: http_client) end |
.rest_key(symbol) ⇒ String
Ruby underscore symbol -> RouterOS hyphenated REST key.
154 155 156 |
# File 'lib/mt/wall/transport/rest_api.rb', line 154 def self.rest_key(symbol) symbol.to_s.tr("_", "-") end |
.ruby_key(string) ⇒ Symbol
RouterOS hyphenated REST key -> Ruby underscore symbol.
160 161 162 |
# File 'lib/mt/wall/transport/rest_api.rb', line 160 def self.ruby_key(string) string.to_s.tr("-", "_").to_sym end |
Instance Method Details
#apply(operations) ⇒ void
This method returns an undefined value.
174 175 176 177 178 179 180 |
# File 'lib/mt/wall/transport/rest_api.rb', line 174 def apply(operations) @id_index = build_id_index(operations) operations.each { |operation| dispatch(operation) } nil ensure @id_index = nil end |
#arm_auto_revert(_snapshot, timeout:) ⇒ String
Back up the box and arm a device-side scheduler that restores the backup after ‘timeout`. The backup is taken BEFORE the scheduler/script are created, so a fired revert (which reboots into the backup) also discards the revert machinery itself. Returns the scheduler name.
189 190 191 192 193 194 195 196 197 |
# File 'lib/mt/wall/transport/rest_api.rb', line 189 def arm_auto_revert(_snapshot, timeout:) name = revert_name request(:post, rest_path("/system/backup/save"), { name: name }) request(:put, rest_path("/system/script"), { name: name, "dont-require-permissions": "yes", source: revert_source(name) }) request(:put, rest_path("/system/scheduler"), { name: name, interval: "#{timeout}s", "start-time": "startup", "on-event": name }) name end |
#confirm(handle) ⇒ void
This method returns an undefined value.
Cancel an armed revert: delete the scheduler and its restore script. Idempotent — missing rows are ignored.
203 204 205 206 207 |
# File 'lib/mt/wall/transport/rest_api.rb', line 203 def confirm(handle) delete_named("/system/scheduler", handle) delete_named("/system/script", handle) nil end |
#fetch(paths, managed_list_names: []) ⇒ DesiredState
167 168 169 170 |
# File 'lib/mt/wall/transport/rest_api.rb', line 167 def fetch(paths, managed_list_names: []) raw = paths.to_h { |path| [path, get_rows(path)] } DesiredState.from_current(raw, managed_list_names: managed_list_names) end |
#inspect ⇒ String Also known as: to_s
Redacted representation — the password is NEVER rendered.
211 212 213 214 |
# File 'lib/mt/wall/transport/rest_api.rb', line 211 def inspect "#<#{self.class.name} host=#{@host.inspect} port=#{@port} " \ "user=#{@user.inspect} secure=#{secure?}>" end |