palo_alto 0.3.0 → 0.3.2
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 +3 -0
- data/examples/test_config.rb +4 -3
- data/examples/test_op.rb +60 -63
- data/lib/palo_alto/config.rb +10623 -10600
- data/lib/palo_alto/version.rb +1 -1
- data/lib/palo_alto.rb +137 -36
- metadata +2 -2
data/lib/palo_alto/version.rb
CHANGED
data/lib/palo_alto.rb
CHANGED
@@ -74,7 +74,6 @@ module PaloAlto
|
|
74
74
|
|
75
75
|
module Helpers
|
76
76
|
class Rest
|
77
|
-
@http_clients = {} # will include [http_client, lock]
|
78
77
|
@global_lock = Mutex.new
|
79
78
|
|
80
79
|
def self.make_request(opts)
|
@@ -91,16 +90,18 @@ module PaloAlto
|
|
91
90
|
options = options.merge(opts)
|
92
91
|
options[:headers].merge!(headers)
|
93
92
|
|
94
|
-
http_client =
|
93
|
+
http_client = nil
|
95
94
|
|
96
95
|
@global_lock.synchronize do
|
97
|
-
|
96
|
+
Thread.current[:http_clients] ||= {}
|
97
|
+
|
98
|
+
unless (http_client = Thread.current[:http_clients][options[:host]])
|
98
99
|
http_client = Net::HTTP.new(options[:host], 443)
|
99
100
|
http_client.use_ssl = true
|
100
101
|
http_client.verify_mode = options[:verify_ssl]
|
101
102
|
http_client.read_timeout = http_client.open_timeout = (options[:timeout] || 60)
|
102
103
|
http_client.set_debug_output(options[:debug].include?(:http) ? $stdout : nil)
|
103
|
-
|
104
|
+
Thread.current[:http_clients][options[:host]] = http_client
|
104
105
|
end
|
105
106
|
end
|
106
107
|
|
@@ -114,11 +115,9 @@ module PaloAlto
|
|
114
115
|
post_req.set_form_data(payload)
|
115
116
|
end
|
116
117
|
|
117
|
-
|
118
|
-
http_client.start unless http_client.started?
|
118
|
+
http_client.start unless http_client.started?
|
119
119
|
|
120
|
-
|
121
|
-
end
|
120
|
+
response = http_client.request(post_req)
|
122
121
|
|
123
122
|
case response.code
|
124
123
|
when '200'
|
@@ -168,9 +167,41 @@ module PaloAlto
|
|
168
167
|
end
|
169
168
|
|
170
169
|
class XML
|
171
|
-
attr_accessor :host, :username, :
|
170
|
+
attr_accessor :host, :username, :auth_key, :verify_ssl, :debug, :timeout
|
171
|
+
|
172
|
+
def pretty_print_instance_variables
|
173
|
+
super - [:@password, :@subclasses, :@subclasses, :@expression, :@arguments, :@cache]
|
174
|
+
end
|
175
|
+
|
176
|
+
def execute(payload, skip_authentication: false, skip_cache: false)
|
177
|
+
if !auth_key && !skip_authentication
|
178
|
+
get_auth_key
|
179
|
+
end
|
180
|
+
|
181
|
+
if payload[:type] == 'config' && !skip_cache
|
182
|
+
if payload[:action] == 'get'
|
183
|
+
start_time = Time.now
|
184
|
+
@cache.each do |cached_xpath, cache|
|
185
|
+
search_xpath = payload[:xpath].sub('/descendant::device-group[1]/', '/device-group/')
|
186
|
+
next unless search_xpath.start_with?(cached_xpath)
|
187
|
+
|
188
|
+
remove = cached_xpath.split('/')[1...-1].join('/').length
|
189
|
+
new_xpath = 'response/result/' + search_xpath[(remove+2)..]
|
190
|
+
|
191
|
+
results = cache.xpath(new_xpath)
|
192
|
+
xml = Nokogiri.parse("<?xml version=\"1.0\"?><response><result>#{results.to_s}</result></response>")
|
193
|
+
|
194
|
+
if debug.include?(:statistics)
|
195
|
+
warn "Elapsed for parsing cache: #{Time.now - start_time} seconds"
|
196
|
+
end
|
197
|
+
|
198
|
+
return xml
|
199
|
+
end
|
200
|
+
elsif !@keep_cache_on_edit
|
201
|
+
@cache = {}
|
202
|
+
end
|
203
|
+
end
|
172
204
|
|
173
|
-
def execute(payload)
|
174
205
|
retried = false
|
175
206
|
begin
|
176
207
|
# configure options for the request
|
@@ -191,14 +222,14 @@ module PaloAlto
|
|
191
222
|
start_time = Time.now
|
192
223
|
text = Helpers::Rest.make_request(options)
|
193
224
|
if debug.include?(:statistics)
|
194
|
-
warn "Elapsed for API call #{payload[:type]}/#{payload[:action] || '(unknown action)'}: #{Time.now - start_time} seconds"
|
225
|
+
warn "Elapsed for API call #{payload[:type]}/#{payload[:action] || '(unknown action)'} on #{host}: #{Time.now - start_time} seconds, #{text.length} bytes"
|
195
226
|
end
|
196
227
|
|
197
228
|
warn "received: #{Time.now}\n#{text}\n" if debug.include?(:received)
|
198
229
|
|
199
230
|
data = Nokogiri::XML.parse(text)
|
200
231
|
unless data.xpath('//response/@status').to_s == 'success'
|
201
|
-
warn
|
232
|
+
warn "command failed on host #{host} at #{Time.now}"
|
202
233
|
warn "sent:\n#{options.inspect}\n" if debug.include?(:sent_on_error)
|
203
234
|
warn "received:\n#{text.inspect}\n" if debug.include?(:received_on_error)
|
204
235
|
code = data.at_xpath('//response/@code')&.value.to_i # sometimes there is no code :( e.g. for 'op' errors
|
@@ -224,16 +255,45 @@ module PaloAlto
|
|
224
255
|
end
|
225
256
|
end
|
226
257
|
|
227
|
-
def
|
258
|
+
def clear_cache!
|
259
|
+
@cache = {}
|
260
|
+
@keep_cache_on_edit = nil
|
261
|
+
end
|
262
|
+
|
263
|
+
def keep_cache_on_edit!
|
264
|
+
@keep_cache_on_edit = true
|
265
|
+
end
|
266
|
+
|
267
|
+
def cache!(xpath)
|
268
|
+
cached_xpath = xpath.is_a?(String) ? xpath : xpath.to_xpath
|
269
|
+
|
270
|
+
payload = {
|
271
|
+
type: 'config',
|
272
|
+
action: 'get',
|
273
|
+
xpath: cached_xpath
|
274
|
+
}
|
275
|
+
|
276
|
+
@cache[cached_xpath] = execute(payload, skip_cache: true)
|
277
|
+
true
|
278
|
+
end
|
279
|
+
|
280
|
+
def commit!(all: false, device_groups: nil, templates: nil,
|
281
|
+
admins: [username],
|
282
|
+
raw_result: false,
|
283
|
+
wait_for_completion: true, wait: 5, timeout: 480)
|
228
284
|
return nil if device_groups.is_a?(Array) && device_groups.empty? && templates.is_a?(Array) && templates.empty?
|
229
285
|
|
230
286
|
cmd = if all
|
231
287
|
'commit'
|
232
288
|
else
|
233
289
|
{ commit: { partial: [
|
234
|
-
{ 'admin':
|
235
|
-
|
236
|
-
|
290
|
+
{ 'admin': admins },
|
291
|
+
if device_groups
|
292
|
+
device_groups.empty? ? 'no-device-group' : { 'device-group': device_groups }
|
293
|
+
end,
|
294
|
+
if templates
|
295
|
+
templates.empty? ? 'no-template' : { 'template': templates }
|
296
|
+
end,
|
237
297
|
'no-template-stack',
|
238
298
|
'no-log-collector',
|
239
299
|
'no-log-collector-group',
|
@@ -245,8 +305,9 @@ module PaloAlto
|
|
245
305
|
end
|
246
306
|
result = op.execute(cmd)
|
247
307
|
|
248
|
-
|
308
|
+
return result if raw_result
|
249
309
|
|
310
|
+
job_id = result.at_xpath('response/result/job')&.text
|
250
311
|
return result unless job_id && wait_for_completion
|
251
312
|
|
252
313
|
wait_for_job_completion(job_id, wait: wait, timeout: timeout) if job_id
|
@@ -283,6 +344,7 @@ module PaloAlto
|
|
283
344
|
|
284
345
|
# will execute block if given and unlock afterwards. returns false if lock could not be aquired
|
285
346
|
def lock(area:, comment: nil, type: nil, location: nil)
|
347
|
+
raise MalformedCommandException, 'No type specified' if location && !type
|
286
348
|
if block_given?
|
287
349
|
return false unless lock(area: area, comment: comment, type: type, location: location)
|
288
350
|
|
@@ -298,7 +360,8 @@ module PaloAlto
|
|
298
360
|
op.execute(cmd, type: type, location: location)
|
299
361
|
true
|
300
362
|
rescue PaloAlto::InternalErrorException => e
|
301
|
-
return true if e.message.start_with?('You already own a config lock for scope ')
|
363
|
+
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}"
|
302
365
|
|
303
366
|
false
|
304
367
|
end
|
@@ -326,9 +389,9 @@ module PaloAlto
|
|
326
389
|
end
|
327
390
|
end
|
328
391
|
|
329
|
-
def check_for_changes(
|
330
|
-
cmd = if
|
331
|
-
{ show: { config: { list: { 'change-summary': { partial: { admin:
|
392
|
+
def check_for_changes(admins: [username])
|
393
|
+
cmd = if admins
|
394
|
+
{ show: { config: { list: { 'change-summary': { partial: { admin: admins } } } } } }
|
332
395
|
else
|
333
396
|
{ show: { config: { list: 'change-summary' } } }
|
334
397
|
end
|
@@ -339,14 +402,53 @@ module PaloAlto
|
|
339
402
|
}
|
340
403
|
end
|
341
404
|
|
342
|
-
|
405
|
+
# returns nil if job isn't finished yet, otherwise the job result
|
406
|
+
def query_and_parse_job(job_id)
|
407
|
+
cmd = { show: { jobs: { id: job_id } } }
|
408
|
+
result = op.execute(cmd)
|
409
|
+
status = result.at_xpath('response/result/job/status')&.text
|
410
|
+
return result unless %w[ACT PEND].include?(status)
|
411
|
+
|
412
|
+
nil
|
413
|
+
rescue => e
|
414
|
+
warn [:job_query_error, e].inspect
|
415
|
+
false
|
416
|
+
end
|
417
|
+
|
418
|
+
# returns true if successful
|
419
|
+
# returns nil if not completed yet
|
420
|
+
# otherwise returns the error
|
421
|
+
def commit_successful?(commit_result)
|
422
|
+
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') ||
|
423
|
+
commit_result.at_xpath('response/msg')&.text == 'There are no changes to commit.'
|
424
|
+
return true
|
425
|
+
end
|
426
|
+
|
427
|
+
job_id = commit_result.at_xpath('response/result/job')&.text
|
428
|
+
unless job_id
|
429
|
+
warn [:no_job_id, result].inspect
|
430
|
+
return false
|
431
|
+
end
|
432
|
+
|
433
|
+
job_result = query_and_parse_job(job_id)
|
434
|
+
return job_result if !job_result # can be either nil or false (errored)
|
435
|
+
|
436
|
+
return true if job_result.xpath('response/result/job/details/line').text&.include?('Configuration committed successfully')
|
437
|
+
|
438
|
+
job_result
|
439
|
+
end
|
440
|
+
|
441
|
+
def wait_for_job_completion(job_id, wait: 5, timeout: 600)
|
343
442
|
cmd = { show: { jobs: { id: job_id } } }
|
344
443
|
start = Time.now
|
345
444
|
loop do
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
445
|
+
begin
|
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
|
+
rescue => e
|
450
|
+
warn [:job_query_error, e].inspect
|
451
|
+
end
|
350
452
|
sleep wait
|
351
453
|
break unless start + timeout > Time.now
|
352
454
|
end
|
@@ -384,20 +486,19 @@ module PaloAlto
|
|
384
486
|
end
|
385
487
|
|
386
488
|
def initialize(host:, username:, password:, verify_ssl: OpenSSL::SSL::VERIFY_NONE, debug: [])
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
489
|
+
@host = host
|
490
|
+
@username = username
|
491
|
+
@password = password
|
492
|
+
@verify_ssl = verify_ssl
|
493
|
+
@debug = debug
|
392
494
|
|
393
495
|
@subclasses = {}
|
394
496
|
|
497
|
+
@cache = {}
|
498
|
+
|
395
499
|
# xpath
|
396
500
|
@expression = :root
|
397
501
|
@arguments = [Expression.new(:this_node), []]
|
398
|
-
|
399
|
-
# attempt to obtain the auth_key
|
400
|
-
get_auth_key
|
401
502
|
end
|
402
503
|
|
403
504
|
# Perform a query to the API endpoint for an auth_key based on the credentials provided
|
@@ -405,10 +506,10 @@ module PaloAlto
|
|
405
506
|
# establish the required options for the key request
|
406
507
|
payload = { type: 'keygen',
|
407
508
|
user: username,
|
408
|
-
password: password }
|
509
|
+
password: @password }
|
409
510
|
|
410
511
|
# get and parse the response for the key
|
411
|
-
xml_data = execute(payload)
|
512
|
+
xml_data = execute(payload, skip_authentication: true)
|
412
513
|
self.auth_key = xml_data.xpath('//response/result/key')[0].content
|
413
514
|
end
|
414
515
|
end
|
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.
|
4
|
+
version: 0.3.2
|
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-01-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|