opn_api 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE +25 -0
- data/README.md +979 -0
- data/bin/opn-api +7 -0
- data/lib/opn_api/cli/commands/api.rb +57 -0
- data/lib/opn_api/cli/commands/backup.rb +38 -0
- data/lib/opn_api/cli/commands/base.rb +50 -0
- data/lib/opn_api/cli/commands/device.rb +53 -0
- data/lib/opn_api/cli/commands/plugin.rb +67 -0
- data/lib/opn_api/cli/commands/reconfigure.rb +40 -0
- data/lib/opn_api/cli/commands/resource.rb +248 -0
- data/lib/opn_api/cli/formatter.rb +198 -0
- data/lib/opn_api/cli/main.rb +160 -0
- data/lib/opn_api/client.rb +204 -0
- data/lib/opn_api/config.rb +111 -0
- data/lib/opn_api/errors.rb +45 -0
- data/lib/opn_api/id_resolver.rb +222 -0
- data/lib/opn_api/logger.rb +29 -0
- data/lib/opn_api/normalize.rb +47 -0
- data/lib/opn_api/resource.rb +142 -0
- data/lib/opn_api/resource_registry.rb +377 -0
- data/lib/opn_api/service_reconfigure.rb +293 -0
- data/lib/opn_api/version.rb +5 -0
- data/lib/opn_api.rb +32 -0
- metadata +73 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpnApi
|
|
4
|
+
# Base error class for all opn_api exceptions.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when a network connection cannot be established
|
|
8
|
+
# (e.g. ECONNREFUSED, EHOSTUNREACH, ETIMEDOUT).
|
|
9
|
+
class ConnectionError < Error; end
|
|
10
|
+
|
|
11
|
+
# Raised when an HTTP request times out (open_timeout or read_timeout).
|
|
12
|
+
class TimeoutError < Error; end
|
|
13
|
+
|
|
14
|
+
# Raised when the OPNsense API returns a non-2xx HTTP response.
|
|
15
|
+
class ApiError < Error
|
|
16
|
+
# @return [Integer] HTTP status code
|
|
17
|
+
attr_reader :code
|
|
18
|
+
|
|
19
|
+
# @return [String] HTTP response body
|
|
20
|
+
attr_reader :body
|
|
21
|
+
|
|
22
|
+
# @return [String] Request URI that caused the error
|
|
23
|
+
attr_reader :uri
|
|
24
|
+
|
|
25
|
+
# @param message [String]
|
|
26
|
+
# @param code [Integer] HTTP status code
|
|
27
|
+
# @param body [String] Response body
|
|
28
|
+
# @param uri [String] Request URI
|
|
29
|
+
def initialize(message, code:, body:, uri:)
|
|
30
|
+
@code = code
|
|
31
|
+
@body = body
|
|
32
|
+
@uri = uri
|
|
33
|
+
super(message)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Raised when a configuration file is missing or malformed.
|
|
38
|
+
class ConfigError < Error; end
|
|
39
|
+
|
|
40
|
+
# Raised when IdResolver cannot translate a name to an ID or vice versa.
|
|
41
|
+
class ResolveError < Error; end
|
|
42
|
+
|
|
43
|
+
# Raised when a configtest returns ALERT (e.g. HAProxy configtest).
|
|
44
|
+
class ConfigTestError < Error; end
|
|
45
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpnApi
|
|
4
|
+
# Resolves ModelRelationField UUIDs/IDs <-> names for OPNsense resources.
|
|
5
|
+
#
|
|
6
|
+
# Maintains a per-run class-level cache keyed by
|
|
7
|
+
# "device:endpoint:id_field:name_field" to avoid redundant API calls.
|
|
8
|
+
#
|
|
9
|
+
# Supports:
|
|
10
|
+
# - Standard UUID fields (ModelRelationField) — id_field: 'uuid', name_field: 'name'
|
|
11
|
+
# - Certificate fields (CertificateField) — id_field: 'refid', name_field: 'descr'
|
|
12
|
+
# - Cron job fields — id_field: 'uuid', name_field: 'description'
|
|
13
|
+
# - Dot-path fields for nested configs — e.g. 'general.stats.allowedUsers'
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# relation_fields = {
|
|
17
|
+
# 'linkedServers' => { endpoint: 'haproxy/settings/search_servers', multiple: true },
|
|
18
|
+
# 'sslCA' => { endpoint: 'trust/ca/search', id_field: 'refid', name_field: 'descr' },
|
|
19
|
+
# }
|
|
20
|
+
# translated = OpnApi::IdResolver.translate_to_names(client, 'myfw', relation_fields, config)
|
|
21
|
+
module IdResolver
|
|
22
|
+
UUID_RE = %r{\A[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\z}
|
|
23
|
+
|
|
24
|
+
@cache = {}
|
|
25
|
+
|
|
26
|
+
# Queries the given endpoint once and builds id<->name maps.
|
|
27
|
+
# Subsequent calls for the same cache key are no-ops.
|
|
28
|
+
#
|
|
29
|
+
# @param client [OpnApi::Client]
|
|
30
|
+
# @param device [String]
|
|
31
|
+
# @param endpoint [String]
|
|
32
|
+
# @param id_field [String] Field containing the ID (default: 'uuid')
|
|
33
|
+
# @param name_field [String] Field containing the display name (default: 'name')
|
|
34
|
+
# @param method [Symbol] HTTP method to use (default: :post)
|
|
35
|
+
def self.populate(client, device, endpoint, id_field: 'uuid', name_field: 'name', method: :post)
|
|
36
|
+
key = cache_key(device, endpoint, id_field, name_field)
|
|
37
|
+
return if @cache.key?(key)
|
|
38
|
+
|
|
39
|
+
id_to_name = {}
|
|
40
|
+
name_to_id = {}
|
|
41
|
+
begin
|
|
42
|
+
response = method == :get ? client.get(endpoint) : client.post(endpoint, {})
|
|
43
|
+
rows = response['rows'] || []
|
|
44
|
+
rows.each do |row|
|
|
45
|
+
id = row[id_field].to_s
|
|
46
|
+
name = row[name_field].to_s
|
|
47
|
+
next if id.empty? || name.empty?
|
|
48
|
+
|
|
49
|
+
id_to_name[id] = name
|
|
50
|
+
name_to_id[name] = id
|
|
51
|
+
end
|
|
52
|
+
rescue OpnApi::Error => e
|
|
53
|
+
OpnApi.logger.warning(
|
|
54
|
+
"IdResolver: failed to populate '#{endpoint}' " \
|
|
55
|
+
"for '#{device}': #{e.message}",
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@cache[key] = { id_to_name: id_to_name, name_to_id: name_to_id }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns a deep copy of +config+ with each relation-field value translated
|
|
63
|
+
# from IDs to names. Falls back to the original value if not found.
|
|
64
|
+
# Supports dot-path field names for nested config hashes.
|
|
65
|
+
#
|
|
66
|
+
# @param client [OpnApi::Client]
|
|
67
|
+
# @param device [String]
|
|
68
|
+
# @param relation_fields [Hash] field_name => { endpoint:, multiple:, ... }
|
|
69
|
+
# @param config [Hash]
|
|
70
|
+
# @return [Hash]
|
|
71
|
+
def self.translate_to_names(client, device, relation_fields, config)
|
|
72
|
+
result = deep_dup(config)
|
|
73
|
+
relation_fields.each do |field, opts|
|
|
74
|
+
id_field = opts[:id_field] || 'uuid'
|
|
75
|
+
name_field = opts[:name_field] || 'name'
|
|
76
|
+
http_method = opts[:method] || :post
|
|
77
|
+
|
|
78
|
+
parent, last_key = dig_path(result, field)
|
|
79
|
+
next unless parent
|
|
80
|
+
|
|
81
|
+
value = parent[last_key]
|
|
82
|
+
next if value.nil? || value.to_s.empty?
|
|
83
|
+
|
|
84
|
+
populate(client, device, opts[:endpoint],
|
|
85
|
+
id_field: id_field, name_field: name_field, method: http_method)
|
|
86
|
+
entry = @cache[cache_key(device, opts[:endpoint], id_field, name_field)] || {}
|
|
87
|
+
map = entry[:id_to_name] || {}
|
|
88
|
+
|
|
89
|
+
parent[last_key] = if opts[:multiple]
|
|
90
|
+
value.to_s.split(',').map do |item|
|
|
91
|
+
item = item.strip
|
|
92
|
+
map[item] || item
|
|
93
|
+
end.join(',')
|
|
94
|
+
else
|
|
95
|
+
map[value.to_s] || value.to_s
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
result
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Returns a deep copy of +config+ with each relation-field value translated
|
|
102
|
+
# from names to IDs. Raises ResolveError if a name cannot be resolved.
|
|
103
|
+
# Values that are already valid IDs pass through unchanged.
|
|
104
|
+
# Supports dot-path field names for nested config hashes.
|
|
105
|
+
#
|
|
106
|
+
# @param client [OpnApi::Client]
|
|
107
|
+
# @param device [String]
|
|
108
|
+
# @param relation_fields [Hash] field_name => { endpoint:, multiple:, ... }
|
|
109
|
+
# @param config [Hash]
|
|
110
|
+
# @return [Hash]
|
|
111
|
+
def self.translate_to_uuids(client, device, relation_fields, config)
|
|
112
|
+
result = deep_dup(config)
|
|
113
|
+
relation_fields.each do |field, opts|
|
|
114
|
+
id_field = opts[:id_field] || 'uuid'
|
|
115
|
+
name_field = opts[:name_field] || 'name'
|
|
116
|
+
http_method = opts[:method] || :post
|
|
117
|
+
|
|
118
|
+
parent, last_key = dig_path(result, field)
|
|
119
|
+
next unless parent
|
|
120
|
+
|
|
121
|
+
value = parent[last_key]
|
|
122
|
+
next if value.nil? || value.to_s.empty?
|
|
123
|
+
|
|
124
|
+
key = cache_key(device, opts[:endpoint], id_field, name_field)
|
|
125
|
+
populate(client, device, opts[:endpoint],
|
|
126
|
+
id_field: id_field, name_field: name_field, method: http_method)
|
|
127
|
+
entry = @cache[key] || {}
|
|
128
|
+
id_to_name = entry[:id_to_name] || {}
|
|
129
|
+
|
|
130
|
+
parent[last_key] = if opts[:multiple]
|
|
131
|
+
value.to_s.split(',').map do |item|
|
|
132
|
+
item = item.strip
|
|
133
|
+
# Already a known ID — pass through
|
|
134
|
+
next item if id_to_name.key?(item)
|
|
135
|
+
# Looks like a UUID — pass through
|
|
136
|
+
next item if UUID_RE.match?(item)
|
|
137
|
+
|
|
138
|
+
resolve_with_retry(
|
|
139
|
+
client, device, opts[:endpoint],
|
|
140
|
+
id_field, name_field, http_method, item
|
|
141
|
+
)
|
|
142
|
+
end.join(',')
|
|
143
|
+
else
|
|
144
|
+
str = value.to_s
|
|
145
|
+
if id_to_name.key?(str) || UUID_RE.match?(str)
|
|
146
|
+
str
|
|
147
|
+
else
|
|
148
|
+
resolve_with_retry(
|
|
149
|
+
client, device, opts[:endpoint],
|
|
150
|
+
id_field, name_field, http_method, str
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
result
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Clears all cached mappings. Call between runs or in tests.
|
|
159
|
+
def self.reset!
|
|
160
|
+
@cache.clear
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Navigates a dotted field path in a hash and returns [parent, last_key].
|
|
164
|
+
# For 'stats.allowedUsers' on { 'stats' => { 'allowedUsers' => 'x' } }
|
|
165
|
+
# returns [{ 'allowedUsers' => 'x' }, 'allowedUsers'].
|
|
166
|
+
# For simple fields like 'defaultBackend', returns [hash, 'defaultBackend'].
|
|
167
|
+
# Returns [nil, nil] if any intermediate key is missing.
|
|
168
|
+
def self.dig_path(hash, dotted_field)
|
|
169
|
+
parts = dotted_field.split('.')
|
|
170
|
+
parent = hash
|
|
171
|
+
parts[0..-2].each do |part|
|
|
172
|
+
parent = parent[part]
|
|
173
|
+
return [nil, nil] unless parent.is_a?(Hash)
|
|
174
|
+
end
|
|
175
|
+
[parent, parts.last]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Deep-duplicates a Hash/Array structure.
|
|
179
|
+
def self.deep_dup(obj)
|
|
180
|
+
case obj
|
|
181
|
+
when Hash
|
|
182
|
+
obj.each_with_object({}) { |(k, v), h| h[k] = deep_dup(v) }
|
|
183
|
+
when Array
|
|
184
|
+
obj.map { |v| deep_dup(v) }
|
|
185
|
+
else
|
|
186
|
+
obj
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Builds a cache key from device, endpoint, id_field, and name_field.
|
|
191
|
+
def self.cache_key(device, endpoint, id_field, name_field)
|
|
192
|
+
"#{device}:#{endpoint}:#{id_field}:#{name_field}"
|
|
193
|
+
end
|
|
194
|
+
private_class_method :cache_key
|
|
195
|
+
|
|
196
|
+
# Attempts to resolve a name to an ID, retrying once with a cache refresh.
|
|
197
|
+
# Raises ResolveError if the name cannot be found after retry.
|
|
198
|
+
def self.resolve_with_retry(client, device, endpoint, id_field, name_field, http_method, name)
|
|
199
|
+
key = cache_key(device, endpoint, id_field, name_field)
|
|
200
|
+
entry = @cache[key] || {}
|
|
201
|
+
name_to_id = entry[:name_to_id] || {}
|
|
202
|
+
|
|
203
|
+
resolved = name_to_id[name]
|
|
204
|
+
unless resolved
|
|
205
|
+
# Cache miss — refresh and try once more
|
|
206
|
+
@cache.delete(key)
|
|
207
|
+
populate(client, device, endpoint,
|
|
208
|
+
id_field: id_field, name_field: name_field, method: http_method)
|
|
209
|
+
entry = @cache[key] || {}
|
|
210
|
+
name_to_id = entry[:name_to_id] || {}
|
|
211
|
+
resolved = name_to_id[name]
|
|
212
|
+
end
|
|
213
|
+
unless resolved
|
|
214
|
+
raise OpnApi::ResolveError,
|
|
215
|
+
"IdResolver: cannot resolve '#{name}' to an ID " \
|
|
216
|
+
"via '#{endpoint}' on '#{device}'"
|
|
217
|
+
end
|
|
218
|
+
resolved
|
|
219
|
+
end
|
|
220
|
+
private_class_method :resolve_with_retry
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpnApi
|
|
4
|
+
# Pluggable logger with five severity levels.
|
|
5
|
+
#
|
|
6
|
+
# Default: writes to $stderr at :info level. For puppet-opn integration,
|
|
7
|
+
# replace with a logger that delegates to Puppet.debug/notice/warning/err.
|
|
8
|
+
#
|
|
9
|
+
# @example Custom logger for Puppet integration
|
|
10
|
+
# OpnApi.logger = PuppetLogger.new
|
|
11
|
+
class Logger
|
|
12
|
+
LEVELS = %i[debug info notice warning error].freeze
|
|
13
|
+
|
|
14
|
+
# @param output [IO] Output stream (default: $stderr)
|
|
15
|
+
# @param level [Symbol] Minimum severity to log (default: :info)
|
|
16
|
+
def initialize(output: $stderr, level: :info)
|
|
17
|
+
@output = output
|
|
18
|
+
@level = LEVELS.index(level) || 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
LEVELS.each_with_index do |name, idx|
|
|
22
|
+
define_method(name) do |msg|
|
|
23
|
+
return if idx < @level
|
|
24
|
+
|
|
25
|
+
@output.puts("[opn_api] #{name.upcase}: #{msg}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpnApi
|
|
4
|
+
# Normalization of OPNsense selection hashes.
|
|
5
|
+
#
|
|
6
|
+
# OPNsense returns multi-select/dropdown fields as hashes like:
|
|
7
|
+
# { "opt1" => { "value" => "Option 1", "selected" => 1 },
|
|
8
|
+
# "opt2" => { "value" => "Option 2", "selected" => 0 } }
|
|
9
|
+
#
|
|
10
|
+
# This module collapses them to comma-separated strings of selected
|
|
11
|
+
# keys (e.g. "opt1") and recurses into nested hashes.
|
|
12
|
+
module Normalize
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Recursively normalizes OPNsense selection hashes to simple values.
|
|
16
|
+
#
|
|
17
|
+
# @param obj [Object] The value to normalize
|
|
18
|
+
# @return [Object] Normalized value
|
|
19
|
+
def normalize_config(obj)
|
|
20
|
+
return obj unless obj.is_a?(Hash)
|
|
21
|
+
return normalize_selection(obj) if selection_hash?(obj)
|
|
22
|
+
|
|
23
|
+
obj.transform_values { |v| normalize_config(v) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Detects whether a hash is an OPNsense selection hash.
|
|
27
|
+
#
|
|
28
|
+
# A selection hash has non-empty entries where every value is a Hash
|
|
29
|
+
# containing at least 'value' and 'selected' keys.
|
|
30
|
+
#
|
|
31
|
+
# @param hash [Hash]
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def selection_hash?(hash)
|
|
34
|
+
hash.is_a?(Hash) &&
|
|
35
|
+
!hash.empty? &&
|
|
36
|
+
hash.values.all? { |v| v.is_a?(Hash) && v.key?('value') && v.key?('selected') }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Collapses a selection hash to a comma-separated string of selected keys.
|
|
40
|
+
#
|
|
41
|
+
# @param hash [Hash] A selection hash (as detected by selection_hash?)
|
|
42
|
+
# @return [String] Comma-separated selected keys
|
|
43
|
+
def normalize_selection(hash)
|
|
44
|
+
hash.select { |_k, v| v['selected'].to_i == 1 }.keys.join(',')
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpnApi
|
|
4
|
+
# Generic CRUD wrapper for OPNsense API resources.
|
|
5
|
+
#
|
|
6
|
+
# Supports three resource patterns:
|
|
7
|
+
#
|
|
8
|
+
# 1. Standard CRUD (search/get/add/set/del with UUID)
|
|
9
|
+
# 2. Singleton settings (GET get / POST set, no UUID, no search/add/del)
|
|
10
|
+
# 3. Resources with GET-based search (e.g. snapshots, trust_crl)
|
|
11
|
+
#
|
|
12
|
+
# @example Via ResourceRegistry (preferred)
|
|
13
|
+
# res = OpnApi::ResourceRegistry.build(client, 'haproxy_server')
|
|
14
|
+
# res.search
|
|
15
|
+
#
|
|
16
|
+
# @example Direct with explicit paths
|
|
17
|
+
# res = OpnApi::Resource.new(
|
|
18
|
+
# client: client,
|
|
19
|
+
# base_path: 'haproxy/settings',
|
|
20
|
+
# search_action: 'search_servers',
|
|
21
|
+
# crud_action: '%{action}_server',
|
|
22
|
+
# )
|
|
23
|
+
#
|
|
24
|
+
# @example Singleton settings resource
|
|
25
|
+
# res = OpnApi::Resource.new(
|
|
26
|
+
# client: client,
|
|
27
|
+
# base_path: 'zabbixagent/settings',
|
|
28
|
+
# search_action: 'get',
|
|
29
|
+
# crud_action: '%{action}',
|
|
30
|
+
# singleton: true,
|
|
31
|
+
# search_method: :get,
|
|
32
|
+
# )
|
|
33
|
+
# config = res.show_settings # GET zabbixagent/settings/get
|
|
34
|
+
# res.update_settings(config) # POST zabbixagent/settings/set
|
|
35
|
+
class Resource
|
|
36
|
+
attr_reader :singleton
|
|
37
|
+
|
|
38
|
+
# @param client [OpnApi::Client] API client instance
|
|
39
|
+
# @param base_path [String] Base path (e.g. 'haproxy/settings')
|
|
40
|
+
# @param search_action [String] Search action name (e.g. 'search_servers')
|
|
41
|
+
# @param crud_action [String] CRUD action template with %{action} placeholder
|
|
42
|
+
# @param singleton [Boolean] True for settings resources (no UUID, no search/add/del)
|
|
43
|
+
# @param search_method [Symbol] HTTP method for search (:post or :get)
|
|
44
|
+
# @param module_name [String] Legacy: OPNsense module (e.g. 'firewall')
|
|
45
|
+
# @param controller [String] Legacy: OPNsense controller (e.g. 'alias')
|
|
46
|
+
# @param resource_type [String] Legacy: Resource type (e.g. 'item')
|
|
47
|
+
def initialize(client:, base_path: nil, search_action: nil, crud_action: nil,
|
|
48
|
+
singleton: false, search_method: :post,
|
|
49
|
+
module_name: nil, controller: nil, resource_type: nil)
|
|
50
|
+
@client = client
|
|
51
|
+
@singleton = singleton
|
|
52
|
+
@search_method = search_method
|
|
53
|
+
|
|
54
|
+
if base_path
|
|
55
|
+
# Explicit path mode
|
|
56
|
+
@base_path = base_path
|
|
57
|
+
@search_action = search_action
|
|
58
|
+
@crud_action = crud_action
|
|
59
|
+
else
|
|
60
|
+
# Legacy module/controller/type mode
|
|
61
|
+
@base_path = "#{module_name}/#{controller}"
|
|
62
|
+
suffix = resource_type.to_s.empty? ? '' : "_#{resource_type}"
|
|
63
|
+
@search_action = "search#{suffix}"
|
|
64
|
+
@crud_action = "%{action}#{suffix}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Searches for resources matching the given parameters.
|
|
69
|
+
#
|
|
70
|
+
# @param params [Hash] Search parameters (passed as POST body, ignored for GET)
|
|
71
|
+
# @return [Object] Search results (Array<Hash> for standard, Hash for singletons)
|
|
72
|
+
def search(params = {})
|
|
73
|
+
path = "#{@base_path}/#{@search_action}"
|
|
74
|
+
result = if @search_method == :get
|
|
75
|
+
@client.get(path)
|
|
76
|
+
else
|
|
77
|
+
@client.post(path, params)
|
|
78
|
+
end
|
|
79
|
+
# Singletons return the full response hash (settings structure).
|
|
80
|
+
# All other searches extract the rows array.
|
|
81
|
+
return result if @singleton
|
|
82
|
+
|
|
83
|
+
result.is_a?(Hash) ? (result['rows'] || []) : result
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Retrieves a single resource by UUID.
|
|
87
|
+
#
|
|
88
|
+
# @param uuid [String]
|
|
89
|
+
# @return [Hash] Resource data
|
|
90
|
+
def get(uuid)
|
|
91
|
+
@client.get("#{@base_path}/#{crud_path('get')}/#{uuid}")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Retrieves a singleton settings resource (no UUID).
|
|
95
|
+
#
|
|
96
|
+
# @return [Hash] Resource data
|
|
97
|
+
def show_settings
|
|
98
|
+
@client.get("#{@base_path}/#{crud_path('get')}")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Creates a new resource.
|
|
102
|
+
#
|
|
103
|
+
# @param config [Hash] Resource configuration (wrapped in wrapper key)
|
|
104
|
+
# @return [Hash] API response (typically includes 'uuid' on success)
|
|
105
|
+
def add(config)
|
|
106
|
+
@client.post("#{@base_path}/#{crud_path('add')}", config)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Updates an existing resource.
|
|
110
|
+
#
|
|
111
|
+
# @param uuid [String]
|
|
112
|
+
# @param config [Hash] Resource configuration
|
|
113
|
+
# @return [Hash] API response
|
|
114
|
+
def set(uuid, config)
|
|
115
|
+
@client.post("#{@base_path}/#{crud_path('set')}/#{uuid}", config)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Updates a singleton settings resource (no UUID).
|
|
119
|
+
#
|
|
120
|
+
# @param config [Hash] Resource configuration
|
|
121
|
+
# @return [Hash] API response
|
|
122
|
+
def update_settings(config)
|
|
123
|
+
@client.post("#{@base_path}/#{crud_path('set')}", config)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Deletes a resource by UUID.
|
|
127
|
+
#
|
|
128
|
+
# @param uuid [String]
|
|
129
|
+
# @return [Hash] API response
|
|
130
|
+
def del(uuid)
|
|
131
|
+
@client.post("#{@base_path}/#{crud_path('del')}/#{uuid}", {})
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
# Builds a CRUD action path from the template.
|
|
137
|
+
# e.g. crud_path('get') with template '%{action}_server' → 'get_server'
|
|
138
|
+
def crud_path(action)
|
|
139
|
+
format(@crud_action, action: action)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|