palo_alto 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +2 -0
- data/README.md +1 -0
- data/examples/test_config.rb +31 -15
- data/examples/test_op.rb +52 -73
- data/lib/palo_alto/config.rb +75269 -51836
- data/lib/palo_alto/op.rb +59 -38
- data/lib/palo_alto/version.rb +1 -1
- data/lib/palo_alto.rb +120 -56
- metadata +2 -2
data/lib/palo_alto/op.rb
CHANGED
@@ -75,63 +75,84 @@ module PaloAlto
|
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
|
-
def
|
79
|
-
|
78
|
+
def xml_builder_iter(xml, ops, data)
|
79
|
+
raise 'No Ops?!' if ops.nil?
|
80
|
+
|
81
|
+
case data
|
80
82
|
when String
|
81
|
-
section =
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
data
|
83
|
+
section = data
|
84
|
+
data2 = nil
|
85
|
+
xml_builder(xml, ops, section, data2)
|
86
|
+
when Hash, Array
|
87
|
+
data.each do |section, data2|
|
88
|
+
xml_builder(xml, ops, section, data2)
|
89
|
+
end
|
86
90
|
else
|
87
|
-
raise
|
88
|
-
end
|
89
|
-
|
90
|
-
unless ops.key?(section.to_s)
|
91
|
-
err = "Error #{section} does not exist. Valid: " + ops.keys.pretty_inspect
|
92
|
-
raise err
|
91
|
+
raise data.pretty_inspect
|
93
92
|
end
|
93
|
+
end
|
94
94
|
|
95
|
-
|
96
|
-
|
97
|
-
section
|
95
|
+
def xml_builder(xml, ops, section, data, type = ops[section.to_s]&.[](:obj))
|
96
|
+
ops_tree = ops[section.to_s] || raise("no ops tree for section #{section}, #{ops.keys.inspect}")
|
97
|
+
# pp [:xml_builder, :section, section, :type, type]
|
98
|
+
# obj = data
|
98
99
|
|
99
|
-
case
|
100
|
+
case type
|
100
101
|
when :element
|
101
|
-
xml.public_send(section, data)
|
102
|
+
xml.public_send(escape_xpath_tag(section), data)
|
102
103
|
when :array
|
103
|
-
xml.public_send(section) do
|
104
|
+
xml.public_send(escape_xpath_tag(section)) do
|
105
|
+
raise 'data is Hash and should be Array' if data.is_a?(Hash)
|
106
|
+
|
104
107
|
data.each do |el|
|
105
108
|
key = ops_tree.keys.first
|
106
|
-
|
109
|
+
case el
|
110
|
+
when Hash
|
111
|
+
attr = ops_tree[key].find { |_k, v| v.is_a?(Hash) && v[:obj] == :'attr-req' }.first
|
112
|
+
xml.public_send(escape_xpath_tag(key), { attr => el[attr.to_sym] }) do
|
113
|
+
remaining_attrs = el.reject { |k, _v| k == attr.to_sym }
|
114
|
+
|
115
|
+
if remaining_attrs.any?
|
116
|
+
xml_builder(xml, ops_tree[key], remaining_attrs.keys.first.to_s, remaining_attrs.values.first,
|
117
|
+
:array)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
when String
|
121
|
+
xml.public_send(key, el)
|
122
|
+
end
|
107
123
|
end
|
108
124
|
end
|
109
125
|
when :sequence
|
110
|
-
if data.nil?
|
111
|
-
xml.send(section)
|
126
|
+
if data.nil? || data == true
|
127
|
+
xml.send(escape_xpath_tag(section))
|
112
128
|
elsif data.is_a?(Hash)
|
113
|
-
xml.send(section)
|
114
|
-
|
129
|
+
xml.send(escape_xpath_tag(section)) do
|
130
|
+
xml_builder_iter(xml, ops_tree, data)
|
115
131
|
end
|
116
|
-
else # array
|
132
|
+
else # array, what else could it be?!
|
133
|
+
raise "Unknown: #{attr.inspect}" unless data.is_a?(Array)
|
117
134
|
|
118
|
-
if data.
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
135
|
+
raise 'Too many hashes in an array, please update' if data.length > 1
|
136
|
+
|
137
|
+
key = ops_tree.keys.first
|
138
|
+
attr_name = ops_tree[key].find { |_k, v| v.is_a?(Hash) && v[:obj] == :'attr-req' }.first
|
139
|
+
|
140
|
+
hash = data.first.dup
|
141
|
+
|
142
|
+
data = [hash.reject { |k| k == attr_name.to_sym }]
|
143
|
+
attr = { attr_name => hash[attr_name.to_sym] }
|
124
144
|
|
125
|
-
xml.public_send(section
|
126
|
-
|
127
|
-
|
145
|
+
xml.public_send(escape_xpath_tag(section)) do
|
146
|
+
xml.public_send(escape_xpath_tag(key), attr) do
|
147
|
+
data.each do |child|
|
148
|
+
xml_builder_iter(xml, ops_tree[key], child)
|
149
|
+
end
|
128
150
|
end
|
129
151
|
end
|
130
152
|
end
|
131
153
|
when :union
|
132
|
-
|
133
|
-
|
134
|
-
xml_builder(xml, ops_tree, v)
|
154
|
+
xml.public_send(escape_xpath_tag(section)) do
|
155
|
+
xml_builder_iter(xml, ops[section.to_s], data)
|
135
156
|
end
|
136
157
|
else
|
137
158
|
raise ops_tree[:obj].pretty_inspect
|
@@ -141,7 +162,7 @@ module PaloAlto
|
|
141
162
|
|
142
163
|
def to_xml(obj)
|
143
164
|
builder = Nokogiri::XML::Builder.new do |xml|
|
144
|
-
|
165
|
+
xml_builder_iter(xml, @@ops, obj)
|
145
166
|
end
|
146
167
|
builder.doc.root.to_xml
|
147
168
|
end
|
data/lib/palo_alto/version.rb
CHANGED
data/lib/palo_alto.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
require 'openssl'
|
4
4
|
require 'nokogiri'
|
5
5
|
require 'net/http'
|
6
|
-
require 'pp'
|
7
6
|
|
8
7
|
require_relative 'palo_alto/version'
|
9
8
|
|
@@ -129,6 +128,7 @@ module PaloAlto
|
|
129
128
|
raise SessionTimedOutException
|
130
129
|
when '400', '403'
|
131
130
|
begin
|
131
|
+
pp [:error, options[:host], response.code, response.message]
|
132
132
|
data = Nokogiri::XML.parse(response.body)
|
133
133
|
message = data.xpath('//response/response/msg').text
|
134
134
|
code = response.code.to_i
|
@@ -141,8 +141,8 @@ module PaloAlto
|
|
141
141
|
end
|
142
142
|
|
143
143
|
nil
|
144
|
-
rescue Net::OpenTimeout, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
145
|
-
raise ConnectionErrorException, e.message
|
144
|
+
rescue Net::OpenTimeout, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::ECONNRESET => e
|
145
|
+
raise ConnectionErrorException, [e.message, options[:host]].inspect
|
146
146
|
end
|
147
147
|
|
148
148
|
def self.raise_error(code, message)
|
@@ -175,13 +175,43 @@ module PaloAlto
|
|
175
175
|
attr_accessor :host, :username, :auth_key, :verify_ssl, :debug, :timeout
|
176
176
|
|
177
177
|
def pretty_print_instance_variables
|
178
|
-
super - [
|
178
|
+
super - %i[@password @subclasses @subclasses @expression @arguments @cache @op @auth_key]
|
179
179
|
end
|
180
180
|
|
181
|
-
|
182
|
-
|
183
|
-
|
181
|
+
@@output_lock = Mutex.new
|
182
|
+
|
183
|
+
def print_sent(options, method = :puts)
|
184
|
+
@@output_lock.synchronize do
|
185
|
+
send(method, "Sent (#{Time.now}, #{options[:session_id]}):")
|
186
|
+
options.each do |k, v|
|
187
|
+
case k
|
188
|
+
when :debug
|
189
|
+
send(method, " #{k}: #{v.reject { |str| str.start_with?('_') }.join(', ')}")
|
190
|
+
when :headers
|
191
|
+
headers = options[:headers].dup.transform_keys(&:to_s)
|
192
|
+
headers['X-PAN-KEY'] = '***' if headers.key?('X-PAN-KEY')
|
193
|
+
puts " #{k}: #{headers}"
|
194
|
+
when :payload
|
195
|
+
send(method, ' payload: ' + options[:payload].map do |k, v|
|
196
|
+
element_length = 1024
|
197
|
+
if k == :element && v.length >= element_length
|
198
|
+
[k.to_s, "#{v[..element_length]}..."]
|
199
|
+
elsif k == :password
|
200
|
+
[k.to_s, '***']
|
201
|
+
else
|
202
|
+
[k.to_s, v]
|
203
|
+
end
|
204
|
+
end.to_h.inspect)
|
205
|
+
else
|
206
|
+
send(method, " #{k}: #{v}")
|
207
|
+
end
|
208
|
+
end
|
184
209
|
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def execute(payload, skip_authentication: false, skip_cache: false)
|
213
|
+
get_auth_key if !auth_key && !skip_authentication
|
214
|
+
session_id = (0...6).map { ('a'..'z').to_a[rand(26)] }.join
|
185
215
|
|
186
216
|
if payload[:type] == 'config' && !skip_cache
|
187
217
|
if payload[:action] == 'get'
|
@@ -191,13 +221,13 @@ module PaloAlto
|
|
191
221
|
next unless search_xpath.start_with?(cached_xpath)
|
192
222
|
|
193
223
|
remove = cached_xpath.split('/')[1...-1].join('/').length
|
194
|
-
new_xpath =
|
224
|
+
new_xpath = "response/result/#{search_xpath[(remove + 2)..]}"
|
195
225
|
|
196
226
|
results = cache.xpath(new_xpath)
|
197
|
-
|
227
|
+
xml = Nokogiri.parse("<?xml version=\"1.0\"?><response><result>#{results}</result></response>")
|
198
228
|
|
199
229
|
if debug.include?(:statistics)
|
200
|
-
warn "Elapsed for parsing cache: #{Time.now - start_time} seconds"
|
230
|
+
warn "Elapsed for parsing cache: #{Time.now - start_time} seconds (#{session_id})"
|
201
231
|
end
|
202
232
|
|
203
233
|
return xml
|
@@ -212,32 +242,40 @@ module PaloAlto
|
|
212
242
|
# configure options for the request
|
213
243
|
options = {}
|
214
244
|
options[:host] = host
|
245
|
+
options[:session_id] = session_id
|
215
246
|
options[:verify_ssl] = verify_ssl
|
216
247
|
options[:payload] = payload
|
217
248
|
options[:debug] = debug
|
218
|
-
options[:timeout] = timeout ||
|
249
|
+
options[:timeout] = timeout || 600
|
219
250
|
options[:headers] = if payload[:type] == 'keygen'
|
220
251
|
{}
|
221
252
|
else
|
222
253
|
{ 'X-PAN-KEY': auth_key }
|
223
254
|
end
|
224
255
|
|
225
|
-
|
256
|
+
print_sent(options) if debug.include?(:sent)
|
226
257
|
|
227
258
|
start_time = Time.now
|
228
259
|
text = Helpers::Rest.make_request(options)
|
229
|
-
if debug.include?(:statistics)
|
230
|
-
warn "Elapsed for API call #{payload[:type]}/#{payload[:action] || '(unknown action)'} on #{host}: #{Time.now - start_time} seconds, #{text.length} bytes"
|
231
|
-
end
|
232
260
|
|
233
|
-
|
261
|
+
@@output_lock.synchronize do
|
262
|
+
if debug.include?(:statistics)
|
263
|
+
warn "Elapsed for API call #{payload[:type]}/#{payload[:action] || '(unknown action)'} on #{host}: #{Time.now - start_time} seconds, #{text.length} bytes (#{session_id})"
|
264
|
+
end
|
265
|
+
|
266
|
+
warn "Received at #{Time.now} (#{session_id}):\n#{text.inspect}\n" if debug.include?(:received)
|
267
|
+
end
|
234
268
|
|
235
269
|
data = Nokogiri::XML.parse(text)
|
236
270
|
unless data.xpath('//response/@status').to_s == 'success'
|
237
271
|
unless %w[op commit].include?(payload[:type]) # here we fail silent
|
238
|
-
warn "
|
239
|
-
warn
|
240
|
-
|
272
|
+
warn "Command failed on host #{host} at #{Time.now} (#{session_id})"
|
273
|
+
print_sent(options, :warn) if debug.include?(:sent_on_error) && !debug.include?(:sent)
|
274
|
+
if debug.include?(:received_on_error) && !debug.include?(:received)
|
275
|
+
@@output_lock.synchronize do
|
276
|
+
warn "Received at #{Time.now} (#{session_id}):\n#{text.inspect}\n"
|
277
|
+
end
|
278
|
+
end
|
241
279
|
end
|
242
280
|
code = data.at_xpath('//response/@code')&.value.to_i # sometimes there is no code :( e.g. for 'op' errors
|
243
281
|
message = data.xpath('/response/msg/line').map(&:text).map(&:strip).join("\n")
|
@@ -245,23 +283,35 @@ module PaloAlto
|
|
245
283
|
end
|
246
284
|
|
247
285
|
data
|
286
|
+
rescue ConnectionErrorException => e
|
287
|
+
# for ConnectionErrorException, you don't know if the command was successful as you don't get a result after an action started:(
|
288
|
+
# As it's a temporary error, we need to rescue/raise it explicitly
|
289
|
+
# #edit! rescues it, for other calls, it normally should not happen.....
|
290
|
+
raise e
|
248
291
|
rescue EOFError, Net::ReadTimeout => e
|
249
292
|
max_retries = if %w[keygen config].include?(payload[:type])
|
250
293
|
# TODO: only retry on config, when it's get or edit, otherwise you may get strange errors
|
251
|
-
|
294
|
+
40
|
252
295
|
else
|
253
296
|
0
|
254
297
|
end
|
255
298
|
|
256
|
-
|
299
|
+
if retried >= max_retries
|
300
|
+
raise ConnectionErrorException, [e.message, options[:host], payload[:type]].inspect
|
301
|
+
end
|
257
302
|
|
258
303
|
retried += 1
|
259
|
-
|
304
|
+
if debug.include?(:warnings)
|
305
|
+
@@output_lock.synchronize do
|
306
|
+
warn "Got connection error #{e.inspect} from #{host}; retrying (try #{retried} of #{max_retries}, #{session_id})"
|
307
|
+
end
|
308
|
+
end
|
260
309
|
sleep 10
|
261
310
|
retry
|
262
311
|
rescue TemporaryException => e
|
263
312
|
dont_retry_at = [
|
264
313
|
'Partial revert is not allowed. Full system commit must be completed.',
|
314
|
+
'Local commit jobs are queued. Revert operation is not allowed.',
|
265
315
|
'Config for scope ',
|
266
316
|
'Config is not currently locked for scope ',
|
267
317
|
'Commit lock is not currently held by',
|
@@ -269,13 +319,14 @@ module PaloAlto
|
|
269
319
|
'This operation is blocked because of ',
|
270
320
|
'Other administrators are holding config locks ',
|
271
321
|
'Configuration is locked by ',
|
322
|
+
'device-group', # device-group -> ... is already in use
|
272
323
|
' device-group' # device-group -> ... is already in use
|
273
324
|
]
|
274
325
|
|
275
326
|
max_retries = if dont_retry_at.any? { |str| e.message.start_with?(str) }
|
276
327
|
0
|
277
328
|
elsif e.message.start_with?('Timed out while getting config lock. Please try again.')
|
278
|
-
|
329
|
+
40
|
279
330
|
else
|
280
331
|
1
|
281
332
|
end
|
@@ -283,7 +334,11 @@ module PaloAlto
|
|
283
334
|
raise e if retried >= max_retries
|
284
335
|
|
285
336
|
retried += 1
|
286
|
-
|
337
|
+
if debug.include?(:warnings)
|
338
|
+
@@output_lock.synchronize do
|
339
|
+
warn "Got temporary error #{e.inspect}; retrying (try #{retried} of #{max_retries})"
|
340
|
+
end
|
341
|
+
end
|
287
342
|
|
288
343
|
get_auth_key if e.is_a?(SessionTimedOutException)
|
289
344
|
retry
|
@@ -321,23 +376,29 @@ module PaloAlto
|
|
321
376
|
cmd = if all
|
322
377
|
'commit'
|
323
378
|
else
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
'no-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
379
|
+
commit_partial = {
|
380
|
+
'no-template-stack': true,
|
381
|
+
'no-log-collector': true,
|
382
|
+
'no-log-collector-group': true,
|
383
|
+
'no-wildfire-appliance': true,
|
384
|
+
'no-wildfire-appliance-cluster': true,
|
385
|
+
'device-and-network': 'excluded',
|
386
|
+
'shared-object': 'excluded'
|
387
|
+
}
|
388
|
+
|
389
|
+
if device_groups
|
390
|
+
commit_partial.merge!(device_groups.empty? ? { 'no-device-group': true } : { 'device-group': device_groups })
|
391
|
+
end
|
392
|
+
|
393
|
+
if templates
|
394
|
+
commit_partial.merge!(templates.empty? ? { 'no-template': true } : { template: templates })
|
395
|
+
end
|
396
|
+
|
397
|
+
commit_partial.merge!({ admin: admins }) if admins
|
398
|
+
|
399
|
+
{ commit: { partial: commit_partial } }
|
340
400
|
end
|
401
|
+
|
341
402
|
result = op.execute(cmd)
|
342
403
|
|
343
404
|
return result if raw_result
|
@@ -358,7 +419,7 @@ module PaloAlto
|
|
358
419
|
def primary_active?
|
359
420
|
cmd = { show: { 'high-availability': 'state' } }
|
360
421
|
state = op.execute(cmd)
|
361
|
-
state.at_xpath('response/result/local-info/state')
|
422
|
+
state.at_xpath('response/result/local-info/state')&.text == 'primary-active'
|
362
423
|
end
|
363
424
|
|
364
425
|
# area: config, commit
|
@@ -380,6 +441,7 @@ module PaloAlto
|
|
380
441
|
# will execute block if given and unlock afterwards. returns false if lock could not be aquired
|
381
442
|
def lock(area:, comment: nil, type: nil, location: nil)
|
382
443
|
raise MalformedCommandException, 'No type specified' if location && !type
|
444
|
+
|
383
445
|
if block_given?
|
384
446
|
return false unless lock(area: area, comment: comment, type: type, location: location)
|
385
447
|
|
@@ -448,7 +510,7 @@ module PaloAlto
|
|
448
510
|
return result unless %w[ACT PEND].include?(status)
|
449
511
|
|
450
512
|
nil
|
451
|
-
rescue => e
|
513
|
+
rescue StandardError => e
|
452
514
|
warn [:job_query_error, @host, e].inspect
|
453
515
|
false
|
454
516
|
end
|
@@ -469,9 +531,11 @@ module PaloAlto
|
|
469
531
|
end
|
470
532
|
|
471
533
|
job_result = query_and_parse_job(job_id)
|
472
|
-
return job_result
|
534
|
+
return job_result unless job_result # can be either nil or false (errored)
|
473
535
|
|
474
|
-
|
536
|
+
if job_result.xpath('response/result/job/details/line').text&.include?('Configuration committed successfully')
|
537
|
+
return true
|
538
|
+
end
|
475
539
|
|
476
540
|
job_result
|
477
541
|
end
|
@@ -484,7 +548,7 @@ module PaloAlto
|
|
484
548
|
result = op.execute(cmd)
|
485
549
|
status = result.at_xpath('response/result/job/status')&.text
|
486
550
|
return result unless %w[ACT PEND].include?(status)
|
487
|
-
rescue => e
|
551
|
+
rescue StandardError => e
|
488
552
|
warn [:job_query_error, Time.now, @host, e].inspect
|
489
553
|
return false if e.message =~ /\Ajob \d+ not found\z/
|
490
554
|
end
|
@@ -500,17 +564,17 @@ module PaloAlto
|
|
500
564
|
cmd = if all
|
501
565
|
{ revert: 'config' }
|
502
566
|
else
|
503
|
-
{ revert: { config: { partial:
|
504
|
-
|
505
|
-
'no-template',
|
506
|
-
'no-template-stack',
|
507
|
-
'no-log-collector',
|
508
|
-
'no-log-collector-group',
|
509
|
-
'no-wildfire-appliance',
|
510
|
-
'no-wildfire-appliance-cluster',
|
511
|
-
|
512
|
-
|
513
|
-
|
567
|
+
{ revert: { config: { partial: {
|
568
|
+
admin: [username],
|
569
|
+
'no-template': true,
|
570
|
+
'no-template-stack': true,
|
571
|
+
'no-log-collector': true,
|
572
|
+
'no-log-collector-group': true,
|
573
|
+
'no-wildfire-appliance': true,
|
574
|
+
'no-wildfire-appliance-cluster': true,
|
575
|
+
'device-and-network': 'excluded',
|
576
|
+
'shared-object': 'excluded'
|
577
|
+
} } } }
|
514
578
|
end
|
515
579
|
|
516
580
|
waited = 0
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: palo_alto
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sebastian Roesner
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-12-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|