palo_alto 0.1.2
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 +7 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +91 -0
- data/LICENSE.txt +94 -0
- data/README.md +0 -0
- data/Rakefile +8 -0
- data/lib/palo_alto/config.rb +371275 -0
- data/lib/palo_alto/log.rb +86 -0
- data/lib/palo_alto/op.rb +39655 -0
- data/lib/palo_alto/version.rb +5 -0
- data/lib/palo_alto.rb +287 -0
- data/palo_alto.gemspec +30 -0
- metadata +72 -0
data/lib/palo_alto.rb
ADDED
@@ -0,0 +1,287 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
require_relative 'palo_alto/version'
|
7
|
+
|
8
|
+
require_relative 'palo_alto/config'
|
9
|
+
require_relative 'palo_alto/log'
|
10
|
+
require_relative 'palo_alto/op'
|
11
|
+
|
12
|
+
module PaloAlto
|
13
|
+
|
14
|
+
class PermanentException < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
class TemporaryException < StandardError
|
18
|
+
end
|
19
|
+
|
20
|
+
class ConnectionErrorException < TemporaryException
|
21
|
+
end
|
22
|
+
|
23
|
+
class BadRequestException < PermanentException
|
24
|
+
end
|
25
|
+
|
26
|
+
class ForbiddenException < PermanentException
|
27
|
+
end
|
28
|
+
|
29
|
+
class UnknownCommandException < PermanentException
|
30
|
+
end
|
31
|
+
|
32
|
+
class InternalErrorsException < TemporaryException
|
33
|
+
end
|
34
|
+
|
35
|
+
class BadXpathException < PermanentException
|
36
|
+
end
|
37
|
+
|
38
|
+
class ObjectNotPresentException < PermanentException
|
39
|
+
end
|
40
|
+
|
41
|
+
class ObjectNotUniqueException < PermanentException
|
42
|
+
end
|
43
|
+
|
44
|
+
class ReferenceCountNotZeroException < PermanentException
|
45
|
+
end
|
46
|
+
|
47
|
+
class InvalidObjectException < PermanentException
|
48
|
+
end
|
49
|
+
|
50
|
+
class OperationNotPossibleException < PermanentException
|
51
|
+
end
|
52
|
+
|
53
|
+
class OperationDeniedException < PermanentException
|
54
|
+
end
|
55
|
+
|
56
|
+
class UnauthorizedException < PermanentException
|
57
|
+
end
|
58
|
+
|
59
|
+
class InvalidCommandException < PermanentException
|
60
|
+
end
|
61
|
+
|
62
|
+
class MalformedCommandException < PermanentException
|
63
|
+
end
|
64
|
+
|
65
|
+
class SuccessException < PermanentException
|
66
|
+
end
|
67
|
+
|
68
|
+
class InternalErrorException < TemporaryException
|
69
|
+
end
|
70
|
+
|
71
|
+
class SessionTimedOutException < TemporaryException
|
72
|
+
end
|
73
|
+
|
74
|
+
module Helpers
|
75
|
+
class Rest
|
76
|
+
def self.make_request(opts)
|
77
|
+
options = {}
|
78
|
+
options[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
|
79
|
+
options[:timeout] = 60
|
80
|
+
|
81
|
+
headers = {}
|
82
|
+
headers['User-Agent'] = 'ruby-keystone-client'
|
83
|
+
headers['Accept'] = 'application/xml'
|
84
|
+
headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
85
|
+
|
86
|
+
# merge in settings from method caller
|
87
|
+
options = options.merge(opts)
|
88
|
+
options[:headers].merge!(headers)
|
89
|
+
|
90
|
+
thread = Thread.current
|
91
|
+
unless thread[:http]
|
92
|
+
thread[:http] = Net::HTTP.new(options[:host], options[:port])
|
93
|
+
thread[:http].use_ssl = true
|
94
|
+
thread[:http].verify_mode = options[:verify_ssl]
|
95
|
+
thread[:http].read_timeout = thread[:http].open_timeout = options[:timeout]
|
96
|
+
thread[:http].set_debug_output($stdout) if XML.debug.include?(:http)
|
97
|
+
end
|
98
|
+
|
99
|
+
thread[:http].start unless thread[:http].started?
|
100
|
+
|
101
|
+
response = thread[:http].post('/api/', URI.encode_www_form(options[:payload]), options[:headers])
|
102
|
+
|
103
|
+
if response.code == '200'
|
104
|
+
return response.body
|
105
|
+
elsif response.code == '400' or response.code == '403'
|
106
|
+
begin
|
107
|
+
data = Nokogiri::XML.parse(response.body)
|
108
|
+
message = data.xpath('//response/response/msg').text
|
109
|
+
code = response.code.to_i
|
110
|
+
rescue
|
111
|
+
raise ConnectionErrorException, "#{response.code} #{response.message}"
|
112
|
+
end
|
113
|
+
raise_error(code, message)
|
114
|
+
else
|
115
|
+
raise ConnectionErrorException, "#{response.code} #{response.message}"
|
116
|
+
end
|
117
|
+
nil
|
118
|
+
|
119
|
+
rescue Net::OpenTimeout, Errno::ECONNREFUSED => e
|
120
|
+
raise ConnectionErrorException, e.message
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.raise_error(code, message)
|
124
|
+
error = case code
|
125
|
+
when 400 then BadRequestException
|
126
|
+
when 403 then ForbiddenException
|
127
|
+
when 1 then UnknownCommandException
|
128
|
+
when 2..5 then InternalErrorsException
|
129
|
+
when 6 then BadXpathException
|
130
|
+
when 7 then ObjectNotPresentException
|
131
|
+
when 8 then ObjectNotUniqueException
|
132
|
+
when 10 then ReferenceCountNotZeroException
|
133
|
+
when 0, 11, 21 then InternalErrorException # also if there is no code..
|
134
|
+
when 12 then InvalidObjectException
|
135
|
+
when 14 then OperationNotPossibleException
|
136
|
+
when 15 then OperationDeniedException
|
137
|
+
when 16 then UnauthorizedException
|
138
|
+
when 17 then InvalidCommandException
|
139
|
+
when 18 then MalformedCommandException
|
140
|
+
when 19..20 then SuccessException
|
141
|
+
when 22 then SessionTimedOutException
|
142
|
+
else InternalErrorException
|
143
|
+
end
|
144
|
+
raise error, message
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.execute(payload, headers: {})
|
148
|
+
retried = false
|
149
|
+
# configure options for the request
|
150
|
+
options = {}
|
151
|
+
options[:host] = XML.host
|
152
|
+
options[:port] = XML.port
|
153
|
+
options[:verify_ssl] = XML.verify_ssl
|
154
|
+
options[:payload] = payload
|
155
|
+
options[:headers] = headers
|
156
|
+
|
157
|
+
if XML.debug.include?(:sent)
|
158
|
+
warn "sent: (#{Time.now}\n#{options.pretty_inspect}\n"
|
159
|
+
end
|
160
|
+
|
161
|
+
start_time = Time.now
|
162
|
+
text = Helpers::Rest.make_request(options)
|
163
|
+
if XML.debug.include?(:statistics)
|
164
|
+
warn "Elapsed for API call #{payload[:type]}/#{payload[:action]||'(unknown action)'}: #{Time.now-start_time} seconds"
|
165
|
+
end
|
166
|
+
|
167
|
+
if XML.debug.include?(:received)
|
168
|
+
warn "received: #{Time.now}\n#{text}\n"
|
169
|
+
end
|
170
|
+
|
171
|
+
data = Nokogiri::XML.parse(text)
|
172
|
+
unless data.xpath('//response/@status').to_s == 'success'
|
173
|
+
if XML.debug.include?(:sent_on_error)
|
174
|
+
warn "sent:\n#{options.inspect}\n"
|
175
|
+
end
|
176
|
+
if XML.debug.include?(:received_on_error)
|
177
|
+
warn "received:\n#{text.inspect}\n"
|
178
|
+
end
|
179
|
+
code = data.at_xpath('//response/@code')&.value.to_i # sometimes there is no code :( e.g. for 'op' errors
|
180
|
+
message = data.xpath('/response/msg/line').map(&:text).map(&:strip).join("\n")
|
181
|
+
raise_error(code, message)
|
182
|
+
end
|
183
|
+
|
184
|
+
return data
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class XML
|
191
|
+
class << self
|
192
|
+
attr_accessor :host, :port, :username, :password, :auth_key, :verify_ssl, :debug
|
193
|
+
|
194
|
+
def execute(payload)
|
195
|
+
retried = false
|
196
|
+
begin
|
197
|
+
Helpers::Rest.execute(payload, headers: {'X-PAN-KEY': self.auth_key})
|
198
|
+
rescue TemporaryException => e
|
199
|
+
unless retried
|
200
|
+
if XML.debug.include?(:warnings)
|
201
|
+
warn "Got error #{e.inspect}; retrying"
|
202
|
+
end
|
203
|
+
retried = true
|
204
|
+
if e.is_a?(SessionTimedOutException)
|
205
|
+
get_auth_key
|
206
|
+
end
|
207
|
+
retry
|
208
|
+
else
|
209
|
+
raise e
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def commit!(all: false)
|
216
|
+
op = if all
|
217
|
+
'commit'
|
218
|
+
else
|
219
|
+
{ commit: { partial: [
|
220
|
+
{ 'admin': [XML.username] },
|
221
|
+
'no-template',
|
222
|
+
'no-template-stack',
|
223
|
+
'no-log-collector',
|
224
|
+
'no-log-collector-group',
|
225
|
+
'no-wildfire-appliance',
|
226
|
+
'no-wildfire-appliance-cluster',
|
227
|
+
{ 'device-and-network': 'excluded' },
|
228
|
+
{ 'shared-object': 'excluded' }
|
229
|
+
] } }
|
230
|
+
end
|
231
|
+
Op.new.execute(op)
|
232
|
+
end
|
233
|
+
|
234
|
+
def revert!(all: false)
|
235
|
+
op = if all
|
236
|
+
{ revert: 'config' }
|
237
|
+
else
|
238
|
+
{ revert: { config: { partial: [
|
239
|
+
{ 'admin': [XML.username] },
|
240
|
+
'no-template',
|
241
|
+
'no-template-stack',
|
242
|
+
'no-log-collector',
|
243
|
+
'no-log-collector-group',
|
244
|
+
'no-wildfire-appliance',
|
245
|
+
'no-wildfire-appliance-cluster',
|
246
|
+
{ 'device-and-network': 'excluded' },
|
247
|
+
{ 'shared-object': 'excluded' }
|
248
|
+
] } } }
|
249
|
+
end
|
250
|
+
Op.new.execute(op)
|
251
|
+
end
|
252
|
+
|
253
|
+
def initialize(host:, port:, username:, password:, debug: [])
|
254
|
+
self.class.host = host
|
255
|
+
self.class.port = port
|
256
|
+
self.class.username = username
|
257
|
+
self.class.password = password
|
258
|
+
self.class.verify_ssl = OpenSSL::SSL::VERIFY_NONE
|
259
|
+
self.class.debug = debug
|
260
|
+
|
261
|
+
@subclasses = {}
|
262
|
+
|
263
|
+
# xpath
|
264
|
+
@expression = :root
|
265
|
+
@arguments = [Expression.new(:this_node), []]
|
266
|
+
|
267
|
+
# attempt to obtain the auth_key
|
268
|
+
#raise 'Exception attempting to obtain the auth_key' if (self.class.auth_key = get_auth_key).nil?
|
269
|
+
self.class.get_auth_key
|
270
|
+
|
271
|
+
self
|
272
|
+
end
|
273
|
+
|
274
|
+
# Perform a query to the API endpoint for an auth_key based on the credentials provided
|
275
|
+
def self.get_auth_key
|
276
|
+
|
277
|
+
# establish the required options for the key request
|
278
|
+
payload = { type: 'keygen',
|
279
|
+
user: self.username,
|
280
|
+
password: self.password }
|
281
|
+
|
282
|
+
# get and parse the response for the key
|
283
|
+
xml_data = Helpers::Rest.execute(payload)
|
284
|
+
self.auth_key = xml_data.xpath('//response/result/key')[0].content
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
data/palo_alto.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/palo_alto/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'palo_alto'
|
7
|
+
spec.version = PaloAlto::VERSION
|
8
|
+
spec.authors = ['Sebastian Roesner']
|
9
|
+
spec.email = ['sroesner-paloalto@roesner-online.de']
|
10
|
+
|
11
|
+
spec.summary = 'Palo Alto API for Ruby'
|
12
|
+
spec.homepage = 'https://github.com/Sebbb/'
|
13
|
+
spec.license = 'artistic-2.0'
|
14
|
+
spec.required_ruby_version = '>= 2.7.0'
|
15
|
+
|
16
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = 'https://github.com/Sebbb/palo_alto'
|
18
|
+
spec.metadata["changelog_uri"] = 'https://github.com/Sebbb/palo_alto/blob/main/README.md'
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features|_generators|bin|pkg|.env)/}) }
|
24
|
+
end
|
25
|
+
spec.bindir = 'exe'
|
26
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ['lib']
|
28
|
+
|
29
|
+
spec.add_dependency 'nokogiri', '~> 1.10'
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: palo_alto
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sebastian Roesner
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-09-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- sroesner-paloalto@roesner-online.de
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- CHANGELOG.md
|
35
|
+
- Gemfile
|
36
|
+
- Gemfile.lock
|
37
|
+
- LICENSE.txt
|
38
|
+
- README.md
|
39
|
+
- Rakefile
|
40
|
+
- lib/palo_alto.rb
|
41
|
+
- lib/palo_alto/config.rb
|
42
|
+
- lib/palo_alto/log.rb
|
43
|
+
- lib/palo_alto/op.rb
|
44
|
+
- lib/palo_alto/version.rb
|
45
|
+
- palo_alto.gemspec
|
46
|
+
homepage: https://github.com/Sebbb/
|
47
|
+
licenses:
|
48
|
+
- artistic-2.0
|
49
|
+
metadata:
|
50
|
+
homepage_uri: https://github.com/Sebbb/
|
51
|
+
source_code_uri: https://github.com/Sebbb/palo_alto
|
52
|
+
changelog_uri: https://github.com/Sebbb/palo_alto/blob/main/README.md
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.7.0
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubygems_version: 3.1.6
|
69
|
+
signing_key:
|
70
|
+
specification_version: 4
|
71
|
+
summary: Palo Alto API for Ruby
|
72
|
+
test_files: []
|