palo_alto 0.1.6 → 0.2.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.
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.6'
4
+ VERSION = '0.2.0'
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,19 +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
- unless retried
201
- if XML.debug.include?(:warnings)
202
- warn "Got error #{e.inspect}; retrying"
203
- end
200
+ dont_retry_at = [
201
+ 'Partial revert is not allowed. Full system commit must be completed.',
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 '
206
+ ]
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)
204
211
  retried = true
205
- if e.is_a?(SessionTimedOutException)
206
- get_auth_key
207
- end
212
+ get_auth_key if e.is_a?(SessionTimedOutException)
208
213
  retry
209
- else
210
- raise e
211
214
  end
212
215
  end
213
216
  end
@@ -221,7 +224,7 @@ module PaloAlto
221
224
  else
222
225
  { commit: { partial: [
223
226
  { 'admin': [XML.username] },
224
- device_groups ? {'device-group': device_groups } : nil,
227
+ device_groups ? { 'device-group': device_groups } : nil,
225
228
  'no-template',
226
229
  'no-template-stack',
227
230
  'no-log-collector',
@@ -241,28 +244,102 @@ module PaloAlto
241
244
  end
242
245
 
243
246
  def full_commit_required?
244
- result = Op.new.execute({check: 'full-commit-required'})
247
+ result = Op.new.execute({ check: 'full-commit-required' })
245
248
  return true unless result.at_xpath('response/result').text == 'no'
246
249
 
247
250
  false
248
251
  end
249
252
 
253
+ def primary_active?
254
+ cmd = { show: { 'high-availability': 'state' } }
255
+ state = Op.new.execute(cmd)
256
+ state.at_xpath('response/result/local-info/state').text == 'primary-active'
257
+ end
258
+
259
+ # area: config, commit
260
+ def show_locks(area:)
261
+ cmd = { show: "#{area}-locks" }
262
+ ret = Op.new.execute(cmd)
263
+ ret.xpath("response/result/#{area}-locks/entry").map do |lock|
264
+ comment = lock.at_xpath('comment').inner_text
265
+ location = lock.at_xpath('name').inner_text
266
+ {
267
+ name: lock.attribute('name').value,
268
+ location: location == 'shared' ? nil : location,
269
+ type: lock.at_xpath('type').inner_text,
270
+ comment: comment == '(null)' ? nil : comment
271
+ }
272
+ end
273
+ end
274
+
275
+ # will execute block if given and unlock afterwards. returns false if lock could not be aquired
276
+ def lock(area:, comment: nil, type: nil, location: nil)
277
+ if block_given?
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)
284
+ end
285
+ end
286
+
287
+ begin
288
+ cmd = { request: { "#{area}-lock": { add: { comment: comment || '(null)' } } } }
289
+ Op.new.execute(cmd, type: type, location: location)
290
+ true
291
+ rescue PaloAlto::InternalErrorException
292
+ false
293
+ end
294
+ end
295
+
296
+ def unlock(area:, type: nil, location: nil, name: nil)
297
+ begin
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)
304
+ rescue PaloAlto::InternalErrorException
305
+ return false
306
+ end
307
+ true
308
+ end
309
+
310
+ def remove_all_locks
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
315
+ end
316
+ end
317
+
250
318
  def check_for_changes(usernames: [XML.username])
251
- result = Op.new.execute({show: {config: {list: {'change-summary': {partial: {admin: usernames}}}}}})
252
- 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
+ }
253
329
  end
254
330
 
255
331
  def wait_for_job_completion(job_id, wait: 5, timeout: 300)
256
- cmd = {show: {jobs: {id: job_id}}}
332
+ cmd = { show: { jobs: { id: job_id } } }
257
333
  start = Time.now
258
- begin
334
+ loop do
259
335
  result = Op.new.execute(cmd)
260
- unless result.at_xpath('response/result/job/status')&.text=='ACT'
261
- return result
262
- end
336
+ status = result.at_xpath('response/result/job/status')&.text
337
+ return result unless %w[ACT PEND].include?(status)
338
+
263
339
  sleep wait
264
- end while start+timeout > Time.now
265
- return false
340
+ break unless start + timeout > Time.now
341
+ end
342
+ false
266
343
  end
267
344
 
268
345
  def revert!(all: false)
@@ -299,19 +376,16 @@ module PaloAlto
299
376
  @arguments = [Expression.new(:this_node), []]
300
377
 
301
378
  # attempt to obtain the auth_key
302
- #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?
303
380
  self.class.get_auth_key
304
-
305
- self
306
381
  end
307
382
 
308
383
  # Perform a query to the API endpoint for an auth_key based on the credentials provided
309
384
  def self.get_auth_key
310
-
311
385
  # establish the required options for the key request
312
386
  payload = { type: 'keygen',
313
- user: self.username,
314
- password: self.password }
387
+ user: username,
388
+ password: password }
315
389
 
316
390
  # get and parse the response for the key
317
391
  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.6
4
+ version: 0.2.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: 2021-10-19 00:00:00.000000000 Z
11
+ date: 2021-11-23 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