palo_alto 0.3.2 → 0.4.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.
data/lib/palo_alto/log.rb CHANGED
@@ -12,7 +12,7 @@ module PaloAlto
12
12
  class Log < Enumerator
13
13
  def initialize(client:, query:, log_type:, nlogs: 20, dir: :backward, show_detail: false, days: 7) # rubocop:disable Metrics/MethodLength,Metrics/ParameterLists
14
14
  @client = client
15
- payload = {
15
+ @log_query_payload = {
16
16
  type: 'log',
17
17
  'log-type': log_type,
18
18
  nlogs: nlogs,
@@ -22,15 +22,22 @@ module PaloAlto
22
22
  }
23
23
 
24
24
  if days
25
- payload[:query] += " AND (receive_time geq '#{(Time.now - days * 3600 * 24).strftime('%Y/%m/%d %H:%M:%S')}')"
25
+ @log_query_payload[:query] += " AND (receive_time geq '#{(Time.now - days * 3600 * 24).strftime('%Y/%m/%d %H:%M:%S')}')"
26
26
  end
27
27
 
28
- result = @client.execute(payload)
28
+ run_query
29
+
30
+ @first_result = fetch_result
31
+ super
32
+ end
33
+
34
+ def run_query
35
+ result = @client.execute(@log_query_payload)
29
36
  @job_id = result.at_xpath('response/result/job').text
37
+ warn "#{@client.host} #{Time.now}: Got job id #{@job_id} for log query"
38
+
30
39
  @count = nil
31
40
  @skip = 0
32
- @first_result = fetch_result
33
- super
34
41
  end
35
42
 
36
43
  def restore_first
@@ -46,17 +53,24 @@ module PaloAlto
46
53
  def fetch_result # rubocop:disable Metrics/MethodLength
47
54
  return nil if @count && @skip == @count
48
55
 
49
- payload = {
50
- type: 'log',
51
- action: 'get',
52
- 'job-id': @job_id,
53
- skip: @skip
54
- }
55
-
56
56
  i = 0
57
57
  loop do
58
58
  sleep 0.5 if i.positive?
59
- @current_result = @client.execute(payload)
59
+ begin
60
+ payload = {
61
+ type: 'log',
62
+ action: 'get',
63
+ 'job-id': @job_id,
64
+ skip: @skip
65
+ }
66
+ @current_result = @client.execute(payload)
67
+ rescue PaloAlto::UnknownErrorException => e
68
+ if e.message == 'Query timed out'
69
+ warn 'Retrying log query'
70
+ run_query
71
+ retry
72
+ end
73
+ end
60
74
  i += 1
61
75
  break if @current_result.at_xpath('response/result/job/status').text == 'FIN'
62
76
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PaloAlto
4
- VERSION = '0.3.2'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/palo_alto.rb CHANGED
@@ -30,7 +30,7 @@ module PaloAlto
30
30
  class UnknownCommandException < PermanentException
31
31
  end
32
32
 
33
- class InternalErrorsException < TemporaryException
33
+ class UnknownErrorException < TemporaryException
34
34
  end
35
35
 
36
36
  class BadXpathException < PermanentException
@@ -39,6 +39,9 @@ module PaloAlto
39
39
  class ObjectNotPresentException < PermanentException
40
40
  end
41
41
 
42
+ class ObjectNotFoundException < PermanentException
43
+ end
44
+
42
45
  class ObjectNotUniqueException < PermanentException
43
46
  end
44
47
 
@@ -122,6 +125,8 @@ module PaloAlto
122
125
  case response.code
123
126
  when '200'
124
127
  return response.body
128
+ when '401'
129
+ raise SessionTimedOutException
125
130
  when '400', '403'
126
131
  begin
127
132
  data = Nokogiri::XML.parse(response.body)
@@ -136,7 +141,7 @@ module PaloAlto
136
141
  end
137
142
 
138
143
  nil
139
- rescue Net::OpenTimeout, Errno::ECONNREFUSED => e
144
+ rescue Net::OpenTimeout, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
140
145
  raise ConnectionErrorException, e.message
141
146
  end
142
147
 
@@ -145,13 +150,12 @@ module PaloAlto
145
150
  when 400 then BadRequestException
146
151
  when 403 then ForbiddenException
147
152
  when 1 then UnknownCommandException
148
- when 2..5 then InternalErrorsException
149
153
  when 6 then BadXpathException
150
154
  when 7 then ObjectNotPresentException
151
155
  when 8 then ObjectNotUniqueException
152
156
  when 10 then ReferenceCountNotZeroException
153
- when 0, 11, 21 then InternalErrorException # also if there is no code..
154
157
  when 12 then InvalidObjectException
158
+ when 13 then ObjectNotFoundException
155
159
  when 14 then OperationNotPossibleException
156
160
  when 15 then OperationDeniedException
157
161
  when 16 then UnauthorizedException
@@ -159,7 +163,8 @@ module PaloAlto
159
163
  when 18 then MalformedCommandException
160
164
  when 19..20 then SuccessException
161
165
  when 22 then SessionTimedOutException
162
- else InternalErrorException
166
+ when 2..5, 11, 21 then InternalErrorException
167
+ else UnknownErrorException
163
168
  end
164
169
  raise error, message
165
170
  end
@@ -170,7 +175,7 @@ module PaloAlto
170
175
  attr_accessor :host, :username, :auth_key, :verify_ssl, :debug, :timeout
171
176
 
172
177
  def pretty_print_instance_variables
173
- super - [:@password, :@subclasses, :@subclasses, :@expression, :@arguments, :@cache]
178
+ super - [:@password, :@subclasses, :@subclasses, :@expression, :@arguments, :@cache, :@op, :@auth_key]
174
179
  end
175
180
 
176
181
  def execute(payload, skip_authentication: false, skip_cache: false)
@@ -202,7 +207,7 @@ module PaloAlto
202
207
  end
203
208
  end
204
209
 
205
- retried = false
210
+ retried = 0
206
211
  begin
207
212
  # configure options for the request
208
213
  options = {}
@@ -229,27 +234,57 @@ module PaloAlto
229
234
 
230
235
  data = Nokogiri::XML.parse(text)
231
236
  unless data.xpath('//response/@status').to_s == 'success'
232
- warn "command failed on host #{host} at #{Time.now}"
233
- warn "sent:\n#{options.inspect}\n" if debug.include?(:sent_on_error)
234
- warn "received:\n#{text.inspect}\n" if debug.include?(:received_on_error)
237
+ unless %w[op commit].include?(payload[:type]) # here we fail silent
238
+ warn "command failed on host #{host} at #{Time.now}"
239
+ warn "sent:\n#{options.inspect}\n" if debug.include?(:sent_on_error)
240
+ warn "received:\n#{text.inspect}\n" if debug.include?(:received_on_error)
241
+ end
235
242
  code = data.at_xpath('//response/@code')&.value.to_i # sometimes there is no code :( e.g. for 'op' errors
236
243
  message = data.xpath('/response/msg/line').map(&:text).map(&:strip).join("\n")
237
244
  Helpers::Rest.raise_error(code, message)
238
245
  end
239
246
 
240
247
  data
248
+ rescue EOFError, Net::ReadTimeout => e
249
+ max_retries = if %w[keygen config].include?(payload[:type])
250
+ # TODO: only retry on config, when it's get or edit, otherwise you may get strange errors
251
+ 3
252
+ else
253
+ 0
254
+ end
255
+
256
+ raise e if retried >= max_retries
257
+
258
+ retried += 1
259
+ warn "Got error #{e.inspect}; retrying (try #{retried})" if debug.include?(:warnings)
260
+ sleep 10
261
+ retry
241
262
  rescue TemporaryException => e
242
263
  dont_retry_at = [
243
264
  'Partial revert is not allowed. Full system commit must be completed.',
244
265
  'Config for scope ',
245
266
  'Config is not currently locked for scope ',
246
267
  'Commit lock is not currently held by',
247
- 'You already own a config lock for scope '
268
+ 'You already own a config lock for scope ',
269
+ 'This operation is blocked because of ',
270
+ 'Other administrators are holding config locks ',
271
+ 'Configuration is locked by ',
272
+ ' device-group' # device-group -> ... is already in use
248
273
  ]
249
- raise e if retried || dont_retry_at.any? { |x| e.message.start_with?(x) }
250
274
 
251
- warn "Got error #{e.inspect}; retrying" if debug.include?(:warnings)
252
- retried = true
275
+ max_retries = if dont_retry_at.any? { |str| e.message.start_with?(str) }
276
+ 0
277
+ elsif e.message.start_with?('Timed out while getting config lock. Please try again.')
278
+ 10
279
+ else
280
+ 1
281
+ end
282
+
283
+ raise e if retried >= max_retries
284
+
285
+ retried += 1
286
+ warn "Got error #{e.inspect}; retrying (try #{retried})" if debug.include?(:warnings)
287
+
253
288
  get_auth_key if e.is_a?(SessionTimedOutException)
254
289
  retry
255
290
  end
@@ -280,7 +315,7 @@ module PaloAlto
280
315
  def commit!(all: false, device_groups: nil, templates: nil,
281
316
  admins: [username],
282
317
  raw_result: false,
283
- wait_for_completion: true, wait: 5, timeout: 480)
318
+ wait_for_completion: true, wait: 5, timeout: 60 * 20)
284
319
  return nil if device_groups.is_a?(Array) && device_groups.empty? && templates.is_a?(Array) && templates.empty?
285
320
 
286
321
  cmd = if all
@@ -359,9 +394,10 @@ module PaloAlto
359
394
  cmd = { request: { "#{area}-lock": { add: { comment: comment || '(null)' } } } }
360
395
  op.execute(cmd, type: type, location: location)
361
396
  true
362
- rescue PaloAlto::InternalErrorException => e
397
+ rescue PaloAlto::UnknownErrorException => e
363
398
  return true if e.message.start_with?('You already own a config lock for scope ') ||
364
- e.message == "Config for scope shared is currently locked by #{username}"
399
+ e.message == "Config for scope shared is currently locked by #{username}" ||
400
+ e.message == "Config for scope #{location} is currently locked by #{username}"
365
401
 
366
402
  false
367
403
  end
@@ -375,7 +411,9 @@ module PaloAlto
375
411
  { request: { "#{area}-lock": 'remove' } }
376
412
  end
377
413
  op.execute(cmd, type: type, location: location)
378
- rescue PaloAlto::InternalErrorException
414
+ rescue PaloAlto::UnknownErrorException => e
415
+ return true if e.message.start_with?('Config is not currently locked')
416
+
379
417
  return false
380
418
  end
381
419
  true
@@ -411,7 +449,7 @@ module PaloAlto
411
449
 
412
450
  nil
413
451
  rescue => e
414
- warn [:job_query_error, e].inspect
452
+ warn [:job_query_error, @host, e].inspect
415
453
  false
416
454
  end
417
455
 
@@ -438,7 +476,7 @@ module PaloAlto
438
476
  job_result
439
477
  end
440
478
 
441
- def wait_for_job_completion(job_id, wait: 5, timeout: 600)
479
+ def wait_for_job_completion(job_id, wait: 5, timeout: 20 * 60)
442
480
  cmd = { show: { jobs: { id: job_id } } }
443
481
  start = Time.now
444
482
  loop do
@@ -447,11 +485,13 @@ module PaloAlto
447
485
  status = result.at_xpath('response/result/job/status')&.text
448
486
  return result unless %w[ACT PEND].include?(status)
449
487
  rescue => e
450
- warn [:job_query_error, e].inspect
488
+ warn [:job_query_error, Time.now, @host, e].inspect
489
+ return false if e.message =~ /\Ajob \d+ not found\z/
451
490
  end
452
491
  sleep wait
453
492
  break unless start + timeout > Time.now
454
493
  end
494
+ warn [:job_query_error, Time.now, @host, :timeout].inspect
455
495
  false
456
496
  end
457
497
 
data/palo_alto.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = 'Palo Alto API for Ruby'
12
12
  spec.homepage = 'https://github.com/Sebbb/'
13
- spec.license = 'artistic-2.0'
13
+ spec.license = 'Artistic-2.0'
14
14
  spec.required_ruby_version = '>= 2.7.0'
15
15
 
16
16
  spec.metadata['homepage_uri'] = spec.homepage
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.3.2
4
+ version: 0.4.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: 2023-01-09 00:00:00.000000000 Z
11
+ date: 2023-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -52,7 +52,7 @@ files:
52
52
  - palo_alto.gemspec
53
53
  homepage: https://github.com/Sebbb/
54
54
  licenses:
55
- - artistic-2.0
55
+ - Artistic-2.0
56
56
  metadata:
57
57
  homepage_uri: https://github.com/Sebbb/
58
58
  source_code_uri: https://github.com/Sebbb/palo_alto/