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,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
|