palo_alto 0.5.1 → 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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PaloAlto
4
- VERSION = '0.5.1'
4
+ VERSION = '0.6.0'
5
5
  end
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
 
@@ -142,8 +141,8 @@ module PaloAlto
142
141
  end
143
142
 
144
143
  nil
145
- rescue Net::OpenTimeout, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
146
- raise ConnectionErrorException, e.message
144
+ rescue Net::OpenTimeout, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::ECONNRESET => e
145
+ raise ConnectionErrorException, [e.message, options[:host]].inspect
147
146
  end
148
147
 
149
148
  def self.raise_error(code, message)
@@ -176,13 +175,43 @@ module PaloAlto
176
175
  attr_accessor :host, :username, :auth_key, :verify_ssl, :debug, :timeout
177
176
 
178
177
  def pretty_print_instance_variables
179
- super - [:@password, :@subclasses, :@subclasses, :@expression, :@arguments, :@cache, :@op, :@auth_key]
178
+ super - %i[@password @subclasses @subclasses @expression @arguments @cache @op @auth_key]
180
179
  end
181
180
 
182
- def execute(payload, skip_authentication: false, skip_cache: false)
183
- if !auth_key && !skip_authentication
184
- get_auth_key
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
185
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
186
215
 
187
216
  if payload[:type] == 'config' && !skip_cache
188
217
  if payload[:action] == 'get'
@@ -192,13 +221,13 @@ module PaloAlto
192
221
  next unless search_xpath.start_with?(cached_xpath)
193
222
 
194
223
  remove = cached_xpath.split('/')[1...-1].join('/').length
195
- new_xpath = 'response/result/' + search_xpath[(remove+2)..]
224
+ new_xpath = "response/result/#{search_xpath[(remove + 2)..]}"
196
225
 
197
226
  results = cache.xpath(new_xpath)
198
- xml = Nokogiri.parse("<?xml version=\"1.0\"?><response><result>#{results.to_s}</result></response>")
227
+ xml = Nokogiri.parse("<?xml version=\"1.0\"?><response><result>#{results}</result></response>")
199
228
 
200
229
  if debug.include?(:statistics)
201
- warn "Elapsed for parsing cache: #{Time.now - start_time} seconds"
230
+ warn "Elapsed for parsing cache: #{Time.now - start_time} seconds (#{session_id})"
202
231
  end
203
232
 
204
233
  return xml
@@ -213,6 +242,7 @@ module PaloAlto
213
242
  # configure options for the request
214
243
  options = {}
215
244
  options[:host] = host
245
+ options[:session_id] = session_id
216
246
  options[:verify_ssl] = verify_ssl
217
247
  options[:payload] = payload
218
248
  options[:debug] = debug
@@ -223,22 +253,29 @@ module PaloAlto
223
253
  { 'X-PAN-KEY': auth_key }
224
254
  end
225
255
 
226
- warn "sent: (#{Time.now}\n#{options.pretty_inspect}\n" if debug.include?(:sent)
256
+ print_sent(options) if debug.include?(:sent)
227
257
 
228
258
  start_time = Time.now
229
259
  text = Helpers::Rest.make_request(options)
230
- if debug.include?(:statistics)
231
- warn "Elapsed for API call #{payload[:type]}/#{payload[:action] || '(unknown action)'} on #{host}: #{Time.now - start_time} seconds, #{text.length} bytes"
232
- end
233
260
 
234
- warn "received: #{Time.now}\n#{text}\n" if debug.include?(:received)
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
235
268
 
236
269
  data = Nokogiri::XML.parse(text)
237
270
  unless data.xpath('//response/@status').to_s == 'success'
238
271
  unless %w[op commit].include?(payload[:type]) # here we fail silent
239
- warn "command failed on host #{host} at #{Time.now}"
240
- warn "sent:\n#{options.inspect}\n" if debug.include?(:sent_on_error)
241
- warn "received:\n#{text.inspect}\n" if debug.include?(:received_on_error)
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
242
279
  end
243
280
  code = data.at_xpath('//response/@code')&.value.to_i # sometimes there is no code :( e.g. for 'op' errors
244
281
  message = data.xpath('/response/msg/line').map(&:text).map(&:strip).join("\n")
@@ -246,18 +283,29 @@ module PaloAlto
246
283
  end
247
284
 
248
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
249
291
  rescue EOFError, Net::ReadTimeout => e
250
292
  max_retries = if %w[keygen config].include?(payload[:type])
251
293
  # TODO: only retry on config, when it's get or edit, otherwise you may get strange errors
252
- 3
294
+ 40
253
295
  else
254
296
  0
255
297
  end
256
298
 
257
- raise e if retried >= max_retries
299
+ if retried >= max_retries
300
+ raise ConnectionErrorException, [e.message, options[:host], payload[:type]].inspect
301
+ end
258
302
 
259
303
  retried += 1
260
- warn "Got error #{e.inspect}; retrying (try #{retried})" if debug.include?(:warnings)
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
261
309
  sleep 10
262
310
  retry
263
311
  rescue TemporaryException => e
@@ -271,13 +319,14 @@ module PaloAlto
271
319
  'This operation is blocked because of ',
272
320
  'Other administrators are holding config locks ',
273
321
  'Configuration is locked by ',
322
+ 'device-group', # device-group -> ... is already in use
274
323
  ' device-group' # device-group -> ... is already in use
275
324
  ]
276
325
 
277
326
  max_retries = if dont_retry_at.any? { |str| e.message.start_with?(str) }
278
327
  0
279
328
  elsif e.message.start_with?('Timed out while getting config lock. Please try again.')
280
- 30
329
+ 40
281
330
  else
282
331
  1
283
332
  end
@@ -285,7 +334,11 @@ module PaloAlto
285
334
  raise e if retried >= max_retries
286
335
 
287
336
  retried += 1
288
- warn "Got error #{e.inspect}; retrying (try #{retried})" if debug.include?(:warnings)
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
289
342
 
290
343
  get_auth_key if e.is_a?(SessionTimedOutException)
291
344
  retry
@@ -334,16 +387,14 @@ module PaloAlto
334
387
  }
335
388
 
336
389
  if device_groups
337
- commit_partial.merge!(device_groups.empty? ? {'no-device-group': true} : { 'device-group': device_groups })
390
+ commit_partial.merge!(device_groups.empty? ? { 'no-device-group': true } : { 'device-group': device_groups })
338
391
  end
339
392
 
340
393
  if templates
341
- commit_partial.merge!(templates.empty? ? {'no-template': true} : { 'template': templates })
394
+ commit_partial.merge!(templates.empty? ? { 'no-template': true } : { template: templates })
342
395
  end
343
396
 
344
- if admins
345
- commit_partial.merge!({'admin': admins})
346
- end
397
+ commit_partial.merge!({ admin: admins }) if admins
347
398
 
348
399
  { commit: { partial: commit_partial } }
349
400
  end
@@ -390,6 +441,7 @@ module PaloAlto
390
441
  # will execute block if given and unlock afterwards. returns false if lock could not be aquired
391
442
  def lock(area:, comment: nil, type: nil, location: nil)
392
443
  raise MalformedCommandException, 'No type specified' if location && !type
444
+
393
445
  if block_given?
394
446
  return false unless lock(area: area, comment: comment, type: type, location: location)
395
447
 
@@ -458,7 +510,7 @@ module PaloAlto
458
510
  return result unless %w[ACT PEND].include?(status)
459
511
 
460
512
  nil
461
- rescue => e
513
+ rescue StandardError => e
462
514
  warn [:job_query_error, @host, e].inspect
463
515
  false
464
516
  end
@@ -479,9 +531,11 @@ module PaloAlto
479
531
  end
480
532
 
481
533
  job_result = query_and_parse_job(job_id)
482
- return job_result if !job_result # can be either nil or false (errored)
534
+ return job_result unless job_result # can be either nil or false (errored)
483
535
 
484
- return true if job_result.xpath('response/result/job/details/line').text&.include?('Configuration committed successfully')
536
+ if job_result.xpath('response/result/job/details/line').text&.include?('Configuration committed successfully')
537
+ return true
538
+ end
485
539
 
486
540
  job_result
487
541
  end
@@ -494,7 +548,7 @@ module PaloAlto
494
548
  result = op.execute(cmd)
495
549
  status = result.at_xpath('response/result/job/status')&.text
496
550
  return result unless %w[ACT PEND].include?(status)
497
- rescue => e
551
+ rescue StandardError => e
498
552
  warn [:job_query_error, Time.now, @host, e].inspect
499
553
  return false if e.message =~ /\Ajob \d+ not found\z/
500
554
  end
@@ -511,7 +565,7 @@ module PaloAlto
511
565
  { revert: 'config' }
512
566
  else
513
567
  { revert: { config: { partial: {
514
- 'admin': [username],
568
+ admin: [username],
515
569
  'no-template': true,
516
570
  'no-template-stack': true,
517
571
  'no-log-collector': true,
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.5.1
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: 2024-04-09 00:00:00.000000000 Z
11
+ date: 2024-12-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri