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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/lib/palo_alto/config.rb +701 -675
- data/lib/palo_alto/op.rb +71 -40
- data/lib/palo_alto/version.rb +1 -1
- data/lib/palo_alto.rb +127 -53
- 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,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':
|
198
|
+
Helpers::Rest.execute(payload, headers: { 'X-PAN-KEY': auth_key })
|
199
199
|
rescue TemporaryException => e
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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
|
-
|
252
|
-
|
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
|
-
|
334
|
+
loop do
|
259
335
|
result = Op.new.execute(cmd)
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
-
|
265
|
-
|
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:
|
314
|
-
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.
|
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-
|
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
|