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.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaloAlto
4
+ VERSION = '0.1.2'
5
+ end
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: []