palo_alto 0.3.1 → 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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/lib/palo_alto/config.rb +77856 -53261
- data/lib/palo_alto/log.rb +27 -13
- data/lib/palo_alto/version.rb +1 -1
- data/lib/palo_alto.rb +157 -27
- data/palo_alto.gemspec +1 -1
- metadata +3 -3
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/palo_alto/version.rb
CHANGED
data/lib/palo_alto.rb
CHANGED
@@ -30,7 +30,7 @@ module PaloAlto
|
|
30
30
|
class UnknownCommandException < PermanentException
|
31
31
|
end
|
32
32
|
|
33
|
-
class
|
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
|
-
|
166
|
+
when 2..5, 11, 21 then InternalErrorException
|
167
|
+
else UnknownErrorException
|
163
168
|
end
|
164
169
|
raise error, message
|
165
170
|
end
|
@@ -170,15 +175,39 @@ 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]
|
178
|
+
super - [:@password, :@subclasses, :@subclasses, :@expression, :@arguments, :@cache, :@op, :@auth_key]
|
174
179
|
end
|
175
180
|
|
176
|
-
def execute(payload, skip_authentication: false)
|
181
|
+
def execute(payload, skip_authentication: false, skip_cache: false)
|
177
182
|
if !auth_key && !skip_authentication
|
178
183
|
get_auth_key
|
179
184
|
end
|
180
185
|
|
181
|
-
|
186
|
+
if payload[:type] == 'config' && !skip_cache
|
187
|
+
if payload[:action] == 'get'
|
188
|
+
start_time = Time.now
|
189
|
+
@cache.each do |cached_xpath, cache|
|
190
|
+
search_xpath = payload[:xpath].sub('/descendant::device-group[1]/', '/device-group/')
|
191
|
+
next unless search_xpath.start_with?(cached_xpath)
|
192
|
+
|
193
|
+
remove = cached_xpath.split('/')[1...-1].join('/').length
|
194
|
+
new_xpath = 'response/result/' + search_xpath[(remove+2)..]
|
195
|
+
|
196
|
+
results = cache.xpath(new_xpath)
|
197
|
+
xml = Nokogiri.parse("<?xml version=\"1.0\"?><response><result>#{results.to_s}</result></response>")
|
198
|
+
|
199
|
+
if debug.include?(:statistics)
|
200
|
+
warn "Elapsed for parsing cache: #{Time.now - start_time} seconds"
|
201
|
+
end
|
202
|
+
|
203
|
+
return xml
|
204
|
+
end
|
205
|
+
elsif !@keep_cache_on_edit
|
206
|
+
@cache = {}
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
retried = 0
|
182
211
|
begin
|
183
212
|
# configure options for the request
|
184
213
|
options = {}
|
@@ -198,47 +227,102 @@ module PaloAlto
|
|
198
227
|
start_time = Time.now
|
199
228
|
text = Helpers::Rest.make_request(options)
|
200
229
|
if debug.include?(:statistics)
|
201
|
-
warn "Elapsed for API call #{payload[:type]}/#{payload[:action] || '(unknown action)'} on #{host}: #{Time.now - start_time} seconds"
|
230
|
+
warn "Elapsed for API call #{payload[:type]}/#{payload[:action] || '(unknown action)'} on #{host}: #{Time.now - start_time} seconds, #{text.length} bytes"
|
202
231
|
end
|
203
232
|
|
204
233
|
warn "received: #{Time.now}\n#{text}\n" if debug.include?(:received)
|
205
234
|
|
206
235
|
data = Nokogiri::XML.parse(text)
|
207
236
|
unless data.xpath('//response/@status').to_s == 'success'
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
211
242
|
code = data.at_xpath('//response/@code')&.value.to_i # sometimes there is no code :( e.g. for 'op' errors
|
212
243
|
message = data.xpath('/response/msg/line').map(&:text).map(&:strip).join("\n")
|
213
244
|
Helpers::Rest.raise_error(code, message)
|
214
245
|
end
|
215
246
|
|
216
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
|
217
262
|
rescue TemporaryException => e
|
218
263
|
dont_retry_at = [
|
219
264
|
'Partial revert is not allowed. Full system commit must be completed.',
|
220
265
|
'Config for scope ',
|
221
266
|
'Config is not currently locked for scope ',
|
222
267
|
'Commit lock is not currently held by',
|
223
|
-
'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
|
224
273
|
]
|
225
|
-
raise e if retried || dont_retry_at.any? { |x| e.message.start_with?(x) }
|
226
274
|
|
227
|
-
|
228
|
-
|
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
|
+
|
229
288
|
get_auth_key if e.is_a?(SessionTimedOutException)
|
230
289
|
retry
|
231
290
|
end
|
232
291
|
end
|
233
292
|
|
234
|
-
def
|
293
|
+
def clear_cache!
|
294
|
+
@cache = {}
|
295
|
+
@keep_cache_on_edit = nil
|
296
|
+
end
|
297
|
+
|
298
|
+
def keep_cache_on_edit!
|
299
|
+
@keep_cache_on_edit = true
|
300
|
+
end
|
301
|
+
|
302
|
+
def cache!(xpath)
|
303
|
+
cached_xpath = xpath.is_a?(String) ? xpath : xpath.to_xpath
|
304
|
+
|
305
|
+
payload = {
|
306
|
+
type: 'config',
|
307
|
+
action: 'get',
|
308
|
+
xpath: cached_xpath
|
309
|
+
}
|
310
|
+
|
311
|
+
@cache[cached_xpath] = execute(payload, skip_cache: true)
|
312
|
+
true
|
313
|
+
end
|
314
|
+
|
315
|
+
def commit!(all: false, device_groups: nil, templates: nil,
|
316
|
+
admins: [username],
|
317
|
+
raw_result: false,
|
318
|
+
wait_for_completion: true, wait: 5, timeout: 60 * 20)
|
235
319
|
return nil if device_groups.is_a?(Array) && device_groups.empty? && templates.is_a?(Array) && templates.empty?
|
236
320
|
|
237
321
|
cmd = if all
|
238
322
|
'commit'
|
239
323
|
else
|
240
324
|
{ commit: { partial: [
|
241
|
-
{ 'admin':
|
325
|
+
{ 'admin': admins },
|
242
326
|
if device_groups
|
243
327
|
device_groups.empty? ? 'no-device-group' : { 'device-group': device_groups }
|
244
328
|
end,
|
@@ -256,8 +340,9 @@ module PaloAlto
|
|
256
340
|
end
|
257
341
|
result = op.execute(cmd)
|
258
342
|
|
259
|
-
|
343
|
+
return result if raw_result
|
260
344
|
|
345
|
+
job_id = result.at_xpath('response/result/job')&.text
|
261
346
|
return result unless job_id && wait_for_completion
|
262
347
|
|
263
348
|
wait_for_job_completion(job_id, wait: wait, timeout: timeout) if job_id
|
@@ -294,6 +379,7 @@ module PaloAlto
|
|
294
379
|
|
295
380
|
# will execute block if given and unlock afterwards. returns false if lock could not be aquired
|
296
381
|
def lock(area:, comment: nil, type: nil, location: nil)
|
382
|
+
raise MalformedCommandException, 'No type specified' if location && !type
|
297
383
|
if block_given?
|
298
384
|
return false unless lock(area: area, comment: comment, type: type, location: location)
|
299
385
|
|
@@ -308,8 +394,10 @@ module PaloAlto
|
|
308
394
|
cmd = { request: { "#{area}-lock": { add: { comment: comment || '(null)' } } } }
|
309
395
|
op.execute(cmd, type: type, location: location)
|
310
396
|
true
|
311
|
-
rescue PaloAlto::
|
312
|
-
return true if e.message.start_with?('You already own a config lock for scope ')
|
397
|
+
rescue PaloAlto::UnknownErrorException => e
|
398
|
+
return true if e.message.start_with?('You already own a config lock for scope ') ||
|
399
|
+
e.message == "Config for scope shared is currently locked by #{username}" ||
|
400
|
+
e.message == "Config for scope #{location} is currently locked by #{username}"
|
313
401
|
|
314
402
|
false
|
315
403
|
end
|
@@ -323,7 +411,9 @@ module PaloAlto
|
|
323
411
|
{ request: { "#{area}-lock": 'remove' } }
|
324
412
|
end
|
325
413
|
op.execute(cmd, type: type, location: location)
|
326
|
-
rescue PaloAlto::
|
414
|
+
rescue PaloAlto::UnknownErrorException => e
|
415
|
+
return true if e.message.start_with?('Config is not currently locked')
|
416
|
+
|
327
417
|
return false
|
328
418
|
end
|
329
419
|
true
|
@@ -337,9 +427,9 @@ module PaloAlto
|
|
337
427
|
end
|
338
428
|
end
|
339
429
|
|
340
|
-
def check_for_changes(
|
341
|
-
cmd = if
|
342
|
-
{ show: { config: { list: { 'change-summary': { partial: { admin:
|
430
|
+
def check_for_changes(admins: [username])
|
431
|
+
cmd = if admins
|
432
|
+
{ show: { config: { list: { 'change-summary': { partial: { admin: admins } } } } } }
|
343
433
|
else
|
344
434
|
{ show: { config: { list: 'change-summary' } } }
|
345
435
|
end
|
@@ -350,7 +440,43 @@ module PaloAlto
|
|
350
440
|
}
|
351
441
|
end
|
352
442
|
|
353
|
-
|
443
|
+
# returns nil if job isn't finished yet, otherwise the job result
|
444
|
+
def query_and_parse_job(job_id)
|
445
|
+
cmd = { show: { jobs: { id: job_id } } }
|
446
|
+
result = op.execute(cmd)
|
447
|
+
status = result.at_xpath('response/result/job/status')&.text
|
448
|
+
return result unless %w[ACT PEND].include?(status)
|
449
|
+
|
450
|
+
nil
|
451
|
+
rescue => e
|
452
|
+
warn [:job_query_error, @host, e].inspect
|
453
|
+
false
|
454
|
+
end
|
455
|
+
|
456
|
+
# returns true if successful
|
457
|
+
# returns nil if not completed yet
|
458
|
+
# otherwise returns the error
|
459
|
+
def commit_successful?(commit_result)
|
460
|
+
if commit_result.at_xpath('response/msg')&.text&.start_with?('The result of this commit would be the same as the previous commit queued/processed') ||
|
461
|
+
commit_result.at_xpath('response/msg')&.text == 'There are no changes to commit.'
|
462
|
+
return true
|
463
|
+
end
|
464
|
+
|
465
|
+
job_id = commit_result.at_xpath('response/result/job')&.text
|
466
|
+
unless job_id
|
467
|
+
warn [:no_job_id, result].inspect
|
468
|
+
return false
|
469
|
+
end
|
470
|
+
|
471
|
+
job_result = query_and_parse_job(job_id)
|
472
|
+
return job_result if !job_result # can be either nil or false (errored)
|
473
|
+
|
474
|
+
return true if job_result.xpath('response/result/job/details/line').text&.include?('Configuration committed successfully')
|
475
|
+
|
476
|
+
job_result
|
477
|
+
end
|
478
|
+
|
479
|
+
def wait_for_job_completion(job_id, wait: 5, timeout: 20 * 60)
|
354
480
|
cmd = { show: { jobs: { id: job_id } } }
|
355
481
|
start = Time.now
|
356
482
|
loop do
|
@@ -359,11 +485,13 @@ module PaloAlto
|
|
359
485
|
status = result.at_xpath('response/result/job/status')&.text
|
360
486
|
return result unless %w[ACT PEND].include?(status)
|
361
487
|
rescue => e
|
362
|
-
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/
|
363
490
|
end
|
364
491
|
sleep wait
|
365
492
|
break unless start + timeout > Time.now
|
366
493
|
end
|
494
|
+
warn [:job_query_error, Time.now, @host, :timeout].inspect
|
367
495
|
false
|
368
496
|
end
|
369
497
|
|
@@ -406,6 +534,8 @@ module PaloAlto
|
|
406
534
|
|
407
535
|
@subclasses = {}
|
408
536
|
|
537
|
+
@cache = {}
|
538
|
+
|
409
539
|
# xpath
|
410
540
|
@expression = :root
|
411
541
|
@arguments = [Expression.new(:this_node), []]
|
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 = '
|
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.
|
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:
|
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
|
-
-
|
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/
|