palo_alto 0.2.7 → 0.3.1

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.2.7'
4
+ VERSION = '0.3.1'
5
5
  end
data/lib/palo_alto.rb CHANGED
@@ -74,30 +74,36 @@ module PaloAlto
74
74
 
75
75
  module Helpers
76
76
  class Rest
77
+ @global_lock = Mutex.new
78
+
77
79
  def self.make_request(opts)
78
80
  options = {}
79
- options[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
80
- options[:timeout] = 60
81
+ options[:verify_ssl] ||= OpenSSL::SSL::VERIFY_PEER
81
82
 
82
- headers = {}
83
- headers['User-Agent'] = 'ruby-keystone-client'
84
- headers['Accept'] = 'application/xml'
85
- headers['Content-Type'] = 'application/x-www-form-urlencoded'
83
+ headers = {
84
+ 'User-Agent': 'ruby-keystone-client',
85
+ 'Accept': 'application/xml',
86
+ 'Content-Type': 'application/x-www-form-urlencoded'
87
+ }
86
88
 
87
89
  # merge in settings from method caller
88
90
  options = options.merge(opts)
89
91
  options[:headers].merge!(headers)
90
92
 
91
- thread = Thread.current
92
- unless thread[:http]
93
- thread[:http] = Net::HTTP.new(options[:host], options[:port])
94
- thread[:http].use_ssl = true
95
- thread[:http].verify_mode = options[:verify_ssl]
96
- thread[:http].read_timeout = thread[:http].open_timeout = options[:timeout]
97
- thread[:http].set_debug_output($stdout) if options[:debug].include?(:http)
98
- end
93
+ http_client = nil
99
94
 
100
- thread[:http].start unless thread[:http].started?
95
+ @global_lock.synchronize do
96
+ Thread.current[:http_clients] ||= {}
97
+
98
+ unless (http_client = Thread.current[:http_clients][options[:host]])
99
+ http_client = Net::HTTP.new(options[:host], 443)
100
+ http_client.use_ssl = true
101
+ http_client.verify_mode = options[:verify_ssl]
102
+ http_client.read_timeout = http_client.open_timeout = (options[:timeout] || 60)
103
+ http_client.set_debug_output(options[:debug].include?(:http) ? $stdout : nil)
104
+ Thread.current[:http_clients][options[:host]] = http_client
105
+ end
106
+ end
101
107
 
102
108
  payload = options[:payload]
103
109
  post_req = Net::HTTP::Post.new('/api/', options[:headers])
@@ -109,7 +115,9 @@ module PaloAlto
109
115
  post_req.set_form_data(payload)
110
116
  end
111
117
 
112
- response = thread[:http].request(post_req)
118
+ http_client.start unless http_client.started?
119
+
120
+ response = http_client.request(post_req)
113
121
 
114
122
  case response.code
115
123
  when '200'
@@ -159,18 +167,26 @@ module PaloAlto
159
167
  end
160
168
 
161
169
  class XML
162
- attr_accessor :host, :port, :username, :password, :auth_key, :verify_ssl, :debug
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]
174
+ end
175
+
176
+ def execute(payload, skip_authentication: false)
177
+ if !auth_key && !skip_authentication
178
+ get_auth_key
179
+ end
163
180
 
164
- def execute(payload)
165
181
  retried = false
166
182
  begin
167
183
  # configure options for the request
168
184
  options = {}
169
185
  options[:host] = host
170
- options[:port] = port
171
186
  options[:verify_ssl] = verify_ssl
172
187
  options[:payload] = payload
173
188
  options[:debug] = debug
189
+ options[:timeout] = timeout || 180
174
190
  options[:headers] = if payload[:type] == 'keygen'
175
191
  {}
176
192
  else
@@ -182,14 +198,14 @@ module PaloAlto
182
198
  start_time = Time.now
183
199
  text = Helpers::Rest.make_request(options)
184
200
  if debug.include?(:statistics)
185
- warn "Elapsed for API call #{payload[:type]}/#{payload[:action] || '(unknown action)'}: #{Time.now - start_time} seconds"
201
+ warn "Elapsed for API call #{payload[:type]}/#{payload[:action] || '(unknown action)'} on #{host}: #{Time.now - start_time} seconds"
186
202
  end
187
203
 
188
204
  warn "received: #{Time.now}\n#{text}\n" if debug.include?(:received)
189
205
 
190
206
  data = Nokogiri::XML.parse(text)
191
207
  unless data.xpath('//response/@status').to_s == 'success'
192
- warn 'command failed'
208
+ warn "command failed on host #{host} at #{Time.now}"
193
209
  warn "sent:\n#{options.inspect}\n" if debug.include?(:sent_on_error)
194
210
  warn "received:\n#{text.inspect}\n" if debug.include?(:received_on_error)
195
211
  code = data.at_xpath('//response/@code')&.value.to_i # sometimes there is no code :( e.g. for 'op' errors
@@ -206,27 +222,29 @@ module PaloAlto
206
222
  'Commit lock is not currently held by',
207
223
  'You already own a config lock for scope '
208
224
  ]
209
- if retried || dont_retry_at.any? { |x| e.message.start_with?(x) }
210
- raise e
211
- else
212
- warn "Got error #{e.inspect}; retrying" if debug.include?(:warnings)
213
- retried = true
214
- get_auth_key if e.is_a?(SessionTimedOutException)
215
- retry
216
- end
225
+ raise e if retried || dont_retry_at.any? { |x| e.message.start_with?(x) }
226
+
227
+ warn "Got error #{e.inspect}; retrying" if debug.include?(:warnings)
228
+ retried = true
229
+ get_auth_key if e.is_a?(SessionTimedOutException)
230
+ retry
217
231
  end
218
232
  end
219
233
 
220
- def commit!(all: false, device_groups: nil, wait_for_completion: true)
221
- return nil if device_groups.is_a?(Array) && device_groups.empty?
234
+ def commit!(all: false, device_groups: nil, templates: nil, wait_for_completion: true, wait: 5, timeout: 480)
235
+ return nil if device_groups.is_a?(Array) && device_groups.empty? && templates.is_a?(Array) && templates.empty?
222
236
 
223
237
  cmd = if all
224
238
  'commit'
225
239
  else
226
240
  { commit: { partial: [
227
241
  { 'admin': [username] },
228
- device_groups ? { 'device-group': device_groups } : nil,
229
- 'no-template',
242
+ if device_groups
243
+ device_groups.empty? ? 'no-device-group' : { 'device-group': device_groups }
244
+ end,
245
+ if templates
246
+ templates.empty? ? 'no-template' : { 'template': templates }
247
+ end,
230
248
  'no-template-stack',
231
249
  'no-log-collector',
232
250
  'no-log-collector-group',
@@ -238,10 +256,11 @@ module PaloAlto
238
256
  end
239
257
  result = op.execute(cmd)
240
258
 
241
- return result unless wait_for_completion
242
-
243
259
  job_id = result.at_xpath('response/result/job')&.text
244
- wait_for_job_completion(job_id) if job_id
260
+
261
+ return result unless job_id && wait_for_completion
262
+
263
+ wait_for_job_completion(job_id, wait: wait, timeout: timeout) if job_id
245
264
  end
246
265
 
247
266
  def full_commit_required?
@@ -289,7 +308,9 @@ module PaloAlto
289
308
  cmd = { request: { "#{area}-lock": { add: { comment: comment || '(null)' } } } }
290
309
  op.execute(cmd, type: type, location: location)
291
310
  true
292
- rescue PaloAlto::InternalErrorException
311
+ rescue PaloAlto::InternalErrorException => e
312
+ return true if e.message.start_with?('You already own a config lock for scope ')
313
+
293
314
  false
294
315
  end
295
316
  end
@@ -329,21 +350,25 @@ module PaloAlto
329
350
  }
330
351
  end
331
352
 
332
- def wait_for_job_completion(job_id, wait: 5, timeout: 300)
353
+ def wait_for_job_completion(job_id, wait: 5, timeout: 600)
333
354
  cmd = { show: { jobs: { id: job_id } } }
334
355
  start = Time.now
335
356
  loop do
336
- result = op.execute(cmd)
337
- status = result.at_xpath('response/result/job/status')&.text
338
- return result unless %w[ACT PEND].include?(status)
339
-
357
+ begin
358
+ result = op.execute(cmd)
359
+ status = result.at_xpath('response/result/job/status')&.text
360
+ return result unless %w[ACT PEND].include?(status)
361
+ rescue => e
362
+ warn [:job_query_error, e].inspect
363
+ end
340
364
  sleep wait
341
365
  break unless start + timeout > Time.now
342
366
  end
343
367
  false
344
368
  end
345
369
 
346
- def revert!(all: false)
370
+ # wait: how long revert is retried (every 10 seconds)
371
+ def revert!(all: false, wait: 60)
347
372
  cmd = if all
348
373
  { revert: 'config' }
349
374
  else
@@ -359,26 +384,31 @@ module PaloAlto
359
384
  { 'shared-object': 'excluded' }
360
385
  ] } } }
361
386
  end
362
- op.execute(cmd)
387
+
388
+ waited = 0
389
+ begin
390
+ op.execute(cmd)
391
+ rescue StandardError => e
392
+ puts 'Revert failed; waiting and retrying'
393
+ sleep 10
394
+ waited += 1
395
+ retry while waited < wait
396
+ raise e
397
+ end
363
398
  end
364
399
 
365
- def initialize(host:, port:, username:, password:, debug: [])
366
- self.host = host
367
- self.port = port
368
- self.username = username
369
- self.password = password
370
- self.verify_ssl = OpenSSL::SSL::VERIFY_NONE
371
- self.debug = debug
400
+ def initialize(host:, username:, password:, verify_ssl: OpenSSL::SSL::VERIFY_NONE, debug: [])
401
+ @host = host
402
+ @username = username
403
+ @password = password
404
+ @verify_ssl = verify_ssl
405
+ @debug = debug
372
406
 
373
407
  @subclasses = {}
374
408
 
375
409
  # xpath
376
410
  @expression = :root
377
411
  @arguments = [Expression.new(:this_node), []]
378
-
379
- # attempt to obtain the auth_key
380
- # raise 'Exception attempting to obtain the auth_key' if (self.class.auth_key = get_auth_key).nil?
381
- get_auth_key
382
412
  end
383
413
 
384
414
  # Perform a query to the API endpoint for an auth_key based on the credentials provided
@@ -386,10 +416,10 @@ module PaloAlto
386
416
  # establish the required options for the key request
387
417
  payload = { type: 'keygen',
388
418
  user: username,
389
- password: password }
419
+ password: @password }
390
420
 
391
421
  # get and parse the response for the key
392
- xml_data = execute(payload)
422
+ xml_data = execute(payload, skip_authentication: true)
393
423
  self.auth_key = xml_data.xpath('//response/result/key')[0].content
394
424
  end
395
425
  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.2.7
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Roesner
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-19 00:00:00.000000000 Z
11
+ date: 2022-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri