palo_alto 0.1.5 → 0.1.9

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.5'
4
+ VERSION = '0.1.9'
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,107 @@ 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
+ # will execute block if given and unlock afterwards. returns false if lock could not be aquired
267
+ def lock(area:, comment: nil, type: nil, location: nil)
268
+ if block_given?
269
+ if lock(area: area, comment: comment, type: type, location: location)
270
+ begin
271
+ return yield
272
+ ensure
273
+ unlock(area: area, type: type, location: location)
274
+ end
275
+ else
276
+ return false
277
+ end
278
+ end
279
+
280
+ begin
281
+ cmd = { request: { "#{area}-lock": { add: { comment: comment || '(null)' } } } }
282
+ Op.new.execute(cmd, type: type, location: location)
283
+ true
284
+ rescue PaloAlto::InternalErrorException
285
+ false
286
+ end
287
+ end
288
+
289
+ def unlock(area:, type: nil, location: nil, name: nil)
290
+ begin
291
+ cmd = if name
292
+ { request: { "#{area}-lock": { remove: { admin: name } } } }
293
+ else
294
+ { request: { "#{area}-lock": 'remove' } }
295
+ end
296
+ Op.new.execute(cmd, type: type, location: location)
297
+ rescue PaloAlto::InternalErrorException
298
+ return false
299
+ end
300
+ true
301
+ end
302
+
303
+ def remove_all_locks
304
+ %w[config commit].each do |area|
305
+ show_locks(area: area).each do |lock|
306
+ unlock(area: area, type: lock[:type], location: lock[:location], name: area == 'commit' ? lock[:name] : nil)
307
+ end
308
+ end
309
+ end
310
+
311
+ def check_for_changes(usernames: [XML.username])
312
+ result = Op.new.execute({ show: { config: { list: { 'change-summary': { partial: { admin: usernames } } } } } })
313
+ result.xpath('response/result/summary/device-group/member').map(&:inner_text)
314
+ end
315
+
316
+ def wait_for_job_completion(job_id, wait: 5, timeout: 300)
317
+ cmd = { show: { jobs: { id: job_id } } }
318
+ start = Time.now
319
+ loop do
320
+ result = Op.new.execute(cmd)
321
+ status = result.at_xpath('response/result/job/status')&.text
322
+ return result unless %w[ACT PEND].include?(status)
323
+
324
+ sleep wait
325
+ break unless start + timeout > Time.now
326
+ end
327
+ false
233
328
  end
234
329
 
235
330
  def revert!(all: false)
@@ -266,7 +361,7 @@ module PaloAlto
266
361
  @arguments = [Expression.new(:this_node), []]
267
362
 
268
363
  # attempt to obtain the auth_key
269
- #raise 'Exception attempting to obtain the auth_key' if (self.class.auth_key = get_auth_key).nil?
364
+ # raise 'Exception attempting to obtain the auth_key' if (self.class.auth_key = get_auth_key).nil?
270
365
  self.class.get_auth_key
271
366
 
272
367
  self
@@ -274,11 +369,10 @@ module PaloAlto
274
369
 
275
370
  # Perform a query to the API endpoint for an auth_key based on the credentials provided
276
371
  def self.get_auth_key
277
-
278
372
  # establish the required options for the key request
279
373
  payload = { type: 'keygen',
280
- user: self.username,
281
- password: self.password }
374
+ user: username,
375
+ password: password }
282
376
 
283
377
  # get and parse the response for the key
284
378
  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.5
4
+ version: 0.1.9
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-23 00:00:00.000000000 Z
11
+ date: 2021-11-21 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