palo_alto 0.1.4 → 0.1.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be267a4c14845513d0ca0b93dcdf2673829983dbf58c6c321f57817bd30f3d82
4
- data.tar.gz: 268e375e0f17b1b5eb1a7dea2fb838a7fe55047bfabe66def538f7afdaf8111f
3
+ metadata.gz: aa30215e631895511ecb3645281eb160db2df58f0a6f6351d2d88069d80d1adc
4
+ data.tar.gz: e9acd3ba580ac92ec84546fc626ab52d9b7cfdd27591bb7b702082d3ddb1e791
5
5
  SHA512:
6
- metadata.gz: f71277a31b9157ebee6b7045327998e483faa68329158d97d70deca570c5486c86eaf3505458b89275991bae635e8e0bdc99e8115b55356a5fcdf43fc32d96bb
7
- data.tar.gz: '084b3b801f5c1390dd668bc45f1c29ddbe00202430c2f68baff227c469d8853575bcd14243c03823bb58f25b4175de2d26bf6f64280a56d2b2155d5e06444248'
6
+ metadata.gz: c556ea99594a4c09967e3271a0250d9864856ef42e8f89e518fe28b4054aeffc40160f5a1c298ebf5c77937c10d25a6bb5443eef64d0c03708bf9283c5647203
7
+ data.tar.gz: cc4a72eeea889757961e407712a9067cf54ee1fad39b394d3e29e230dbc8f306e9f53df2966ed576298135b9afb8496a77e4f372dd63c4d30a493653ca5b5136
@@ -1,4 +1,4 @@
1
- # generated: 2021-08-27 17:33:49 +0200
1
+ # generated: 2021-10-19 00:56:02 +0200
2
2
  require 'openssl'
3
3
  require 'nokogiri'
4
4
 
@@ -268,7 +268,11 @@ module PaloAlto
268
268
  end
269
269
 
270
270
  def binary_operator(name, left, right)
271
- "#{left}#{name}#{right}".gsub('./@', '@')
271
+ if %w(and or).include?(name)
272
+ "(#{left} #{name} #{right})".gsub('./@', '@')
273
+ else
274
+ "#{left}#{name}#{right}".gsub('./@', '@')
275
+ end
272
276
  end
273
277
 
274
278
  def root(current, element_names)
@@ -425,7 +429,7 @@ module PaloAlto
425
429
  start_time=Time.now
426
430
  result = self.parent_instance.dup.create!.clear!.external_set(data.xpath('//response/result').first)
427
431
  if XML.debug.include?(:statistics)
428
- puts "Elapsed for parsing #{result.length} results: #{Time.now-start_time} seconds"
432
+ warn "Elapsed for parsing #{result.length} results: #{Time.now-start_time} seconds"
429
433
  end
430
434
  result
431
435
  end
@@ -461,19 +465,19 @@ module PaloAlto
461
465
  else
462
466
  @create_children=true
463
467
  n = data.xpath('//response/result/*')
468
+ if n.any?
469
+ clear!
470
+ external_set(n.first)
464
471
 
465
- clear!
466
- external_set(n.first)
467
-
468
- if is_a?(ArrayConfigClass)
469
- primary_key = get_primary_key(n.first.attribute_nodes, self.class.props)
470
- set_array_class_attributes(n.first, primary_key) # primary key, api_attributes
472
+ if is_a?(ArrayConfigClass)
473
+ primary_key = get_primary_key(n.first.attribute_nodes, self.class.props)
474
+ set_array_class_attributes(n.first, primary_key) # primary key, api_attributes
475
+ end
471
476
  end
472
-
473
477
  self
474
478
  end.tap do
475
479
  if XML.debug.include?(:statistics)
476
- puts "Elapsed for parsing: #{Time.now-start_time} seconds"
480
+ warn "Elapsed for parsing: #{Time.now-start_time} seconds"
477
481
  end
478
482
  end
479
483
  end
@@ -531,9 +535,9 @@ module PaloAlto
531
535
  when 'bool'
532
536
  return true if ['yes', true].include?(value)
533
537
  return false if ['no', false].include?(value)
534
- raise ArgumentError, 'Not bool: ' + value.inspect
538
+ raise ArgumentError, "Not bool: #{value.inspect}"
535
539
  when 'string', 'ipdiscontmask', 'iprangespec', 'ipspec', 'rangelistspec'
536
- raise(ArgumentError, 'Not string') unless value.is_a?(String)
540
+ raise(ArgumentError, "Not string: #{value.inspect}") unless value.is_a?(String)
537
541
  if prop_arr['regex']
538
542
  raise ArgumentError, "Not matching regex: #{value.inspect} (#{prop_arr["regex"].inspect})" unless value.match(prop_arr["regex"])
539
543
  end
@@ -667,28 +671,34 @@ module PaloAlto
667
671
  elsif @external_values.has_key?(prop)
668
672
  return @external_values[prop]
669
673
  elsif my_prop.has_key?("default") && include_defaults
670
- return enforce_type(my_prop, my_prop['default'])
674
+ return enforce_types(my_prop, my_prop['default'])
671
675
  else
672
676
  return nil
673
677
  end
674
678
  end
675
679
 
676
- def prop_set(prop, value)
677
- my_prop = self.class.props[prop] or raise(InternalErrorException, "Unknown attribute for #{self.class}: #{prop}")
680
+ def enforce_types(prop_arr, values)
681
+ return if values.nil?
678
682
 
679
- if has_multiple_values? && value.is_a?(String)
680
- value = value.split(/\s+/)
683
+ if has_multiple_values? && values.is_a?(String)
684
+ values = values.split(/\s+/)
681
685
  end
682
686
 
683
- if value.is_a?(Array)
684
- @values[prop] = value.map{|v| enforce_type(my_prop, v)}
685
- elsif value.nil?
686
- @values[prop] = nil
687
+ if values.is_a?(Array) && has_multiple_values?
688
+ values.map{|v| enforce_type(prop_arr, v)}
689
+ elsif !has_multiple_values?
690
+ enforce_type(prop_arr, values)
687
691
  else
688
- @values[prop] = enforce_type(my_prop, value)
692
+ raise(ArgumentError, 'Needs to be Array but is not, or vice versa')
689
693
  end
690
694
  end
691
695
 
696
+ def prop_set(prop, value)
697
+ my_prop = self.class.props[prop] or raise(InternalErrorException, "Unknown attribute for #{self.class}: #{prop}")
698
+
699
+ @values[prop] = enforce_types(my_prop, value)
700
+ end
701
+
692
702
  def to_xml(changed_only:, full_tree:, include_root: )
693
703
  builder = Nokogiri::XML::Builder.new{|xml|
694
704
  xml.send(self._section, (self.selector rescue nil)) {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PaloAlto
4
- VERSION = '0.1.4'
4
+ VERSION = '0.1.8'
5
5
  end
data/lib/palo_alto.rb CHANGED
@@ -71,13 +71,13 @@ module PaloAlto
71
71
 
72
72
  class SessionTimedOutException < TemporaryException
73
73
  end
74
-
74
+
75
75
  module Helpers
76
76
  class Rest
77
77
  def self.make_request(opts)
78
- options = {}
79
- options[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
80
- options[:timeout] = 60
78
+ options = {}
79
+ options[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
80
+ options[:timeout] = 60
81
81
 
82
82
  headers = {}
83
83
  headers['User-Agent'] = 'ruby-keystone-client'
@@ -101,22 +101,23 @@ module PaloAlto
101
101
 
102
102
  response = thread[:http].post('/api/', URI.encode_www_form(options[:payload]), options[:headers])
103
103
 
104
- if response.code == '200'
104
+ case response.code
105
+ when '200'
105
106
  return response.body
106
- elsif response.code == '400' or response.code == '403'
107
+ when '400', '403'
107
108
  begin
108
109
  data = Nokogiri::XML.parse(response.body)
109
110
  message = data.xpath('//response/response/msg').text
110
111
  code = response.code.to_i
111
- rescue
112
+ rescue StandardError
112
113
  raise ConnectionErrorException, "#{response.code} #{response.message}"
113
114
  end
114
115
  raise_error(code, message)
115
116
  else
116
117
  raise ConnectionErrorException, "#{response.code} #{response.message}"
117
118
  end
118
- nil
119
119
 
120
+ nil
120
121
  rescue Net::OpenTimeout, Errno::ECONNREFUSED => e
121
122
  raise ConnectionErrorException, e.message
122
123
  end
@@ -141,7 +142,7 @@ module PaloAlto
141
142
  when 19..20 then SuccessException
142
143
  when 22 then SessionTimedOutException
143
144
  else InternalErrorException
144
- end
145
+ end
145
146
  raise error, message
146
147
  end
147
148
 
@@ -155,36 +156,27 @@ module PaloAlto
155
156
  options[:payload] = payload
156
157
  options[:headers] = headers
157
158
 
158
- if XML.debug.include?(:sent)
159
- warn "sent: (#{Time.now}\n#{options.pretty_inspect}\n"
160
- end
159
+ warn "sent: (#{Time.now}\n#{options.pretty_inspect}\n" if XML.debug.include?(:sent)
161
160
 
162
161
  start_time = Time.now
163
162
  text = Helpers::Rest.make_request(options)
164
163
  if XML.debug.include?(:statistics)
165
- warn "Elapsed for API call #{payload[:type]}/#{payload[:action]||'(unknown action)'}: #{Time.now-start_time} seconds"
164
+ warn "Elapsed for API call #{payload[:type]}/#{payload[:action] || '(unknown action)'}: #{Time.now - start_time} seconds"
166
165
  end
167
166
 
168
- if XML.debug.include?(:received)
169
- warn "received: #{Time.now}\n#{text}\n"
170
- end
167
+ warn "received: #{Time.now}\n#{text}\n" if XML.debug.include?(:received)
171
168
 
172
169
  data = Nokogiri::XML.parse(text)
173
170
  unless data.xpath('//response/@status').to_s == 'success'
174
- if XML.debug.include?(:sent_on_error)
175
- warn "sent:\n#{options.inspect}\n"
176
- end
177
- if XML.debug.include?(:received_on_error)
178
- warn "received:\n#{text.inspect}\n"
179
- end
171
+ warn "sent:\n#{options.inspect}\n" if XML.debug.include?(:sent_on_error)
172
+ warn "received:\n#{text.inspect}\n" if XML.debug.include?(:received_on_error)
180
173
  code = data.at_xpath('//response/@code')&.value.to_i # sometimes there is no code :( e.g. for 'op' errors
181
174
  message = data.xpath('/response/msg/line').map(&:text).map(&:strip).join("\n")
182
175
  raise_error(code, message)
183
176
  end
184
177
 
185
- return data
178
+ data
186
179
  end
187
-
188
180
  end
189
181
  end
190
182
 
@@ -195,30 +187,35 @@ module PaloAlto
195
187
  def execute(payload)
196
188
  retried = false
197
189
  begin
198
- Helpers::Rest.execute(payload, headers: {'X-PAN-KEY': self.auth_key})
190
+ Helpers::Rest.execute(payload, headers: { 'X-PAN-KEY': auth_key })
199
191
  rescue TemporaryException => e
200
- unless retried
201
- if XML.debug.include?(:warnings)
202
- warn "Got error #{e.inspect}; retrying"
203
- end
192
+ dont_continue_at = [
193
+ 'Partial revert is not allowed. Full system commit must be completed.',
194
+ 'Config for scope ',
195
+ 'Config is not currently locked for scope ',
196
+ 'Commit lock is not currently held by'
197
+ ]
198
+ if retried || dont_continue_at.any? { |x| e.message.start_with?(x) }
199
+ raise e
200
+ else
201
+ warn "Got error #{e.inspect}; retrying" if XML.debug.include?(:warnings)
204
202
  retried = true
205
- if e.is_a?(SessionTimedOutException)
206
- get_auth_key
207
- end
203
+ get_auth_key if e.is_a?(SessionTimedOutException)
208
204
  retry
209
- else
210
- raise e
211
205
  end
212
206
  end
213
207
  end
214
208
  end
215
209
 
216
- def commit!(all: false)
210
+ def commit!(all: false, device_groups: nil, wait_for_completion: true)
211
+ return nil if device_groups.is_a?(Array) && device_groups.empty?
212
+
217
213
  op = if all
218
214
  'commit'
219
215
  else
220
216
  { commit: { partial: [
221
217
  { 'admin': [XML.username] },
218
+ device_groups ? { 'device-group': device_groups } : nil,
222
219
  'no-template',
223
220
  'no-template-stack',
224
221
  'no-log-collector',
@@ -227,9 +224,137 @@ module PaloAlto
227
224
  'no-wildfire-appliance-cluster',
228
225
  { 'device-and-network': 'excluded' },
229
226
  { 'shared-object': 'excluded' }
230
- ] } }
227
+ ].compact } }
231
228
  end
232
- Op.new.execute(op)
229
+ Op.new.execute(op).tap do |result|
230
+ if wait_for_completion
231
+ job_id = result.at_xpath('response/result/job')&.text
232
+ wait_for_job_completion(job_id) if job_id
233
+ end
234
+ end
235
+ end
236
+
237
+ def full_commit_required?
238
+ result = Op.new.execute({ check: 'full-commit-required' })
239
+ return true unless result.at_xpath('response/result').text == 'no'
240
+
241
+ false
242
+ end
243
+
244
+ def primary_active?
245
+ cmd = { show: { 'high-availability': 'state' } }
246
+ state = Op.new.execute(cmd)
247
+ state.at_xpath('response/result/local-info/state').text == 'primary-active'
248
+ end
249
+
250
+ # area: config, commit
251
+ def show_locks(area:)
252
+ cmd = { show: "#{area}-locks" }
253
+ ret = Op.new.execute(cmd)
254
+ ret.xpath("response/result/#{area}-locks/entry").map do |lock|
255
+ comment = lock.at_xpath('comment').inner_text
256
+ location = lock.at_xpath('name').inner_text
257
+ {
258
+ name: lock.attribute('name').value,
259
+ location: location == 'shared' ? nil : location,
260
+ type: lock.at_xpath('type').inner_text,
261
+ comment: comment == '(null)' ? nil : comment
262
+ }
263
+ end
264
+ end
265
+
266
+ def execute_with_type(cmd, type:, location:)
267
+ if type == 'tpl'
268
+ run_with_template_scope(location) { Op.new.execute(cmd) }
269
+ elsif type == 'dg'
270
+ Op.new.execute(cmd, { vsys: location })
271
+ elsif !type || type == 'shared'
272
+ Op.new.execute(cmd)
273
+ else
274
+ raise(ArgumentError, "invalid type: #{type.inspect}")
275
+ end
276
+ end
277
+
278
+ # will execute block if given and unlock afterwards. returns false if lock could not be aquired
279
+ def lock(area:, comment: nil, type: nil, location: nil)
280
+ if block_given?
281
+ if lock(area: area, comment: comment, type: type, location: location)
282
+ begin
283
+ return yield
284
+ ensure
285
+ unlock(area: area, type: type, location: location)
286
+ end
287
+ else
288
+ return false
289
+ end
290
+ end
291
+
292
+ begin
293
+ cmd = { request: { "#{area}-lock": { add: { comment: comment || '(null)' } } } }
294
+ execute_with_type(cmd, type: type, location: location)
295
+ true
296
+ rescue PaloAlto::InternalErrorException
297
+ false
298
+ end
299
+ end
300
+
301
+ def unlock(area:, type: nil, location: nil, name: nil)
302
+ begin
303
+ cmd = if name
304
+ { request: { "#{area}-lock": { remove: { admin: name } } } }
305
+ else
306
+ { request: { "#{area}-lock": 'remove' } }
307
+ end
308
+ execute_with_type(cmd, type: type, location: location)
309
+ rescue PaloAlto::InternalErrorException
310
+ return false
311
+ end
312
+ true
313
+ end
314
+
315
+ def remove_all_locks
316
+ %w[config commit].each do |area|
317
+ show_locks(area: area).each do |lock|
318
+ unlock(area: area, type: lock[:type], location: lock[:location], name: area=='commit' ? lock[:name] : nil )
319
+ end
320
+ end
321
+ end
322
+
323
+ def run_with_template_scope(name)
324
+ if block_given?
325
+ run_with_template_scope(name)
326
+ begin
327
+ return yield
328
+ ensure
329
+ run_with_template_scope(nil)
330
+ end
331
+ end
332
+
333
+ cmd = if name
334
+ { set: { system: { setting: { target: { template: { name: name } } } } } }
335
+ else
336
+ { set: { system: { setting: { target: 'none' } } } }
337
+ end
338
+
339
+ Op.new.execute(cmd)
340
+ end
341
+
342
+ def check_for_changes(usernames: [XML.username])
343
+ result = Op.new.execute({ show: { config: { list: { 'change-summary': { partial: { admin: usernames } } } } } })
344
+ result.xpath('response/result/summary/device-group/member').map(&:inner_text)
345
+ end
346
+
347
+ def wait_for_job_completion(job_id, wait: 5, timeout: 300)
348
+ cmd = { show: { jobs: { id: job_id } } }
349
+ start = Time.now
350
+ loop do
351
+ result = Op.new.execute(cmd)
352
+ return result unless result.at_xpath('response/result/job/status')&.text == 'ACT'
353
+
354
+ sleep wait
355
+ break unless start + timeout > Time.now
356
+ end
357
+ false
233
358
  end
234
359
 
235
360
  def revert!(all: false)
@@ -266,7 +391,7 @@ module PaloAlto
266
391
  @arguments = [Expression.new(:this_node), []]
267
392
 
268
393
  # attempt to obtain the auth_key
269
- #raise 'Exception attempting to obtain the auth_key' if (self.class.auth_key = get_auth_key).nil?
394
+ # raise 'Exception attempting to obtain the auth_key' if (self.class.auth_key = get_auth_key).nil?
270
395
  self.class.get_auth_key
271
396
 
272
397
  self
@@ -274,11 +399,10 @@ module PaloAlto
274
399
 
275
400
  # Perform a query to the API endpoint for an auth_key based on the credentials provided
276
401
  def self.get_auth_key
277
-
278
402
  # establish the required options for the key request
279
403
  payload = { type: 'keygen',
280
- user: self.username,
281
- password: self.password }
404
+ user: username,
405
+ password: password }
282
406
 
283
407
  # get and parse the response for the key
284
408
  xml_data = Helpers::Rest.execute(payload)
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.1.4
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Roesner
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-20 00:00:00.000000000 Z
11
+ date: 2021-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri