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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/lib/palo_alto/config.rb +701 -679
- data/lib/palo_alto/op.rb +71 -40
- data/lib/palo_alto/version.rb +1 -1
- data/lib/palo_alto.rb +82 -87
- metadata +3 -2
data/lib/palo_alto/op.rb
CHANGED
@@ -1,58 +1,90 @@
|
|
1
|
-
|
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(
|
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
|
-
|
27
|
-
type:
|
59
|
+
{
|
60
|
+
type: type,
|
28
61
|
action: action,
|
29
|
-
cmd:
|
30
|
-
}
|
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
|
70
|
+
"#{tag}_"
|
40
71
|
end
|
41
72
|
end
|
42
73
|
|
43
74
|
def xml_builder(xml, ops, obj)
|
44
|
-
|
75
|
+
case obj
|
76
|
+
when String
|
45
77
|
section = obj
|
46
78
|
data = nil
|
47
|
-
|
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.
|
55
|
-
err = "Error #{section
|
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
|
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
|
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]
|
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
|
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
|
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"=>
|
data/lib/palo_alto/version.rb
CHANGED
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]
|
80
|
-
options[:timeout]
|
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
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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':
|
198
|
+
Helpers::Rest.execute(payload, headers: { 'X-PAN-KEY': auth_key })
|
199
199
|
rescue TemporaryException => e
|
200
|
-
|
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
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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(
|
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
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
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,
|
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 =
|
302
|
-
|
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
|
311
|
-
show_locks(area: area).each
|
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
|
-
|
319
|
-
|
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
|
-
|
334
|
+
loop do
|
326
335
|
result = Op.new.execute(cmd)
|
327
|
-
|
328
|
-
|
329
|
-
|
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
|
-
|
332
|
-
|
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:
|
381
|
-
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
|
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-
|
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
|