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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/lib/palo_alto/config.rb +703 -671
- data/lib/palo_alto/op.rb +71 -40
- data/lib/palo_alto/version.rb +1 -1
- data/lib/palo_alto.rb +135 -41
- 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'
|
@@ -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
|
-
|
104
|
+
case response.code
|
105
|
+
when '200'
|
105
106
|
return response.body
|
106
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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':
|
190
|
+
Helpers::Rest.execute(payload, headers: { 'X-PAN-KEY': auth_key })
|
199
191
|
rescue TemporaryException => e
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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:
|
281
|
-
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.
|
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-
|
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
|