palo_alto 0.1.7 → 0.2.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.
data/lib/palo_alto/op.rb CHANGED
@@ -1,58 +1,90 @@
1
- require "nokogiri"
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
2
4
 
3
5
  module PaloAlto
4
6
  class XML
5
-
6
7
  def op
7
8
  Op.new
8
9
  end
9
10
 
10
11
  class Op
11
- def execute(obj, additional_payload = {})
12
+ def execute(cmd, type: nil, location: nil, additional_payload: {})
13
+ payload = build_payload(cmd).merge(additional_payload)
14
+
15
+ if type == 'tpl'
16
+ run_with_template_scope(location) { XML.execute(payload) }
17
+ elsif type == 'dg'
18
+ XML.execute(payload.merge({ vsys: location }))
19
+ elsif !type || type == 'shared'
20
+ XML.execute(payload)
21
+ else
22
+ raise(ArgumentError, "invalid type: #{type.inspect}")
23
+ end
24
+ end
25
+
26
+ def run_with_template_scope(name)
27
+ if block_given?
28
+ run_with_template_scope(name)
29
+ begin
30
+ return yield
31
+ ensure
32
+ run_with_template_scope(nil)
33
+ end
34
+ end
35
+
36
+ cmd = if name
37
+ { set: { system: { setting: { target: { template: { name: name } } } } } }
38
+ else
39
+ { set: { system: { setting: { target: 'none' } } } }
40
+ end
41
+
42
+ execute(cmd)
43
+ end
12
44
 
45
+ def build_payload(obj)
13
46
  cmd = to_xml(obj)
14
47
 
15
- if obj=='commit' || obj.keys.first.to_sym == :commit
16
- type='commit'
17
- action='panorama'
18
- elsif obj=='commit-all' || obj.keys.first.to_sym == :'commit-all'
19
- type='commit'
20
- action='all'
48
+ if obj == 'commit' || obj.keys.first.to_sym == :commit
49
+ type = 'commit'
50
+ action = 'panorama'
51
+ elsif obj == 'commit-all' || obj.keys.first.to_sym == :'commit-all'
52
+ type = 'commit'
53
+ action = 'all'
21
54
  else
22
- type='op'
23
- action='panorama'
55
+ type = 'op'
56
+ action = 'panorama'
24
57
  end
25
58
 
26
- payload = {
27
- type: type,
59
+ {
60
+ type: type,
28
61
  action: action,
29
- cmd: cmd
30
- }.merge(additional_payload)
31
-
32
- XML.execute(payload)
62
+ cmd: cmd
63
+ }
33
64
  end
34
65
 
35
66
  def escape_xpath_tag(tag)
36
67
  if tag.to_s.include?('-') # https://stackoverflow.com/questions/48628259/nokogiri-how-to-name-a-node-comment
37
68
  tag
38
69
  else
39
- tag.to_s + "_"
70
+ "#{tag}_"
40
71
  end
41
72
  end
42
73
 
43
74
  def xml_builder(xml, ops, obj)
44
- if obj.is_a?(String)
75
+ case obj
76
+ when String
45
77
  section = obj
46
78
  data = nil
47
- elsif obj.is_a?(Hash)
79
+ when Hash
48
80
  section = obj.keys.first
49
81
  data = obj[section]
50
82
  else
51
83
  raise obj.pretty_inspect
52
84
  end
53
85
 
54
- unless ops.has_key?(section.to_s)
55
- err = "Error #{section.to_s} does not exist. Valid: " + ops.keys.pretty_inspect
86
+ unless ops.key?(section.to_s)
87
+ err = "Error #{section} does not exist. Valid: " + ops.keys.pretty_inspect
56
88
  raise err
57
89
  end
58
90
 
@@ -64,50 +96,49 @@ module PaloAlto
64
96
  when :element
65
97
  xml.public_send(section, data)
66
98
  when :array
67
- xml.public_send(section) {
68
- data.each{|el|
99
+ xml.public_send(section) do
100
+ data.each do |el|
69
101
  key = ops_tree.keys.first
70
102
  xml.public_send(escape_xpath_tag(key), el)
71
- }
72
- }
103
+ end
104
+ end
73
105
  when :sequence
74
- if data==nil
106
+ if data.nil?
75
107
  xml.send(section)
76
108
  elsif data.is_a?(Hash)
77
- xml.send(section){
109
+ xml.send(section) do
78
110
  xml_builder(xml, ops_tree, data)
79
- }
111
+ end
80
112
  else # array
81
113
 
82
114
  if data.is_a?(Array)
83
- attr = data.find { |child| child.is_a?(Hash) && ops_tree[child.keys.first.to_s][:obj]==:'attr-req' }
115
+ attr = data.find { |child| child.is_a?(Hash) && ops_tree[child.keys.first.to_s][:obj] == :'attr-req' }
84
116
  data.delete(attr)
85
117
  else
86
118
  attr = {}
87
119
  end
88
120
 
89
- xml.public_send(section, attr){
90
- data.each{|child|
121
+ xml.public_send(section, attr) do
122
+ data.each do |child|
91
123
  xml_builder(xml, ops_tree, child)
92
- }
93
- }
124
+ end
125
+ end
94
126
  end
95
127
  when :union
96
- k,v=obj.first
97
- xml.send("#{k}_"){
128
+ k, v = obj.first
129
+ xml.send("#{k}_") do
98
130
  xml_builder(xml, ops_tree, v)
99
- }
131
+ end
100
132
  else
101
133
  raise ops_tree[:obj].pretty_inspect
102
134
  end
103
135
  xml
104
136
  end
105
137
 
106
-
107
138
  def to_xml(obj)
108
- builder = Nokogiri::XML::Builder.new{|xml|
139
+ builder = Nokogiri::XML::Builder.new do |xml|
109
140
  xml_builder(xml, @@ops, obj)
110
- }
141
+ end
111
142
  builder.doc.root.to_xml
112
143
  end
113
144
  @@ops={"schedule"=>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PaloAlto
4
- VERSION = '0.1.7'
4
+ VERSION = '0.2.1'
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'
@@ -99,24 +99,33 @@ module PaloAlto
99
99
 
100
100
  thread[:http].start unless thread[:http].started?
101
101
 
102
- response = thread[:http].post('/api/', URI.encode_www_form(options[:payload]), options[:headers])
103
-
104
- if response.code == '200'
102
+ payload = options[:payload]
103
+ response = if payload.values.any? { |value| [IO, StringIO].any? { |t| value.is_a?(t) } }
104
+ payload.values.select { |value| [IO, StringIO].any? { |t| value.is_a?(t) } }.each(&:rewind)
105
+ post_req = Net::HTTP::Post.new('/api/', options[:headers])
106
+ post_req.set_form payload.map { |k, v| [k.to_s, v] }, 'multipart/form-data'
107
+ thread[:http].request(post_req)
108
+ else
109
+ thread[:http].post('/api/', URI.encode_www_form(payload), options[:headers])
110
+ end
111
+
112
+ case response.code
113
+ when '200'
105
114
  return response.body
106
- elsif response.code == '400' or response.code == '403'
115
+ when '400', '403'
107
116
  begin
108
117
  data = Nokogiri::XML.parse(response.body)
109
118
  message = data.xpath('//response/response/msg').text
110
119
  code = response.code.to_i
111
- rescue
120
+ rescue StandardError
112
121
  raise ConnectionErrorException, "#{response.code} #{response.message}"
113
122
  end
114
123
  raise_error(code, message)
115
124
  else
116
125
  raise ConnectionErrorException, "#{response.code} #{response.message}"
117
126
  end
118
- nil
119
127
 
128
+ nil
120
129
  rescue Net::OpenTimeout, Errno::ECONNREFUSED => e
121
130
  raise ConnectionErrorException, e.message
122
131
  end
@@ -141,7 +150,7 @@ module PaloAlto
141
150
  when 19..20 then SuccessException
142
151
  when 22 then SessionTimedOutException
143
152
  else InternalErrorException
144
- end
153
+ end
145
154
  raise error, message
146
155
  end
147
156
 
@@ -155,36 +164,27 @@ module PaloAlto
155
164
  options[:payload] = payload
156
165
  options[:headers] = headers
157
166
 
158
- if XML.debug.include?(:sent)
159
- warn "sent: (#{Time.now}\n#{options.pretty_inspect}\n"
160
- end
167
+ warn "sent: (#{Time.now}\n#{options.pretty_inspect}\n" if XML.debug.include?(:sent)
161
168
 
162
169
  start_time = Time.now
163
170
  text = Helpers::Rest.make_request(options)
164
171
  if XML.debug.include?(:statistics)
165
- warn "Elapsed for API call #{payload[:type]}/#{payload[:action]||'(unknown action)'}: #{Time.now-start_time} seconds"
172
+ warn "Elapsed for API call #{payload[:type]}/#{payload[:action] || '(unknown action)'}: #{Time.now - start_time} seconds"
166
173
  end
167
174
 
168
- if XML.debug.include?(:received)
169
- warn "received: #{Time.now}\n#{text}\n"
170
- end
175
+ warn "received: #{Time.now}\n#{text}\n" if XML.debug.include?(:received)
171
176
 
172
177
  data = Nokogiri::XML.parse(text)
173
178
  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
179
+ warn "sent:\n#{options.inspect}\n" if XML.debug.include?(:sent_on_error)
180
+ warn "received:\n#{text.inspect}\n" if XML.debug.include?(:received_on_error)
180
181
  code = data.at_xpath('//response/@code')&.value.to_i # sometimes there is no code :( e.g. for 'op' errors
181
182
  message = data.xpath('/response/msg/line').map(&:text).map(&:strip).join("\n")
182
183
  raise_error(code, message)
183
184
  end
184
185
 
185
- return data
186
+ data
186
187
  end
187
-
188
188
  end
189
189
  end
190
190
 
@@ -195,23 +195,22 @@ module PaloAlto
195
195
  def execute(payload)
196
196
  retried = false
197
197
  begin
198
- Helpers::Rest.execute(payload, headers: {'X-PAN-KEY': self.auth_key})
198
+ Helpers::Rest.execute(payload, headers: { 'X-PAN-KEY': auth_key })
199
199
  rescue TemporaryException => e
200
- dont_continue_at = [
200
+ dont_retry_at = [
201
201
  'Partial revert is not allowed. Full system commit must be completed.',
202
- 'Config for scope '
202
+ 'Config for scope ',
203
+ 'Config is not currently locked for scope ',
204
+ 'Commit lock is not currently held by',
205
+ 'You already own a config lock for scope '
203
206
  ]
204
- unless retried || dont_continue_at.any? { |x| e.message.start_with?(x) }
205
- if XML.debug.include?(:warnings)
206
- warn "Got error #{e.inspect}; retrying"
207
- end
207
+ if retried || dont_retry_at.any? { |x| e.message.start_with?(x) }
208
+ raise e
209
+ else
210
+ warn "Got error #{e.inspect}; retrying" if XML.debug.include?(:warnings)
208
211
  retried = true
209
- if e.is_a?(SessionTimedOutException)
210
- get_auth_key
211
- end
212
+ get_auth_key if e.is_a?(SessionTimedOutException)
212
213
  retry
213
- else
214
- raise e
215
214
  end
216
215
  end
217
216
  end
@@ -225,7 +224,7 @@ module PaloAlto
225
224
  else
226
225
  { commit: { partial: [
227
226
  { 'admin': [XML.username] },
228
- device_groups ? {'device-group': device_groups } : nil,
227
+ device_groups ? { 'device-group': device_groups } : nil,
229
228
  'no-template',
230
229
  'no-template-stack',
231
230
  'no-log-collector',
@@ -245,21 +244,21 @@ module PaloAlto
245
244
  end
246
245
 
247
246
  def full_commit_required?
248
- result = Op.new.execute({check: 'full-commit-required'})
247
+ result = Op.new.execute({ check: 'full-commit-required' })
249
248
  return true unless result.at_xpath('response/result').text == 'no'
250
249
 
251
250
  false
252
251
  end
253
252
 
254
253
  def primary_active?
255
- cmd = {show: {'high-availability': 'state'}}
254
+ cmd = { show: { 'high-availability': 'state' } }
256
255
  state = Op.new.execute(cmd)
257
- state.at_xpath("response/result/local-info/state").text == "primary-active"
256
+ state.at_xpath('response/result/local-info/state').text == 'primary-active'
258
257
  end
259
258
 
260
259
  # area: config, commit
261
260
  def show_locks(area:)
262
- cmd = {show: "#{area}-locks"}
261
+ cmd = { show: "#{area}-locks" }
263
262
  ret = Op.new.execute(cmd)
264
263
  ret.xpath("response/result/#{area}-locks/entry").map do |lock|
265
264
  comment = lock.at_xpath('comment').inner_text
@@ -276,30 +275,32 @@ module PaloAlto
276
275
  # will execute block if given and unlock afterwards. returns false if lock could not be aquired
277
276
  def lock(area:, comment: nil, type: nil, location: nil)
278
277
  if block_given?
279
- if lock(area: area, comment: comment, type: type, location: location)
280
- begin
281
- return yield
282
- ensure
283
- unlock(area: area, type: type, location: location)
284
- end
285
- else
286
- return false
278
+ return false unless lock(area: area, comment: comment, type: type, location: location)
279
+
280
+ begin
281
+ return yield
282
+ ensure
283
+ unlock(area: area, type: type, location: location)
287
284
  end
288
285
  end
289
286
 
290
287
  begin
291
- cmd = {request: {"#{area}-lock": {add: {comment: comment || '(null)' }}}}
292
- Op.new.execute(cmd, get_extra_argument(type: type, location: location))
288
+ cmd = { request: { "#{area}-lock": { add: { comment: comment || '(null)' } } } }
289
+ Op.new.execute(cmd, type: type, location: location)
293
290
  true
294
291
  rescue PaloAlto::InternalErrorException
295
292
  false
296
293
  end
297
294
  end
298
295
 
299
- def unlock(area:, type: nil, location: nil)
296
+ def unlock(area:, type: nil, location: nil, name: nil)
300
297
  begin
301
- cmd = {request: {"#{area}-lock": 'remove'}}
302
- Op.new.execute(cmd, get_extra_argument(type: type, location: location))
298
+ cmd = if name
299
+ { request: { "#{area}-lock": { remove: { admin: name } } } }
300
+ else
301
+ { request: { "#{area}-lock": 'remove' } }
302
+ end
303
+ Op.new.execute(cmd, type: type, location: location)
303
304
  rescue PaloAlto::InternalErrorException
304
305
  return false
305
306
  end
@@ -307,29 +308,38 @@ module PaloAlto
307
308
  end
308
309
 
309
310
  def remove_all_locks
310
- %w(config commit).each do |area|
311
- show_locks(area: area).each {|lock|
312
- unlock(area: area, type: lock[:type], location: lock[:location])
313
- }
311
+ %w[config commit].each do |area|
312
+ show_locks(area: area).each do |lock|
313
+ unlock(area: area, type: lock[:type], location: lock[:location], name: area == 'commit' ? lock[:name] : nil)
314
+ end
314
315
  end
315
316
  end
316
317
 
317
318
  def check_for_changes(usernames: [XML.username])
318
- result = Op.new.execute({show: {config: {list: {'change-summary': {partial: {admin: usernames}}}}}})
319
- result.xpath('response/result/summary/device-group/member').map(&:inner_text)
319
+ cmd = if usernames
320
+ { show: { config: { list: { 'change-summary': { partial: { admin: usernames } } } } } }
321
+ else
322
+ { show: { config: { list: 'change-summary' } } }
323
+ end
324
+ result = Op.new.execute(cmd)
325
+ {
326
+ device_groups: result.xpath('response/result/summary/device-group/member').map(&:inner_text),
327
+ templates: result.xpath('response/result/summary/template/member').map(&:inner_text)
328
+ }
320
329
  end
321
330
 
322
331
  def wait_for_job_completion(job_id, wait: 5, timeout: 300)
323
- cmd = {show: {jobs: {id: job_id}}}
332
+ cmd = { show: { jobs: { id: job_id } } }
324
333
  start = Time.now
325
- begin
334
+ loop do
326
335
  result = Op.new.execute(cmd)
327
- unless result.at_xpath('response/result/job/status')&.text=='ACT'
328
- return result
329
- end
336
+ status = result.at_xpath('response/result/job/status')&.text
337
+ return result unless %w[ACT PEND].include?(status)
338
+
330
339
  sleep wait
331
- end while start+timeout > Time.now
332
- return false
340
+ break unless start + timeout > Time.now
341
+ end
342
+ false
333
343
  end
334
344
 
335
345
  def revert!(all: false)
@@ -366,35 +376,20 @@ module PaloAlto
366
376
  @arguments = [Expression.new(:this_node), []]
367
377
 
368
378
  # attempt to obtain the auth_key
369
- #raise 'Exception attempting to obtain the auth_key' if (self.class.auth_key = get_auth_key).nil?
379
+ # raise 'Exception attempting to obtain the auth_key' if (self.class.auth_key = get_auth_key).nil?
370
380
  self.class.get_auth_key
371
-
372
- self
373
381
  end
374
382
 
375
383
  # Perform a query to the API endpoint for an auth_key based on the credentials provided
376
384
  def self.get_auth_key
377
-
378
385
  # establish the required options for the key request
379
386
  payload = { type: 'keygen',
380
- user: self.username,
381
- password: self.password }
387
+ user: username,
388
+ password: password }
382
389
 
383
390
  # get and parse the response for the key
384
391
  xml_data = Helpers::Rest.execute(payload)
385
392
  self.auth_key = xml_data.xpath('//response/result/key')[0].content
386
393
  end
387
-
388
- private
389
-
390
- # used to limit an op command to a specifc dg/template
391
- def get_extra_argument(type:, location:)
392
- case type
393
- when 'dg' then {vsys: location}
394
- when 'tpl' then raise
395
- else {}
396
- end
397
- end
398
-
399
394
  end
400
395
  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.1.7
4
+ version: 0.2.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: 2021-10-22 00:00:00.000000000 Z
11
+ date: 2021-11-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -31,6 +31,7 @@ executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
+ - ".gitignore"
34
35
  - CHANGELOG.md
35
36
  - Gemfile
36
37
  - Gemfile.lock