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.
@@ -0,0 +1,377 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpnApi
4
+ # Registry of known OPNsense resource types with their exact API endpoints.
5
+ #
6
+ # OPNsense has no consistent endpoint naming convention — some use snake_case
7
+ # with plural search (search_servers/add_server), others use camelCase
8
+ # (searchConnection/addConnection), and some use bare names (search/add).
9
+ # This registry maps user-friendly resource names to the correct API paths.
10
+ #
11
+ # Resource names follow the puppet-opn naming convention (opn_ prefix stripped).
12
+ #
13
+ # Each entry defines:
14
+ # - base_path: module/controller prefix (e.g. 'haproxy/settings')
15
+ # - search_action: full action name for search (e.g. 'search_servers')
16
+ # - crud_action: template for get/add/set/del — `%{action}` is replaced
17
+ # (e.g. '%{action}_server' → 'get_server', 'add_server')
18
+ # - wrapper: POST body wrapper key (e.g. 'server')
19
+ # - singleton: true for settings resources (GET get / POST set, no UUID)
20
+ # - search_method: :get for resources using GET instead of POST for search
21
+ # - response_dig: keys to extract from GET response after unwrapping.
22
+ # Single-element array → dig into that sub-key (e.g. ['settings']).
23
+ # Multi-element array → slice those keys (e.g. ['general', 'maintenance']).
24
+ # - response_reject: keys to exclude from GET response (e.g. sub-resource data).
25
+ #
26
+ # @example
27
+ # entry = OpnApi::ResourceRegistry.lookup('haproxy_server')
28
+ # entry[:base_path] # => 'haproxy/settings'
29
+ # entry[:search_action] # => 'search_servers'
30
+ module ResourceRegistry
31
+ # Resource definitions keyed by puppet-opn name (without opn_ prefix).
32
+ RESOURCES = {
33
+ # --- ACME Client ---
34
+ 'acmeclient_account' => {
35
+ base_path: 'acmeclient/accounts', search_action: 'search',
36
+ crud_action: '%{action}', wrapper: 'account'
37
+ },
38
+ 'acmeclient_action' => {
39
+ base_path: 'acmeclient/actions', search_action: 'search',
40
+ crud_action: '%{action}', wrapper: 'action'
41
+ },
42
+ 'acmeclient_certificate' => {
43
+ base_path: 'acmeclient/certificates', search_action: 'search',
44
+ crud_action: '%{action}', wrapper: 'certificate'
45
+ },
46
+ 'acmeclient_settings' => {
47
+ base_path: 'acmeclient/settings', search_action: 'get',
48
+ crud_action: '%{action}', wrapper: 'acmeclient',
49
+ singleton: true, search_method: :get,
50
+ response_dig: ['settings']
51
+ },
52
+ 'acmeclient_validation' => {
53
+ base_path: 'acmeclient/validations', search_action: 'search',
54
+ crud_action: '%{action}', wrapper: 'validation'
55
+ },
56
+ # --- Cron ---
57
+ 'cron' => {
58
+ base_path: 'cron/settings', search_action: 'search_jobs',
59
+ crud_action: '%{action}_job', wrapper: 'job'
60
+ },
61
+ # --- DHC Relay ---
62
+ 'dhcrelay' => {
63
+ base_path: 'dhcrelay/settings', search_action: 'search_relay',
64
+ crud_action: '%{action}_relay', wrapper: 'relay'
65
+ },
66
+ 'dhcrelay_destination' => {
67
+ base_path: 'dhcrelay/settings', search_action: 'search_dest',
68
+ crud_action: '%{action}_dest', wrapper: 'destination'
69
+ },
70
+ # --- Firewall ---
71
+ 'firewall_alias' => {
72
+ base_path: 'firewall/alias', search_action: 'search_item',
73
+ crud_action: '%{action}_item', wrapper: 'alias'
74
+ },
75
+ 'firewall_category' => {
76
+ base_path: 'firewall/category', search_action: 'search_item',
77
+ crud_action: '%{action}_item', wrapper: 'category'
78
+ },
79
+ 'firewall_group' => {
80
+ base_path: 'firewall/group', search_action: 'search_item',
81
+ crud_action: '%{action}_item', wrapper: 'group'
82
+ },
83
+ 'firewall_rule' => {
84
+ base_path: 'firewall/filter', search_action: 'search_rule',
85
+ crud_action: '%{action}_rule', wrapper: 'rule'
86
+ },
87
+ # --- Gateway ---
88
+ 'gateway' => {
89
+ base_path: 'routing/settings', search_action: 'searchGateway',
90
+ crud_action: '%{action}Gateway', wrapper: 'gateway_item'
91
+ },
92
+ # --- Group (auth) ---
93
+ 'group' => {
94
+ base_path: 'auth/group', search_action: 'search',
95
+ crud_action: '%{action}', wrapper: 'group'
96
+ },
97
+ # --- HAProxy ---
98
+ 'haproxy_acl' => {
99
+ base_path: 'haproxy/settings', search_action: 'search_acls',
100
+ crud_action: '%{action}_acl', wrapper: 'acl'
101
+ },
102
+ 'haproxy_action' => {
103
+ base_path: 'haproxy/settings', search_action: 'search_actions',
104
+ crud_action: '%{action}_action', wrapper: 'action'
105
+ },
106
+ 'haproxy_backend' => {
107
+ base_path: 'haproxy/settings', search_action: 'search_backends',
108
+ crud_action: '%{action}_backend', wrapper: 'backend'
109
+ },
110
+ 'haproxy_cpu' => {
111
+ base_path: 'haproxy/settings', search_action: 'search_cpus',
112
+ crud_action: '%{action}_cpu', wrapper: 'cpu'
113
+ },
114
+ 'haproxy_errorfile' => {
115
+ base_path: 'haproxy/settings', search_action: 'search_errorfiles',
116
+ crud_action: '%{action}_errorfile', wrapper: 'errorfile'
117
+ },
118
+ 'haproxy_fcgi' => {
119
+ base_path: 'haproxy/settings', search_action: 'search_fcgis',
120
+ crud_action: '%{action}_fcgi', wrapper: 'fcgi'
121
+ },
122
+ 'haproxy_frontend' => {
123
+ base_path: 'haproxy/settings', search_action: 'search_frontends',
124
+ crud_action: '%{action}_frontend', wrapper: 'frontend'
125
+ },
126
+ 'haproxy_group' => {
127
+ base_path: 'haproxy/settings', search_action: 'search_groups',
128
+ crud_action: '%{action}_group', wrapper: 'group'
129
+ },
130
+ 'haproxy_healthcheck' => {
131
+ base_path: 'haproxy/settings', search_action: 'search_healthchecks',
132
+ crud_action: '%{action}_healthcheck', wrapper: 'healthcheck'
133
+ },
134
+ 'haproxy_lua' => {
135
+ base_path: 'haproxy/settings', search_action: 'search_luas',
136
+ crud_action: '%{action}_lua', wrapper: 'lua'
137
+ },
138
+ 'haproxy_mailer' => {
139
+ base_path: 'haproxy/settings', search_action: 'searchmailers',
140
+ crud_action: '%{action}mailer', wrapper: 'mailer'
141
+ },
142
+ 'haproxy_mapfile' => {
143
+ base_path: 'haproxy/settings', search_action: 'search_mapfiles',
144
+ crud_action: '%{action}_mapfile', wrapper: 'mapfile'
145
+ },
146
+ 'haproxy_resolver' => {
147
+ base_path: 'haproxy/settings', search_action: 'searchresolvers',
148
+ crud_action: '%{action}resolver', wrapper: 'resolver'
149
+ },
150
+ 'haproxy_server' => {
151
+ base_path: 'haproxy/settings', search_action: 'search_servers',
152
+ crud_action: '%{action}_server', wrapper: 'server'
153
+ },
154
+ 'haproxy_settings' => {
155
+ base_path: 'haproxy/settings', search_action: 'get',
156
+ crud_action: '%{action}', wrapper: 'haproxy',
157
+ singleton: true, search_method: :get,
158
+ response_dig: %w[general maintenance]
159
+ },
160
+ 'haproxy_user' => {
161
+ base_path: 'haproxy/settings', search_action: 'search_users',
162
+ crud_action: '%{action}_user', wrapper: 'user'
163
+ },
164
+ # --- HA Sync ---
165
+ 'hasync' => {
166
+ base_path: 'core/hasync', search_action: 'get',
167
+ crud_action: '%{action}', wrapper: 'hasync',
168
+ singleton: true, search_method: :get
169
+ },
170
+ # --- IPsec ---
171
+ 'ipsec_child' => {
172
+ base_path: 'ipsec/connections', search_action: 'searchChild',
173
+ crud_action: '%{action}Child', wrapper: 'child'
174
+ },
175
+ 'ipsec_connection' => {
176
+ base_path: 'ipsec/connections', search_action: 'searchConnection',
177
+ crud_action: '%{action}Connection', wrapper: 'connection'
178
+ },
179
+ 'ipsec_keypair' => {
180
+ base_path: 'ipsec/key_pairs', search_action: 'search_item',
181
+ crud_action: '%{action}_item', wrapper: 'keyPair'
182
+ },
183
+ 'ipsec_local' => {
184
+ base_path: 'ipsec/connections', search_action: 'searchLocal',
185
+ crud_action: '%{action}Local', wrapper: 'local'
186
+ },
187
+ 'ipsec_pool' => {
188
+ base_path: 'ipsec/pools', search_action: 'search',
189
+ crud_action: '%{action}', wrapper: 'pool'
190
+ },
191
+ 'ipsec_presharedkey' => {
192
+ base_path: 'ipsec/pre_shared_keys', search_action: 'search_item',
193
+ crud_action: '%{action}_item', wrapper: 'preSharedKey'
194
+ },
195
+ 'ipsec_remote' => {
196
+ base_path: 'ipsec/connections', search_action: 'searchRemote',
197
+ crud_action: '%{action}Remote', wrapper: 'remote'
198
+ },
199
+ 'ipsec_settings' => {
200
+ base_path: 'ipsec/settings', search_action: 'get',
201
+ crud_action: '%{action}', wrapper: 'ipsec',
202
+ singleton: true, search_method: :get,
203
+ response_dig: %w[general charon]
204
+ },
205
+ 'ipsec_vti' => {
206
+ base_path: 'ipsec/vti', search_action: 'search',
207
+ crud_action: '%{action}', wrapper: 'vti'
208
+ },
209
+ # --- Kea DHCP ---
210
+ 'kea_ctrl_agent' => {
211
+ base_path: 'kea/ctrl_agent', search_action: 'get',
212
+ crud_action: '%{action}', wrapper: 'ctrlagent',
213
+ singleton: true, search_method: :get,
214
+ response_dig: ['general']
215
+ },
216
+ 'kea_dhcpv4' => {
217
+ base_path: 'kea/dhcpv4', search_action: 'get',
218
+ crud_action: '%{action}', wrapper: 'dhcpv4',
219
+ singleton: true, search_method: :get,
220
+ response_dig: %w[general lexpire ha]
221
+ },
222
+ 'kea_dhcpv4_peer' => {
223
+ base_path: 'kea/dhcpv4', search_action: 'searchPeer',
224
+ crud_action: '%{action}Peer', wrapper: 'peer'
225
+ },
226
+ 'kea_dhcpv4_reservation' => {
227
+ base_path: 'kea/dhcpv4', search_action: 'searchReservation',
228
+ crud_action: '%{action}Reservation', wrapper: 'reservation'
229
+ },
230
+ 'kea_dhcpv4_subnet' => {
231
+ base_path: 'kea/dhcpv4', search_action: 'searchSubnet',
232
+ crud_action: '%{action}Subnet', wrapper: 'subnet4'
233
+ },
234
+ 'kea_dhcpv6' => {
235
+ base_path: 'kea/dhcpv6', search_action: 'get',
236
+ crud_action: '%{action}', wrapper: 'dhcpv6',
237
+ singleton: true, search_method: :get,
238
+ response_dig: %w[general lexpire ha]
239
+ },
240
+ 'kea_dhcpv6_pd_pool' => {
241
+ base_path: 'kea/dhcpv6', search_action: 'searchPdPool',
242
+ crud_action: '%{action}PdPool', wrapper: 'pd_pool'
243
+ },
244
+ 'kea_dhcpv6_peer' => {
245
+ base_path: 'kea/dhcpv6', search_action: 'searchPeer',
246
+ crud_action: '%{action}Peer', wrapper: 'peer'
247
+ },
248
+ 'kea_dhcpv6_reservation' => {
249
+ base_path: 'kea/dhcpv6', search_action: 'searchReservation',
250
+ crud_action: '%{action}Reservation', wrapper: 'reservation'
251
+ },
252
+ 'kea_dhcpv6_subnet' => {
253
+ base_path: 'kea/dhcpv6', search_action: 'searchSubnet',
254
+ crud_action: '%{action}Subnet', wrapper: 'subnet6'
255
+ },
256
+ # --- Node Exporter ---
257
+ 'node_exporter' => {
258
+ base_path: 'nodeexporter/general', search_action: 'get',
259
+ crud_action: '%{action}', wrapper: 'general',
260
+ singleton: true, search_method: :get
261
+ },
262
+ # --- OpenVPN ---
263
+ 'openvpn_cso' => {
264
+ base_path: 'openvpn/client_overwrites', search_action: 'search',
265
+ crud_action: '%{action}', wrapper: 'cso'
266
+ },
267
+ 'openvpn_instance' => {
268
+ base_path: 'openvpn/instances', search_action: 'search',
269
+ crud_action: '%{action}', wrapper: 'instance'
270
+ },
271
+ 'openvpn_statickey' => {
272
+ base_path: 'openvpn/instances', search_action: 'search_static_key',
273
+ crud_action: '%{action}_static_key', wrapper: 'statickey'
274
+ },
275
+ # --- Route ---
276
+ 'route' => {
277
+ base_path: 'routes/routes', search_action: 'searchroute',
278
+ crud_action: '%{action}route', wrapper: 'route'
279
+ },
280
+ # --- Snapshot ---
281
+ 'snapshot' => {
282
+ base_path: 'core/snapshots', search_action: 'search',
283
+ crud_action: '%{action}', wrapper: nil,
284
+ search_method: :get
285
+ },
286
+ # --- Syslog ---
287
+ 'syslog' => {
288
+ base_path: 'syslog/settings', search_action: 'search_destinations',
289
+ crud_action: '%{action}_destination', wrapper: 'destination'
290
+ },
291
+ # --- Trust ---
292
+ 'trust_ca' => {
293
+ base_path: 'trust/ca', search_action: 'search',
294
+ crud_action: '%{action}', wrapper: 'ca'
295
+ },
296
+ 'trust_cert' => {
297
+ base_path: 'trust/cert', search_action: 'search',
298
+ crud_action: '%{action}', wrapper: 'cert'
299
+ },
300
+ 'trust_crl' => {
301
+ base_path: 'trust/crl', search_action: 'search',
302
+ crud_action: '%{action}', wrapper: 'crl',
303
+ search_method: :get
304
+ },
305
+ # --- Tunable ---
306
+ 'tunable' => {
307
+ base_path: 'core/tunables', search_action: 'search_item',
308
+ crud_action: '%{action}_item', wrapper: 'sysctl'
309
+ },
310
+ # --- User ---
311
+ 'user' => {
312
+ base_path: 'auth/user', search_action: 'search',
313
+ crud_action: '%{action}', wrapper: 'user'
314
+ },
315
+ # --- Zabbix Agent ---
316
+ 'zabbix_agent' => {
317
+ base_path: 'zabbixagent/settings', search_action: 'get',
318
+ crud_action: '%{action}', wrapper: 'zabbixagent',
319
+ singleton: true, search_method: :get,
320
+ response_reject: %w[userparameters aliases]
321
+ },
322
+ 'zabbix_agent_alias' => {
323
+ base_path: 'zabbixagent/settings', search_action: 'get',
324
+ crud_action: '%{action}Alias', wrapper: 'alias',
325
+ search_method: :get
326
+ },
327
+ 'zabbix_agent_userparameter' => {
328
+ base_path: 'zabbixagent/settings', search_action: 'get',
329
+ crud_action: '%{action}Userparameter', wrapper: 'userparameter',
330
+ search_method: :get
331
+ },
332
+ # --- Zabbix Proxy ---
333
+ 'zabbix_proxy' => {
334
+ base_path: 'zabbixproxy/general', search_action: 'get',
335
+ crud_action: '%{action}', wrapper: 'general',
336
+ singleton: true, search_method: :get
337
+ },
338
+ }.freeze
339
+
340
+ class << self
341
+ # Looks up a resource by name.
342
+ #
343
+ # @param name [String] Resource name (e.g. 'haproxy_server')
344
+ # @return [Hash, nil] Resource definition or nil if not found
345
+ def lookup(name)
346
+ RESOURCES[name.to_s]
347
+ end
348
+
349
+ # Returns all registered resource names (sorted).
350
+ #
351
+ # @return [Array<String>]
352
+ def names
353
+ RESOURCES.keys.sort
354
+ end
355
+
356
+ # Builds a Resource instance from a registry entry.
357
+ #
358
+ # @param client [OpnApi::Client] API client
359
+ # @param name [String] Resource name from registry
360
+ # @return [OpnApi::Resource]
361
+ # @raise [OpnApi::Error] if resource name not found
362
+ def build(client, name)
363
+ entry = lookup(name)
364
+ raise OpnApi::Error, "Unknown resource type: '#{name}'. Run 'opn-api resources' for a list." unless entry
365
+
366
+ OpnApi::Resource.new(
367
+ client: client,
368
+ base_path: entry[:base_path],
369
+ search_action: entry[:search_action],
370
+ crud_action: entry[:crud_action],
371
+ singleton: entry[:singleton] || false,
372
+ search_method: entry[:search_method] || :post,
373
+ )
374
+ end
375
+ end
376
+ end
377
+ end
@@ -0,0 +1,293 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpnApi
4
+ # Unified reconfigure handler with registry pattern.
5
+ #
6
+ # Each reconfigure group is registered as a named instance. Callers
7
+ # call mark() to track devices with pending changes, then run() to
8
+ # perform the actual reconfigure. The first run() call performs the
9
+ # work; subsequent calls are no-ops because tracking state is cleared.
10
+ #
11
+ # Error tracking: if any caller marks a device as errored (via mark_error),
12
+ # reconfigure is skipped for that device.
13
+ #
14
+ # @example Register and use a reconfigure group
15
+ # OpnApi::ServiceReconfigure.register(:ipsec,
16
+ # endpoint: 'ipsec/service/reconfigure',
17
+ # log_prefix: 'opn_ipsec')
18
+ #
19
+ # OpnApi::ServiceReconfigure[:ipsec].mark('myfw', client)
20
+ # results = OpnApi::ServiceReconfigure[:ipsec].run
21
+ # # => { 'myfw' => :ok }
22
+ class ServiceReconfigure
23
+ # Global registry of named instances.
24
+ @registry = {}
25
+ @defaults_loaded = false
26
+
27
+ # Registers a new reconfigure group. Idempotent — returns the existing
28
+ # instance if the name is already registered.
29
+ #
30
+ # @param name [Symbol] Unique group identifier (e.g. :haproxy, :ipsec)
31
+ # @param endpoint [String] API endpoint for POST reconfigure call
32
+ # @param log_prefix [String] Prefix for log messages
33
+ # @param configtest_endpoint [String, nil] Optional GET endpoint for configtest.
34
+ # When set, configtest is run before reconfigure; ALERT results skip reconfigure.
35
+ # @return [ServiceReconfigure] The registered instance
36
+ def self.register(name, endpoint:, log_prefix:, configtest_endpoint: nil)
37
+ @registry[name] ||= new(
38
+ name: name,
39
+ endpoint: endpoint,
40
+ log_prefix: log_prefix,
41
+ configtest_endpoint: configtest_endpoint,
42
+ )
43
+ end
44
+
45
+ # Retrieves a registered instance by name. Loads defaults on first access
46
+ # if not already loaded.
47
+ #
48
+ # @param name [Symbol]
49
+ # @return [ServiceReconfigure]
50
+ # @raise [OpnApi::Error] if the name is not registered
51
+ def self.[](name)
52
+ load_defaults! unless @defaults_loaded
53
+ instance = @registry[name]
54
+ raise OpnApi::Error, "ServiceReconfigure: unknown group '#{name}'" unless instance
55
+
56
+ instance
57
+ end
58
+
59
+ # Returns all registered group names.
60
+ #
61
+ # @return [Array<Symbol>]
62
+ def self.registered_names
63
+ load_defaults! unless @defaults_loaded
64
+ @registry.keys
65
+ end
66
+
67
+ # Clears all registrations and instance state.
68
+ def self.reset!
69
+ @registry.each_value(&:clear_state)
70
+ @registry.clear
71
+ @defaults_loaded = false
72
+ end
73
+
74
+ # Registers all default OPNsense reconfigure groups.
75
+ # Called automatically on first access via [], but can be called
76
+ # explicitly for eager loading.
77
+ def self.load_defaults!
78
+ return if @defaults_loaded
79
+
80
+ @defaults_loaded = true
81
+ register_defaults
82
+ end
83
+
84
+ # @return [Symbol] The group name
85
+ attr_reader :name
86
+
87
+ # Registers a device as having pending changes. Subsequent calls for
88
+ # the same device are ignored (first client wins).
89
+ #
90
+ # @param device_name [String]
91
+ # @param client [OpnApi::Client]
92
+ def mark(device_name, client)
93
+ @devices_to_reconfigure[device_name] ||= client
94
+ end
95
+
96
+ # Registers a device as having a resource evaluation error. Used to
97
+ # suppress reconfigure when the service config may be inconsistent.
98
+ #
99
+ # @param device_name [String]
100
+ def mark_error(device_name)
101
+ @devices_with_errors[device_name] = true
102
+ end
103
+
104
+ # Performs reconfigure for all marked devices, then clears state.
105
+ # Returns a result hash per device.
106
+ #
107
+ # @return [Hash{String => Symbol}] Results per device:
108
+ # :ok — reconfigure succeeded
109
+ # :skipped — skipped due to error or configtest ALERT
110
+ # :error — reconfigure call failed
111
+ # :warning — reconfigure returned unexpected status
112
+ def run
113
+ results = {}
114
+ @devices_to_reconfigure.each do |device_name, client|
115
+ results[device_name] = reconfigure_device(device_name, client)
116
+ end
117
+ clear_state
118
+ results
119
+ end
120
+
121
+ # Clears all tracking state (devices + errors).
122
+ def clear_state
123
+ @devices_to_reconfigure.clear
124
+ @devices_with_errors.clear
125
+ end
126
+
127
+ private
128
+
129
+ def initialize(name:, endpoint:, log_prefix:, configtest_endpoint:)
130
+ @name = name
131
+ @endpoint = endpoint
132
+ @log_prefix = log_prefix
133
+ @configtest_endpoint = configtest_endpoint
134
+ @devices_to_reconfigure = {}
135
+ @devices_with_errors = {}
136
+ end
137
+
138
+ # Reconfigures a single device. Returns a status symbol.
139
+ def reconfigure_device(device_name, client)
140
+ # Skip devices with resource evaluation errors
141
+ if @devices_with_errors[device_name]
142
+ OpnApi.logger.error(
143
+ "#{@log_prefix}: skipping reconfigure for '#{device_name}' " \
144
+ 'because one or more resources failed to evaluate',
145
+ )
146
+ return :skipped
147
+ end
148
+
149
+ # Run configtest if configured (e.g. HAProxy)
150
+ return :skipped if @configtest_endpoint && !run_configtest(device_name, client)
151
+
152
+ execute_reconfigure(device_name, client)
153
+ rescue OpnApi::Error => e
154
+ OpnApi.logger.error("#{@log_prefix}: reconfigure of '#{device_name}' failed: #{e.message}")
155
+ :error
156
+ end
157
+
158
+ # Runs configtest for a device. Returns true if reconfigure should proceed.
159
+ def run_configtest(device_name, client)
160
+ result = client.get(@configtest_endpoint)
161
+ test_output = result.is_a?(Hash) ? result['result'].to_s : ''
162
+
163
+ if test_output.include?('ALERT')
164
+ OpnApi.logger.error(
165
+ "#{@log_prefix}: configtest for '#{device_name}' reported ALERT, " \
166
+ "skipping reconfigure: #{test_output.strip}",
167
+ )
168
+ return false
169
+ elsif test_output.include?('WARNING')
170
+ OpnApi.logger.warning(
171
+ "#{@log_prefix}: configtest for '#{device_name}' reported WARNING: " \
172
+ "#{test_output.strip}",
173
+ )
174
+ else
175
+ OpnApi.logger.notice("#{@log_prefix}: configtest for '#{device_name}' passed")
176
+ end
177
+
178
+ true
179
+ end
180
+
181
+ # Executes the reconfigure POST and returns a status symbol.
182
+ def execute_reconfigure(device_name, client)
183
+ reconf = client.post(@endpoint, {})
184
+ status = reconf.is_a?(Hash) ? reconf['status'].to_s.strip.downcase : nil
185
+ if status == 'ok'
186
+ OpnApi.logger.notice("#{@log_prefix}: reconfigure of '#{device_name}' completed")
187
+ :ok
188
+ else
189
+ OpnApi.logger.warning(
190
+ "#{@log_prefix}: reconfigure of '#{device_name}' returned unexpected " \
191
+ "status: #{reconf.inspect}",
192
+ )
193
+ :warning
194
+ end
195
+ end
196
+
197
+ # Registers all default OPNsense reconfigure groups (ported from
198
+ # puppet-opn's service_reconfigure_registry.rb).
199
+ def self.register_defaults
200
+ # ACME Client
201
+ register(:acmeclient,
202
+ endpoint: 'acmeclient/service/reconfigure',
203
+ log_prefix: 'opn_acmeclient')
204
+
205
+ # Cron jobs
206
+ register(:cron,
207
+ endpoint: 'cron/service/reconfigure',
208
+ log_prefix: 'opn_cron')
209
+
210
+ # DHCP Relay
211
+ register(:dhcrelay,
212
+ endpoint: 'dhcrelay/service/reconfigure',
213
+ log_prefix: 'opn_dhcrelay')
214
+
215
+ # Firewall aliases
216
+ register(:firewall_alias,
217
+ endpoint: 'firewall/alias/reconfigure',
218
+ log_prefix: 'opn_firewall_alias')
219
+
220
+ # Firewall interface groups
221
+ register(:firewall_group,
222
+ endpoint: 'firewall/group/reconfigure',
223
+ log_prefix: 'opn_firewall_group')
224
+
225
+ # Firewall rules (uses 'apply' instead of 'reconfigure')
226
+ register(:firewall_rule,
227
+ endpoint: 'firewall/filter/apply',
228
+ log_prefix: 'opn_firewall_rule')
229
+
230
+ # Routing gateways
231
+ register(:gateway,
232
+ endpoint: 'routing/settings/reconfigure',
233
+ log_prefix: 'opn_gateway')
234
+
235
+ # HAProxy (with configtest before reconfigure)
236
+ register(:haproxy,
237
+ endpoint: 'haproxy/service/reconfigure',
238
+ log_prefix: 'opn_haproxy',
239
+ configtest_endpoint: 'haproxy/service/configtest')
240
+
241
+ # HA sync / CARP
242
+ register(:hasync,
243
+ endpoint: 'core/hasync/reconfigure',
244
+ log_prefix: 'opn_hasync')
245
+
246
+ # IPsec
247
+ register(:ipsec,
248
+ endpoint: 'ipsec/service/reconfigure',
249
+ log_prefix: 'opn_ipsec')
250
+
251
+ # KEA DHCP
252
+ register(:kea,
253
+ endpoint: 'kea/service/reconfigure',
254
+ log_prefix: 'opn_kea')
255
+
256
+ # Node Exporter
257
+ register(:node_exporter,
258
+ endpoint: 'nodeexporter/service/reconfigure',
259
+ log_prefix: 'opn_node_exporter')
260
+
261
+ # OpenVPN
262
+ register(:openvpn,
263
+ endpoint: 'openvpn/service/reconfigure',
264
+ log_prefix: 'opn_openvpn')
265
+
266
+ # Static routes
267
+ register(:route,
268
+ endpoint: 'routes/routes/reconfigure',
269
+ log_prefix: 'opn_route')
270
+
271
+ # Syslog
272
+ register(:syslog,
273
+ endpoint: 'syslog/service/reconfigure',
274
+ log_prefix: 'opn_syslog')
275
+
276
+ # System tunables (sysctl)
277
+ register(:tunable,
278
+ endpoint: 'core/tunables/reconfigure',
279
+ log_prefix: 'opn_tunable')
280
+
281
+ # Zabbix Agent
282
+ register(:zabbix_agent,
283
+ endpoint: 'zabbixagent/service/reconfigure',
284
+ log_prefix: 'opn_zabbix_agent')
285
+
286
+ # Zabbix Proxy
287
+ register(:zabbix_proxy,
288
+ endpoint: 'zabbixproxy/service/reconfigure',
289
+ log_prefix: 'opn_zabbix_proxy')
290
+ end
291
+ private_class_method :register_defaults
292
+ end
293
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpnApi
4
+ VERSION = '0.1.0'
5
+ end