From 645f02c4141ca2c63ca9ed519d72167d0829baa4 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Mon, 10 Dec 2018 18:03:14 +0000 Subject: [PATCH] (FM-7602) Implement the panos Transport Experimental implementation of a `panos` transport based on the new in development transport support. This still needs some cleanups before it can be merged. Follow the TODO breadcrumbs. --- .sync.yml | 2 +- Gemfile | 2 +- .../panos_arbitrary_commands.rb | 8 +- .../provider/panos_commit/panos_commit.rb | 6 +- .../provider/panos_path_monitor_base.rb | 8 +- lib/puppet/provider/panos_provider.rb | 8 +- .../provider/panos_static_route_base.rb | 8 +- lib/puppet/transport/panos.rb | 287 ++++++++++++ lib/puppet/transport/schema/panos.rb | 36 ++ .../util/network_device/panos/device.rb | 289 +------------ .../panos_arbitrary_commands_spec.rb | 18 +- .../panos_commit/panos_commit_spec.rb | 10 +- .../provider/panos_path_monitor_base_spec.rb | 18 +- .../puppet/provider/panos_provider_spec.rb | 16 +- .../provider/panos_static_route_base_spec.rb | 18 +- spec/unit/puppet/transport/panos_spec.rb | 397 +++++++++++++++++ .../util/network_device/panos/device_spec.rb | 408 +----------------- tasks/apikey.rb | 6 +- tasks/commit.rb | 8 +- tasks/set_config.rb | 8 +- tasks/store_config.rb | 6 +- 21 files changed, 807 insertions(+), 760 deletions(-) create mode 100644 lib/puppet/transport/panos.rb create mode 100644 lib/puppet/transport/schema/panos.rb create mode 100644 spec/unit/puppet/transport/panos_spec.rb diff --git a/.sync.yml b/.sync.yml index 49c7e812..6745bd18 100644 --- a/.sync.yml +++ b/.sync.yml @@ -10,7 +10,7 @@ Gemfile: ref: 'master' - gem: 'puppet-resource_api' git: 'https://github.com/puppetlabs/puppet-resource_api.git' - ref: 'master' + ref: 'transport' # required for internal pipelines - gem: 'beaker-hostgenerator' # the first version to contain Palo Alto support diff --git a/Gemfile b/Gemfile index 07618086..cf7dbcaf 100644 --- a/Gemfile +++ b/Gemfile @@ -29,7 +29,7 @@ group :development do gem "webmock", require: false gem "builder", '~> 3.2.2', require: false gem "puppet-strings", require: false, git: 'https://github.com/puppetlabs/puppet-strings.git', ref: 'master' - gem "puppet-resource_api", require: false, git: 'https://github.com/puppetlabs/puppet-resource_api.git', ref: 'master' + gem "puppet-resource_api", require: false, git: 'https://github.com/DavidS/puppet-resource_api.git', ref: 'FM-7726-context-transport' gem "beaker-hostgenerator", '~> 1.1.15', require: false gem "github_changelog_generator", require: false, git: 'https://github.com/skywinder/github-changelog-generator', ref: '20ee04ba1234e9e83eb2ffb5056e23d641c7a018' if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.2.2') end diff --git a/lib/puppet/provider/panos_arbitrary_commands/panos_arbitrary_commands.rb b/lib/puppet/provider/panos_arbitrary_commands/panos_arbitrary_commands.rb index 7b468062..1b7825e4 100644 --- a/lib/puppet/provider/panos_arbitrary_commands/panos_arbitrary_commands.rb +++ b/lib/puppet/provider/panos_arbitrary_commands/panos_arbitrary_commands.rb @@ -26,7 +26,7 @@ def canonicalize(_context, resources) def get(context, xpaths = nil) return [] if xpaths.nil? results = [] - config = context.device.get_config('/config/' + xpaths.first) unless xpaths.first.nil? + config = context.transport.get_config('/config/' + xpaths.first) unless xpaths.first.nil? if xpaths.first config.elements.collect('/response/result') do |entry| # rubocop:disable Style/CollectionMethods xml = str_from_xml(entry.to_s) @@ -48,7 +48,7 @@ def create(context, xpath, should) raise Puppet::ResourceError, parse_exception.message end - context.device.set_config('/config/' + xpath, should) + context.transport.set_config('/config/' + xpath, should) end def update(context, xpath, should) @@ -58,11 +58,11 @@ def update(context, xpath, should) raise Puppet::ResourceError, parse_exception.message end - context.device.edit_config('/config/' + xpath, should) + context.transport.edit_config('/config/' + xpath, should) end def delete(context, xpath) - context.device.delete_config('/config/' + xpath) + context.transport.delete_config('/config/' + xpath) end def str_from_xml(xml) diff --git a/lib/puppet/provider/panos_commit/panos_commit.rb b/lib/puppet/provider/panos_commit/panos_commit.rb index 8e650ff3..6cf3050a 100644 --- a/lib/puppet/provider/panos_commit/panos_commit.rb +++ b/lib/puppet/provider/panos_commit/panos_commit.rb @@ -5,16 +5,16 @@ def get(context) { name: 'commit', # return a value that causes an update if the user requested one - commit: !context.device.outstanding_changes?, + commit: !context.transport.outstanding_changes?, }, ] end def set(context, changes) - if context.device.outstanding_changes? + if context.transport.outstanding_changes? if changes['commit'][:should][:commit] context.updating('commit') do - context.device.commit + context.transport.commit end else context.info('changes detected, but skipping commit as requested') diff --git a/lib/puppet/provider/panos_path_monitor_base.rb b/lib/puppet/provider/panos_path_monitor_base.rb index 8b981afd..20ccbe26 100644 --- a/lib/puppet/provider/panos_path_monitor_base.rb +++ b/lib/puppet/provider/panos_path_monitor_base.rb @@ -26,7 +26,7 @@ def xml_from_should(name, should) def get(context) results = [] - config = context.device.get_config(context.type.definition[:base_xpath] + '/entry') + config = context.transport.get_config(context.type.definition[:base_xpath] + '/entry') config.elements.collect('/response/result/entry') do |entry| # rubocop:disable Style/CollectionMethods vr_name = REXML::XPath.match(entry, 'string(@name)').first config.elements.collect("/response/result/entry[@name='#{vr_name}']/routing-table/#{@version_label}/static-route/entry") do |static_route_entry| # rubocop:disable Style/CollectionMethods @@ -51,17 +51,17 @@ def get(context) def create(context, name, should) paths = name[:route].split('/') context.type.definition[:base_xpath] = "/config/devices/entry/network/virtual-router/entry[@name='#{paths[0]}']/routing-table/#{@version_label}/static-route/entry[@name='#{paths[1]}']/path-monitor/monitor-destinations" # rubocop:disable Metrics/LineLength - context.device.set_config(context.type.definition[:base_xpath], xml_from_should(name, should)) + context.transport.set_config(context.type.definition[:base_xpath], xml_from_should(name, should)) end def update(context, name, should) paths = name[:route].split('/') context.type.definition[:base_xpath] = "/config/devices/entry/network/virtual-router/entry[@name='#{paths[0]}']/routing-table/#{@version_label}/static-route/entry[@name='#{paths[1]}']/path-monitor/monitor-destinations" # rubocop:disable Metrics/LineLength - context.device.set_config(context.type.definition[:base_xpath], xml_from_should(name, should)) + context.transport.set_config(context.type.definition[:base_xpath], xml_from_should(name, should)) end def delete(context, name) names = name[:route].split('/') - context.device.delete_config(context.type.definition[:base_xpath] + "/entry[@name='#{names[0]}']/routing-table/#{@version_label}/static-route/entry[@name='#{names[1]}']/path-monitor/monitor-destinations/entry[@name='#{name[:path]}']") # rubocop:disable Metrics/LineLength + context.transport.delete_config(context.type.definition[:base_xpath] + "/entry[@name='#{names[0]}']/routing-table/#{@version_label}/static-route/entry[@name='#{names[1]}']/path-monitor/monitor-destinations/entry[@name='#{name[:path]}']") # rubocop:disable Metrics/LineLength end end diff --git a/lib/puppet/provider/panos_provider.rb b/lib/puppet/provider/panos_provider.rb index 35c5dcaf..bf339d89 100644 --- a/lib/puppet/provider/panos_provider.rb +++ b/lib/puppet/provider/panos_provider.rb @@ -9,7 +9,7 @@ def initialize end def get(context) - config = context.device.get_config(context.type.definition[:base_xpath] + '/entry') + config = context.transport.get_config(context.type.definition[:base_xpath] + '/entry') config.elements.collect('/response/result/entry') do |entry| # rubocop:disable Style/CollectionMethods result = {} context.type.attributes.each do |attr_name, attr| @@ -21,16 +21,16 @@ def get(context) def create(context, name, should) validate_should(should) if defined? validate_should - context.device.set_config(context.type.definition[:base_xpath], xml_from_should(name, should)) + context.transport.set_config(context.type.definition[:base_xpath], xml_from_should(name, should)) end def update(context, name, should) validate_should(should) if defined? validate_should - context.device.edit_config(context.type.definition[:base_xpath] + "/entry[@name='#{name}']", xml_from_should(name, should)) + context.transport.edit_config(context.type.definition[:base_xpath] + "/entry[@name='#{name}']", xml_from_should(name, should)) end def delete(context, name) - context.device.delete_config(context.type.definition[:base_xpath] + "/entry[@name='#{name}']") + context.transport.delete_config(context.type.definition[:base_xpath] + "/entry[@name='#{name}']") end def match(entry, attr, attr_name) diff --git a/lib/puppet/provider/panos_static_route_base.rb b/lib/puppet/provider/panos_static_route_base.rb index 6774fc0c..448fdbde 100644 --- a/lib/puppet/provider/panos_static_route_base.rb +++ b/lib/puppet/provider/panos_static_route_base.rb @@ -61,7 +61,7 @@ def validate_should(should) # Overiding the get method, as the base xpath points towards virtual routers, and therefore the base provider's get will only return once for each VR. def get(context) results = [] - config = context.device.get_config(context.type.definition[:base_xpath] + '/entry') + config = context.transport.get_config(context.type.definition[:base_xpath] + '/entry') config.elements.collect('/response/result/entry') do |entry| # rubocop:disable Style/CollectionMethods vr_name = REXML::XPath.match(entry, 'string(@name)').first # rubocop:disable Style/CollectionMethods @@ -84,16 +84,16 @@ def get(context) def create(context, name, should) context.type.definition[:base_xpath] = "/config/devices/entry/network/virtual-router/entry[@name='#{name[:vr_name]}']/routing-table/#{@version_label}/static-route" validate_should(should) - context.device.set_config(context.type.definition[:base_xpath], xml_from_should(name, should)) + context.transport.set_config(context.type.definition[:base_xpath], xml_from_should(name, should)) end def update(context, name, should) context.type.definition[:base_xpath] = "/config/devices/entry/network/virtual-router/entry[@name='#{name[:vr_name]}']/routing-table/#{@version_label}/static-route" validate_should(should) - context.device.set_config(context.type.definition[:base_xpath], xml_from_should(name, should)) + context.transport.set_config(context.type.definition[:base_xpath], xml_from_should(name, should)) end def delete(context, name) - context.device.delete_config(context.type.definition[:base_xpath] + "/entry[@name='#{name[:vr_name]}']/routing-table/#{@version_label}/static-route/entry[@name='#{name[:route]}']") + context.transport.delete_config(context.type.definition[:base_xpath] + "/entry[@name='#{name[:vr_name]}']/routing-table/#{@version_label}/static-route/entry[@name='#{name[:route]}']") end end diff --git a/lib/puppet/transport/panos.rb b/lib/puppet/transport/panos.rb new file mode 100644 index 00000000..35c0e117 --- /dev/null +++ b/lib/puppet/transport/panos.rb @@ -0,0 +1,287 @@ +require 'net/http' +require 'openssl' +require 'rexml/document' +require 'securerandom' +require 'cgi' + +module Puppet::Transport + # The main connection class to a PAN-OS API endpoint + class Panos + def self.validate_connection_info(connection_info) + raise Puppet::ResourceError, 'Could not find "username"/"password" or "apikey" in the configuration' unless (connection_info.key?(:username) && connection_info.key?(:password)) || connection_info.key?(:apikey) # rubocop:disable Metrics/LineLength + connection_info + end + + # attr_reader :config + + def initialize(_context, connection_info) + @connection_info = self.class.validate_connection_info(connection_info) + end + + def facts(context) + @facts ||= parse_device_facts(fetch_device_facts(context)) + end + + def fetch_device_facts(context) + context.debug('Retrieving PANOS Device Facts') + # https:///api/?key=apikey&type=version + api.request('version') + end + + def parse_device_facts(response) + facts = {} + + model = response.elements['/response/result/model'].text + version = response.elements['/response/result/sw-version'].text + vsys = response.elements['/response/result/multi-vsys'].text + + facts['operatingsystem'] = model if model + facts['operatingsystemrelease'] = version if version + facts['multi-vsys'] = vsys if vsys + facts + end + + def get_config(xpath) + Puppet.debug("Retrieving #{xpath}") + # https:///api/?key=apikey&type=config&action=get&xpath= + api.request('config', action: 'get', xpath: xpath) + end + + def set_config(xpath, document) + Puppet.debug("Writing to #{xpath}") + # https:///api/?key=apikey&type=config&action=set&xpath=xpath-value&element=element-value + api.request('config', action: 'set', xpath: xpath, element: document) + end + + def edit_config(xpath, document) + Puppet.debug("Updating #{xpath}") + # https:///api/?key=apikey&type=config&action=edit&xpath=xpath-value&element=element-value + api.request('config', action: 'edit', xpath: xpath, element: document) + end + + def delete_config(xpath) + Puppet.debug("Deleting #{xpath}") + # https:///api/?key=apikey&type=config&action=delete&xpath=xpath-value + api.request('config', action: 'delete', xpath: xpath) + end + + def import(file_path, category) + Puppet.debug("Importing #{category}") + # https:///api/?key=apikey&type=import&category=category + # POST: File(file_path) + api.upload('import', file_path, category: category) + end + + def load_config(file_name) + Puppet.debug('Loading Config') + # https:///api/?type=op&cmd=file_name + api.request('op', cmd: "#{file_name}") + end + + def show_config + Puppet.debug('Retrieving Config') + # https:///api/?type=op&cmd= + api.request('op', cmd: '') + end + + def outstanding_changes? + # /api/?type=op&cmd= + result = api.request('op', cmd: '') + result.elements['/response/result'].text == 'yes' + end + + def validate + Puppet.debug('Validating configuration') + # https:///api/?type=op&cmd= + api.job_request('op', cmd: '') + end + + def commit + Puppet.debug('Committing outstanding changes') + # https:///api/?type=commit&cmd= + api.job_request('commit', cmd: '') + end + + private + + def api + @api ||= API.new(@connection_info) + end + + # A simple adaptor to expose the basic PAN-OS XML API operations. + # Having this in a separate class aids with keeping the gnarly HTTP code + # away from the business logic, and helps with testing, too. + # @api private + class API + def initialize(connection_info) + @host = connection_info[:host] || connection_info[:address] + @port = connection_info.key?(:port) ? connection_info[:port].to_i : 443 + @user = connection_info[:user] || connection_info[:username] + @password = connection_info[:password] + @apikey = connection_info[:apikey] + end + + def http + @http ||= begin + Puppet.debug('Connecting to https://%{host}:%{port}' % { host: @host, port: @port }) + Net::HTTP.start(@host, @port, + use_ssl: true, + verify_mode: OpenSSL::SSL::VERIFY_NONE) + end + end + + def fetch_apikey(user, password) + uri = URI::HTTP.build(path: '/api/') + params = { type: 'keygen', user: user, password: password } + uri.query = URI.encode_www_form(params) + + res = http.get(uri) + unless res.is_a?(Net::HTTPSuccess) + raise "Error: #{res}: #{res.message}" + end + doc = REXML::Document.new(res.body) + handle_response_errors(doc) + doc.elements['/response/result/key'].text + end + + def apikey + @apikey ||= fetch_apikey(@user, @password) + end + + def request(type, **options) + params = { type: type, key: apikey } + params.merge!(options) + + uri = URI::HTTP.build(path: '/api/') + uri.query = URI.encode_www_form(params) + + res = http.get(uri) + unless res.is_a?(Net::HTTPSuccess) + raise "Error: #{res}: #{res.message}" + end + doc = REXML::Document.new(res.body) + handle_response_errors(doc) + doc + end + + def upload(type, file, **options) + params = { type: type, key: apikey } + params.merge!(options) + + uri = URI::HTTP.build(path: '/api/') + uri.query = URI.encode_www_form(params) + + raise Puppet::ResourceError, "File: `#{file}` does not exist" unless File.exist?(file) + + # from: http://www.rubyinside.com/nethttp-cheat-sheet-2940.html + # Token used to terminate the file in the post body. + @boundary ||= SecureRandom.hex(25) + + post_body = [] + post_body << "--#{@boundary}\r\n" + post_body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{CGI.escape(File.basename(file))}\"\r\n" + post_body << "Content-Type: text/plain\r\n" + post_body << "\r\n" + post_body << File.open(file, 'rb') { |f| f.read } + post_body << "\r\n--#{@boundary}--\r\n" + + request = Net::HTTP::Post.new(uri.request_uri) + request.body = post_body.join + request.content_type = "multipart/form-data, boundary=#{@boundary}" + + res = http.request(request) + unless res.is_a?(Net::HTTPSuccess) + raise "Error: #{res}: #{res.message}" + end + doc = REXML::Document.new(res.body) + handle_response_errors(doc) + doc + end + + def job_request(type, **options) + result = request(type, options) + response_message = result.elements['/response/msg'] + if response_message + Puppet.debug('api response (no changes): %{msg}' % { msg: response_message.text }) + return + end + + job_id = result.elements['/response/result/job'].text + job_msg = [] + result.elements['/response/result/msg'].each_element_with_text { |e| job_msg << e.text } + Puppet.debug('api response (job queued): %{msg}' % { msg: job_msg.join("\n") }) + + tries = 0 + loop do + # https:///api/?type=op&cmd=4 + poll_result = request('op', cmd: "#{job_id}") + status = poll_result.elements['/response/result/job/status'].text + result = poll_result.elements['/response/result/job/result'].text + progress = poll_result.elements['/response/result/job/progress'].text + details = [] + poll_result.elements['/response/result/job/details'].each_element_with_text { |e| details << e.text } + if status == 'FIN' + # TODO: go to debug + # poll_result.write($stdout, 2) + break if result == 'OK' + raise Puppet::ResourceError, 'job failed. result="%{result}": %{details}' % { result: result, details: details.join("\n") } + end + tries += 1 + + details.unshift("sleeping for #{tries} seconds") + Puppet.debug('job still in progress (%{progress}%%). result="%{result}": %{details}' % { result: result, progress: progress, details: details.join("\n") }) + sleep tries + end + + Puppet.debug('job was successful') + end + + def message_from_code(code) + message_codes ||= begin + h = Hash.new { |_hash, key| 'Unknown error code %{code}' % { code: key } } + h['1'] = 'Unknown command: The specific config or operational command is not recognized.' + h['2'] = "Internal error: Check with Palo Alto's technical support." + h['3'] = "Internal error: Check with Palo Alto's technical support." + h['4'] = "Internal error: Check with Palo Alto's technical support." + h['5'] = "Internal error: Check with Palo Alto's technical support." + h['11'] = "Internal error: Check with Palo Alto's technical support." + h['21'] = "Internal error: Check with Palo Alto's technical support." + h['6'] = 'Bad XPath: The xpath specified in one or more attributes of the command is invalid.' + h['7'] = "Object not present: Object specified by the xpath is not present. For example, entry[@name='value'] where no object with name 'value' is present." + h['8'] = 'Object not unique: For commands that operate on a single object, the specified object is not unique.' + h['10'] = 'Reference count not zero: Object cannot be deleted as there are other objects that refer to it. For example,:addressobject still in use in policy.' + h['12'] = 'Invalid object: Xpath or element values provided are not complete.' + h['14'] = 'Operation not possible: Operation is allowed but not possible in this case. For example, moving a rule up one position when it is already at the top.' + h['15'] = 'Operation denied: Operation is allowed. For example, Admin not allowed to delete own account, Running a command that is not allowed on a passive device.' + h['16'] = 'Unauthorized: The API role does not have access rights to run this query.' + h['17'] = 'Invalid command: Invalid command or parameters.' + h['18'] = 'Malformed command: The XML is malformed.' + h['19'] = 'Success: Command completed successfully.' + h['20'] = 'Success: Command completed successfully.' + h['22'] = 'Session timed out: The session for this query timed out.' + h + end + message_codes[code] + end + + def handle_response_errors(doc) + status = doc.elements['/response'].attributes['status'] + code = doc.elements['/response'].attributes['code'] + error_message = ('Received "%{status}" with code %{code}: %{message}' % { + status: status, + code: code, + message: message_from_code(code), + }) + # require 'pry';binding.pry + if status == 'success' + # Messages without a code require more processing by the caller + Puppet.debug(error_message) if code + else + error_message << "\n" + doc.write(error_message, 2) + raise Puppet::ResourceError, error_message + end + end + end + end +end diff --git a/lib/puppet/transport/schema/panos.rb b/lib/puppet/transport/schema/panos.rb new file mode 100644 index 00000000..fd9e6698 --- /dev/null +++ b/lib/puppet/transport/schema/panos.rb @@ -0,0 +1,36 @@ +require 'puppet/resource_api' + +Puppet::ResourceApi.register_transport( + name: 'panos', + desc: <<-EOS, +This transport connects to Palo Alto Firewalls using their HTTP XML API. +EOS + features: [], + connection_info: { + address: { + type: 'String', + desc: 'The FQDN or IP address of the firewall to connect to.', + }, + port: { + type: 'Optional[Integer]', + desc: 'The port of the firewall to connect to.', + }, + username: { + type: 'Optional[String]', + desc: 'The username to use for authenticating all connections to the firewall. Only one of `username`/`password` or `apikey` can be specified.', + }, + password: { + type: 'Optional[String]', + desc: 'The password to use for authenticating all connections to the firewall. Only one of `username`/`password` or `apikey` can be specified.', + }, + apikey: { + type: 'Optional[String]', + desc: <<-EOS, +The API key to use for authenticating all connections to the firewall. +Only one of `username`/`password` or `apikey` can be specified. +Using the API key is preferred, because it avoids storing a password +in the clear, and is easily revoked by changing the password on the associated user. +EOS + }, + }, +) diff --git a/lib/puppet/util/network_device/panos/device.rb b/lib/puppet/util/network_device/panos/device.rb index a69c17c7..1ea1b5a2 100644 --- a/lib/puppet/util/network_device/panos/device.rb +++ b/lib/puppet/util/network_device/panos/device.rb @@ -1,286 +1,13 @@ -require 'net/http' -require 'openssl' -require 'puppet/util/network_device/simple/device' -require 'rexml/document' -require 'securerandom' -require 'cgi' +require 'puppet' +require 'puppet/resource_api/transport/wrapper' +# force registering the transport +require 'puppet/transport/schema/panos' module Puppet::Util::NetworkDevice::Panos - # The main connection class to a PAN-OS API endpoint - class Device < Puppet::Util::NetworkDevice::Simple::Device - def facts - @facts ||= parse_device_facts(fetch_device_facts) - end - - def config - raise Puppet::ResourceError, 'Could not find host or address in the configuration' unless super.key?('host') || super.key?('address') - raise Puppet::ResourceError, 'The port attribute in the configuration is not an integer' if super.key?('port') && super['port'] !~ %r{\A[0-9]+\Z} - raise Puppet::ResourceError, 'Could not find user/password or apikey in the configuration' unless ((super.key?('user') || super.key?('username')) && super.key?('password')) || super.key?('apikey') # rubocop:disable Metrics/LineLength - raise Puppet::ResourceError, 'User and username are mutually exclusive' if super.key?('user') && super.key?('username') - raise Puppet::ResourceError, 'Host and address are mutually exclusive' if super.key?('host') && super.key?('address') - super - end - - def fetch_device_facts - Puppet.debug('Retrieving PANOS Device Facts') - # https:///api/?key=apikey&type=version - api.request('version') - end - - def parse_device_facts(response) - facts = {} - - model = response.elements['/response/result/model'].text - version = response.elements['/response/result/sw-version'].text - vsys = response.elements['/response/result/multi-vsys'].text - - facts['operatingsystem'] = model if model - facts['operatingsystemrelease'] = version if version - facts['multi-vsys'] = vsys if vsys - facts - end - - def get_config(xpath) - Puppet.debug("Retrieving #{xpath}") - # https:///api/?key=apikey&type=config&action=get&xpath= - api.request('config', action: 'get', xpath: xpath) - end - - def set_config(xpath, document) - Puppet.debug("Writing to #{xpath}") - # https:///api/?key=apikey&type=config&action=set&xpath=xpath-value&element=element-value - api.request('config', action: 'set', xpath: xpath, element: document) - end - - def edit_config(xpath, document) - Puppet.debug("Updating #{xpath}") - # https:///api/?key=apikey&type=config&action=edit&xpath=xpath-value&element=element-value - api.request('config', action: 'edit', xpath: xpath, element: document) - end - - def delete_config(xpath) - Puppet.debug("Deleting #{xpath}") - # https:///api/?key=apikey&type=config&action=delete&xpath=xpath-value - api.request('config', action: 'delete', xpath: xpath) - end - - def import(file_path, category) - Puppet.debug("Importing #{category}") - # https:///api/?key=apikey&type=import&category=category - # POST: File(file_path) - api.upload('import', file_path, category: category) - end - - def load_config(file_name) - Puppet.debug('Loading Config') - # https:///api/?type=op&cmd=file_name - api.request('op', cmd: "#{file_name}") - end - - def show_config - Puppet.debug('Retrieving Config') - # https:///api/?type=op&cmd= - api.request('op', cmd: '') - end - - def outstanding_changes? - # /api/?type=op&cmd= - result = api.request('op', cmd: '') - result.elements['/response/result'].text == 'yes' - end - - def validate - Puppet.debug('Validating configuration') - # https:///api/?type=op&cmd= - api.job_request('op', cmd: '') - end - - def commit - Puppet.debug('Committing outstanding changes') - # https:///api/?type=commit&cmd= - api.job_request('commit', cmd: '') - end - - private - - def api - @api ||= API.new(config) - end - end - - # A simple adaptor to expose the basic PAN-OS XML API operations. - # Having this in a separate class aids with keeping the gnarly HTTP code - # away from the business logic, and helps with testing, too. - # @api private - class API - def initialize(credentials) - @host = credentials['host'] || credentials['address'] - @port = credentials.key?('port') ? credentials['port'].to_i : 443 - @user = credentials['user'] || credentials['username'] - @password = credentials['password'] - @apikey = credentials['apikey'] - end - - def http - @http ||= begin - Puppet.debug('Connecting to https://%{host}:%{port}' % { host: @host, port: @port }) - Net::HTTP.start(@host, @port, - use_ssl: true, - verify_mode: OpenSSL::SSL::VERIFY_NONE) - end - end - - def fetch_apikey(user, password) - uri = URI::HTTP.build(path: '/api/') - params = { type: 'keygen', user: user, password: password } - uri.query = URI.encode_www_form(params) - - res = http.get(uri) - unless res.is_a?(Net::HTTPSuccess) - raise "Error: #{res}: #{res.message}" - end - doc = REXML::Document.new(res.body) - handle_response_errors(doc) - doc.elements['/response/result/key'].text - end - - def apikey - @apikey ||= fetch_apikey(@user, @password) - end - - def request(type, **options) - params = { type: type, key: apikey } - params.merge!(options) - - uri = URI::HTTP.build(path: '/api/') - uri.query = URI.encode_www_form(params) - - res = http.get(uri) - unless res.is_a?(Net::HTTPSuccess) - raise "Error: #{res}: #{res.message}" - end - doc = REXML::Document.new(res.body) - handle_response_errors(doc) - doc - end - - def upload(type, file, **options) - params = { type: type, key: apikey } - params.merge!(options) - - uri = URI::HTTP.build(path: '/api/') - uri.query = URI.encode_www_form(params) - - raise Puppet::ResourceError, "File: `#{file}` does not exist" unless File.exist?(file) - - # from: http://www.rubyinside.com/nethttp-cheat-sheet-2940.html - # Token used to terminate the file in the post body. - @boundary ||= SecureRandom.hex(25) - - post_body = [] - post_body << "--#{@boundary}\r\n" - post_body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{CGI.escape(File.basename(file))}\"\r\n" - post_body << "Content-Type: text/plain\r\n" - post_body << "\r\n" - post_body << File.open(file, 'rb') { |f| f.read } - post_body << "\r\n--#{@boundary}--\r\n" - - request = Net::HTTP::Post.new(uri.request_uri) - request.body = post_body.join - request.content_type = "multipart/form-data, boundary=#{@boundary}" - - res = http.request(request) - unless res.is_a?(Net::HTTPSuccess) - raise "Error: #{res}: #{res.message}" - end - doc = REXML::Document.new(res.body) - handle_response_errors(doc) - doc - end - - def job_request(type, **options) - result = request(type, options) - response_message = result.elements['/response/msg'] - if response_message - Puppet.debug('api response (no changes): %{msg}' % { msg: response_message.text }) - return - end - - job_id = result.elements['/response/result/job'].text - job_msg = [] - result.elements['/response/result/msg'].each_element_with_text { |e| job_msg << e.text } - Puppet.debug('api response (job queued): %{msg}' % { msg: job_msg.join("\n") }) - - tries = 0 - loop do - # https:///api/?type=op&cmd=4 - poll_result = request('op', cmd: "#{job_id}") - status = poll_result.elements['/response/result/job/status'].text - result = poll_result.elements['/response/result/job/result'].text - progress = poll_result.elements['/response/result/job/progress'].text - details = [] - poll_result.elements['/response/result/job/details'].each_element_with_text { |e| details << e.text } - if status == 'FIN' - # TODO: go to debug - # poll_result.write($stdout, 2) - break if result == 'OK' - raise Puppet::ResourceError, 'job failed. result="%{result}": %{details}' % { result: result, details: details.join("\n") } - end - tries += 1 - - details.unshift("sleeping for #{tries} seconds") - Puppet.debug('job still in progress (%{progress}%%). result="%{result}": %{details}' % { result: result, progress: progress, details: details.join("\n") }) - sleep tries - end - - Puppet.debug('job was successful') - end - - def message_from_code(code) - message_codes ||= begin - h = Hash.new { |_hash, key| 'Unknown error code %{code}' % { code: key } } - h['1'] = 'Unknown command: The specific config or operational command is not recognized.' - h['2'] = "Internal error: Check with Palo Alto's technical support." - h['3'] = "Internal error: Check with Palo Alto's technical support." - h['4'] = "Internal error: Check with Palo Alto's technical support." - h['5'] = "Internal error: Check with Palo Alto's technical support." - h['11'] = "Internal error: Check with Palo Alto's technical support." - h['21'] = "Internal error: Check with Palo Alto's technical support." - h['6'] = 'Bad XPath: The xpath specified in one or more attributes of the command is invalid.' - h['7'] = "Object not present: Object specified by the xpath is not present. For example, entry[@name='value'] where no object with name 'value' is present." - h['8'] = 'Object not unique: For commands that operate on a single object, the specified object is not unique.' - h['10'] = 'Reference count not zero: Object cannot be deleted as there are other objects that refer to it. For example, address object still in use in policy.' - h['12'] = 'Invalid object: Xpath or element values provided are not complete.' - h['14'] = 'Operation not possible: Operation is allowed but not possible in this case. For example, moving a rule up one position when it is already at the top.' - h['15'] = 'Operation denied: Operation is allowed. For example, Admin not allowed to delete own account, Running a command that is not allowed on a passive device.' - h['16'] = 'Unauthorized: The API role does not have access rights to run this query.' - h['17'] = 'Invalid command: Invalid command or parameters.' - h['18'] = 'Malformed command: The XML is malformed.' - h['19'] = 'Success: Command completed successfully.' - h['20'] = 'Success: Command completed successfully.' - h['22'] = 'Session timed out: The session for this query timed out.' - h - end - message_codes[code] - end - - def handle_response_errors(doc) - status = doc.elements['/response'].attributes['status'] - code = doc.elements['/response'].attributes['code'] - error_message = ('Received "%{status}" with code %{code}: %{message}' % { - status: status, - code: code, - message: message_from_code(code), - }) - # require 'pry';binding.pry - if status == 'success' - # Messages without a code require more processing by the caller - Puppet.debug(error_message) if code - else - error_message << "\n" - doc.write(error_message, 2) - raise Puppet::ResourceError, error_message - end + # connect to a panos transport using backwards compatible configuration + class Device < Puppet::ResourceApi::Transport::Wrapper + def initialize(url_or_config, _options = {}) + super('panos', url_or_config) end end end diff --git a/spec/unit/puppet/provider/panos_arbitrary_commands/panos_arbitrary_commands_spec.rb b/spec/unit/puppet/provider/panos_arbitrary_commands/panos_arbitrary_commands_spec.rb index 2200f52c..b87aae6e 100644 --- a/spec/unit/puppet/provider/panos_arbitrary_commands/panos_arbitrary_commands_spec.rb +++ b/spec/unit/puppet/provider/panos_arbitrary_commands/panos_arbitrary_commands_spec.rb @@ -8,7 +8,7 @@ module Puppet::Provider::PanosArbitraryCommands; end subject(:provider) { described_class.new } let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } - let(:device) { instance_double('Puppet::Util::NetworkDevice::Panos::Device', 'device') } + let(:transport) { instance_double('Puppet::ResourceApi::Transport::Panos', 'transport') } let(:typedef) { instance_double('Puppet::ResourceApi::TypeDefinition', 'typedef') } let(:example_data) do @@ -68,7 +68,7 @@ module Puppet::Provider::PanosArbitraryCommands; end end before(:each) do - allow(context).to receive(:device).with(no_args).and_return(device) + allow(context).to receive(:transport).with(no_args).and_return(transport) end describe '#canonicalize' do @@ -96,15 +96,15 @@ module Puppet::Provider::PanosArbitraryCommands; end end context 'when `names` is not nil' do it 'returns resource' do - allow(device).to receive(:get_config).with('/config/foo').and_return(example_data) + allow(transport).to receive(:get_config).with('/config/foo').and_return(example_data) allow(provider).to receive(:str_from_xml).and_return(parsed_xml) # rubocop:disable RSpec/SubjectStub expect(provider.get(context, ['foo'])).to eq(resource_data) end end - context 'when device issues an error' do - it 'allows for device errors to bubble up' do - allow(device).to receive(:get_config).with('/config/some').and_raise(Puppet::ResourceError, 'Some Error Message') + context 'when transport issues an error' do + it 'allows for transport errors to bubble up' do + allow(transport).to receive(:get_config).with('/config/some').and_raise(Puppet::ResourceError, 'Some Error Message') expect { provider.get(context, ['some']) }.to raise_error Puppet::ResourceError end @@ -115,7 +115,7 @@ module Puppet::Provider::PanosArbitraryCommands; end context 'when xml is valid' do it 'does not produce an error' do allow(REXML::Document).to receive(:new).with(parsed_xml).and_return(example_data) - allow(device).to receive(:set_config).with('/config/foo', example_data) + allow(transport).to receive(:set_config).with('/config/foo', example_data) expect { provider.create(context, 'foo', resource_data[0]) }.not_to raise_error end @@ -133,7 +133,7 @@ module Puppet::Provider::PanosArbitraryCommands; end context 'when xml is valid' do it 'does not produce an error' do allow(REXML::Document).to receive(:new).with(parsed_xml).and_return(example_data) - allow(device).to receive(:edit_config).with('/config/foo', example_data) + allow(transport).to receive(:edit_config).with('/config/foo', example_data) expect { provider.update(context, 'foo', resource_data[0]) }.not_to raise_error end @@ -149,7 +149,7 @@ module Puppet::Provider::PanosArbitraryCommands; end describe '#delete' do it 'calls provider functions' do - expect(device).to receive(:delete_config).with('/config/foo') + expect(transport).to receive(:delete_config).with('/config/foo') provider.delete(context, resource_data[0][:xpath]) end diff --git a/spec/unit/puppet/provider/panos_commit/panos_commit_spec.rb b/spec/unit/puppet/provider/panos_commit/panos_commit_spec.rb index 5d626d6d..3f9d4b94 100644 --- a/spec/unit/puppet/provider/panos_commit/panos_commit_spec.rb +++ b/spec/unit/puppet/provider/panos_commit/panos_commit_spec.rb @@ -7,11 +7,11 @@ subject(:provider) { described_class.new } let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } - let(:device) { instance_double('Puppet::Util::NetworkDevice::Panos::Device', 'device') } + let(:transport) { instance_double('Puppet::ResourceApi::Transport::Panos', 'transport') } before(:each) do - allow(context).to receive(:device).with(no_args).and_return(device) - allow(device).to receive(:outstanding_changes?).and_return(outstanding_changes) + allow(context).to receive(:transport).with(no_args).and_return(transport) + allow(transport).to receive(:outstanding_changes?).and_return(outstanding_changes) end describe '#get' do @@ -48,7 +48,7 @@ context 'when the user requested a commit' do it 'commits them' do allow(context).to receive(:updating).with('commit').and_yield - expect(device).to receive(:commit) + expect(transport).to receive(:commit) provider.set(context, 'commit' => { should: { commit: true } }) end end @@ -57,7 +57,7 @@ it 'ignores them' do expect(context).to receive(:info).with('changes detected, but skipping commit as requested') expect(context).not_to receive(:updating).with('commit').and_yield - expect(device).not_to receive(:commit) + expect(transport).not_to receive(:commit) provider.set(context, 'commit' => { should: { commit: false } }) end end diff --git a/spec/unit/puppet/provider/panos_path_monitor_base_spec.rb b/spec/unit/puppet/provider/panos_path_monitor_base_spec.rb index b7485eeb..7aa7b465 100644 --- a/spec/unit/puppet/provider/panos_path_monitor_base_spec.rb +++ b/spec/unit/puppet/provider/panos_path_monitor_base_spec.rb @@ -4,12 +4,12 @@ require 'support/shared_examples' RSpec.describe Puppet::Provider::PanosPathMonitorBase do let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } - let(:device) { instance_double('Puppet::Util::NetworkDevice::Panos::Device', 'device') } + let(:transport) { instance_double('Puppet::ResourceApi::Transport::Panos', 'transport') } let(:typedef) { instance_double('Puppet::ResourceApi::TypeDefinition', 'typedef') } let(:provider) { described_class.new('ip') } before(:each) do - allow(context).to receive(:device).with(no_args).and_return(device) + allow(context).to receive(:transport).with(no_args).and_return(transport) allow(context).to receive(:type).with(no_args).and_return(typedef) allow(typedef).to receive(:ensurable?).and_return(true) end @@ -145,8 +145,8 @@ allow(typedef).to receive(:definition).and_return(base_xpath: 'some_xpath') end - it 'allows device api error to bubble up' do - allow(device).to receive(:get_config).with('some_xpath/entry').and_raise(Puppet::ResourceError, 'Some Error Message') + it 'allows transport api error to bubble up' do + allow(transport).to receive(:get_config).with('some_xpath/entry').and_raise(Puppet::ResourceError, 'Some Error Message') expect { provider.get(context) }.to raise_error Puppet::ResourceError end @@ -156,7 +156,7 @@ let(:provider) { described_class.new(ip_version) } it 'processes resources' do - allow(device).to receive(:get_config).with('some_xpath/entry').and_return(example_data) + allow(transport).to receive(:get_config).with('some_xpath/entry').and_return(example_data) allow(typedef).to receive(:attributes).and_return(Puppet::Type.type(:panos_path_monitor).type_definition.attributes) expect(provider.get(context)).to eq resource_data @@ -168,7 +168,7 @@ let(:provider) { described_class.new(ip_version) } it 'processes resources' do - allow(device).to receive(:get_config).with('some_xpath/entry').and_return(example_data) + allow(transport).to receive(:get_config).with('some_xpath/entry').and_return(example_data) allow(typedef).to receive(:attributes).with(no_args).and_return(Puppet::Type.type(:panos_path_monitor).type_definition.attributes) expect(provider.get(context)).to eq resource_data @@ -193,7 +193,7 @@ it 'will call set_config' do expect(typedef).to receive(:definition).and_return(mystruct).twice expect(provider).to receive(:xml_from_should).with(namevars, anything) - expect(device).to receive(:set_config).with(expected_path, anything) + expect(transport).to receive(:set_config).with(expected_path, anything) provider.create(context, namevars, anything) end end @@ -216,7 +216,7 @@ it 'will call set_config' do expect(typedef).to receive(:definition).and_return(mystruct).twice expect(provider).to receive(:xml_from_should).with(namevars, anything) - expect(device).to receive(:set_config).with(expected_path, anything) + expect(transport).to receive(:set_config).with(expected_path, anything) provider.update(context, namevars, anything) end end @@ -242,7 +242,7 @@ it 'will call delete_config' do expect(typedef).to receive(:definition).and_return(mystruct) - expect(device).to receive(:delete_config).with(expected_path) + expect(transport).to receive(:delete_config).with(expected_path) provider.delete(context, namevars) end end diff --git a/spec/unit/puppet/provider/panos_provider_spec.rb b/spec/unit/puppet/provider/panos_provider_spec.rb index 2f54f2ef..d82b6182 100644 --- a/spec/unit/puppet/provider/panos_provider_spec.rb +++ b/spec/unit/puppet/provider/panos_provider_spec.rb @@ -6,7 +6,7 @@ subject(:provider) { described_class.new } let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } - let(:device) { instance_double('Puppet::Util::NetworkDevice::Panos::Device', 'device') } + let(:transport) { instance_double('Puppet::ResourceApi::Transport::Panos', 'transport') } let(:typedef) { instance_double('Puppet::ResourceApi::TypeDefinition', 'typedef') } let(:attrs) do @@ -89,7 +89,7 @@ end before(:each) do - allow(context).to receive(:device).with(no_args).and_return(device) + allow(context).to receive(:transport).with(no_args).and_return(transport) allow(context).to receive(:type).with(no_args).and_return(typedef) allow(context).to receive(:notice) allow(typedef).to receive(:definition).with(no_args).and_return(base_xpath: 'some xpath') @@ -101,13 +101,13 @@ describe '#get' do it 'processes resources' do allow(typedef).to receive(:attributes).with(no_args).and_return(attrs) - allow(device).to receive(:get_config).with('some xpath/entry').and_return(example_data) + allow(transport).to receive(:get_config).with('some xpath/entry').and_return(example_data) expect(provider.get(context)).to eq resource_data end - it 'allows device api error to bubble up' do + it 'allows transport api error to bubble up' do allow(typedef).to receive(:attributes).with(no_args).and_return(attrs) - allow(device).to receive(:get_config).with('some xpath/entry').and_raise(Puppet::ResourceError, 'Some Error Message') + allow(transport).to receive(:get_config).with('some xpath/entry').and_raise(Puppet::ResourceError, 'Some Error Message') expect { provider.get(context) }.to raise_error Puppet::ResourceError end @@ -216,7 +216,7 @@ describe 'create(context, name, should)' do it 'calls provider functions' do - expect(device).to receive(:set_config).with('some xpath', instance_of(String)) do |_xpath, doc| + expect(transport).to receive(:set_config).with('some xpath', instance_of(String)) do |_xpath, doc| expect(doc).to have_xml('entry/description', '<eas&lt;yxss/>') expect(doc).to have_xml('entry/isenabled', 'Yes') expect(doc).to have_xml('entry/enabled', 'No') @@ -230,7 +230,7 @@ describe 'update(context, name, should)' do it 'calls provider functions' do - expect(device).to receive(:edit_config).with('some xpath/entry[@name=\'value1\']', instance_of(String)) do |_xpath, doc| + expect(transport).to receive(:edit_config).with('some xpath/entry[@name=\'value1\']', instance_of(String)) do |_xpath, doc| expect(doc).to have_xml('entry/description', '<eas&lt;yxss/>') expect(doc).to have_xml('entry/isenabled', 'Yes') expect(doc).to have_xml('entry/enabled', 'No') @@ -245,7 +245,7 @@ describe 'delete(context, name)' do it 'calls provider functions' do - expect(device).to receive(:delete_config).with('some xpath/entry[@name=\'value1\']') + expect(transport).to receive(:delete_config).with('some xpath/entry[@name=\'value1\']') provider.delete(context, resource_data[0][:name]) end diff --git a/spec/unit/puppet/provider/panos_static_route_base_spec.rb b/spec/unit/puppet/provider/panos_static_route_base_spec.rb index 3c4ec316..e7bb0cfa 100644 --- a/spec/unit/puppet/provider/panos_static_route_base_spec.rb +++ b/spec/unit/puppet/provider/panos_static_route_base_spec.rb @@ -6,12 +6,12 @@ require 'puppet/type/panos_static_route' RSpec.describe Puppet::Provider::PanosStaticRouteBase do let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } - let(:device) { instance_double('Puppet::Util::NetworkDevice::Panos::Device', 'device') } + let(:transport) { instance_double('Puppet::ResourceApi::Transport::Panos', 'transport') } let(:typedef) { instance_double('Puppet::ResourceApi::TypeDefinition', 'typedef') } let(:provider) { described_class.new('ip') } before(:each) do - allow(context).to receive(:device).with(no_args).and_return(device) + allow(context).to receive(:transport).with(no_args).and_return(transport) allow(context).to receive(:type).with(no_args).and_return(typedef) allow(typedef).to receive(:ensurable?).and_return(true) end @@ -490,8 +490,8 @@ allow(typedef).to receive(:definition).and_return(base_xpath: 'some_xpath') end - it 'allows device api error to bubble up' do - allow(device).to receive(:get_config).with('some_xpath/entry').and_raise(Puppet::ResourceError, 'Some Error Message') + it 'allows transport api error to bubble up' do + allow(transport).to receive(:get_config).with('some_xpath/entry').and_raise(Puppet::ResourceError, 'Some Error Message') expect { provider.get(context) }.to raise_error Puppet::ResourceError end @@ -501,7 +501,7 @@ let(:provider) { described_class.new(ip_version) } it 'processes resources' do - allow(device).to receive(:get_config).with('some_xpath/entry').and_return(example_data) + allow(transport).to receive(:get_config).with('some_xpath/entry').and_return(example_data) allow(typedef).to receive(:attributes).and_return(Puppet::Type.type(:panos_static_route).type_definition.attributes) expect(provider.get(context)).to eq resource_data @@ -513,7 +513,7 @@ let(:provider) { described_class.new(ip_version) } it 'processes resources' do - allow(device).to receive(:get_config).with('some_xpath/entry').and_return(example_data) + allow(transport).to receive(:get_config).with('some_xpath/entry').and_return(example_data) allow(typedef).to receive(:attributes).with(no_args).and_return(Puppet::Type.type(:panos_ipv6_static_route).type_definition.attributes) expect(provider.get(context)).to eq resource_data @@ -539,7 +539,7 @@ expect(typedef).to receive(:definition).and_return(mystruct).twice expect(provider).to receive(:validate_should).with(anything) expect(provider).to receive(:xml_from_should).with(namevars, anything) - expect(device).to receive(:set_config).with(expected_path, anything) + expect(transport).to receive(:set_config).with(expected_path, anything) provider.create(context, namevars, anything) end end @@ -563,7 +563,7 @@ expect(typedef).to receive(:definition).and_return(mystruct).twice expect(provider).to receive(:validate_should).with(anything) expect(provider).to receive(:xml_from_should).with(namevars, anything) - expect(device).to receive(:set_config).with(expected_path, anything) + expect(transport).to receive(:set_config).with(expected_path, anything) provider.update(context, namevars, anything) end end @@ -589,7 +589,7 @@ it 'will call delete_config' do expect(typedef).to receive(:definition).and_return(mystruct) - expect(device).to receive(:delete_config).with(expected_path) + expect(transport).to receive(:delete_config).with(expected_path) provider.delete(context, namevars) end end diff --git a/spec/unit/puppet/transport/panos_spec.rb b/spec/unit/puppet/transport/panos_spec.rb new file mode 100644 index 00000000..b1601bcb --- /dev/null +++ b/spec/unit/puppet/transport/panos_spec.rb @@ -0,0 +1,397 @@ +require 'spec_helper' +require 'puppet/transport/panos' +require 'support/matchers/have_xml' + +RSpec.describe Puppet::Transport do + describe Puppet::Transport::Panos do + let(:transport) { described_class.new(context, connection_info) } + let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } + let(:connection_info) { { address: 'www.example.com', username: 'admin', password: 'password' } } + let(:api) { instance_double('Puppet::Transport::Panos::API', 'api') } + let(:xml_doc) { REXML::Document.new(device_response) } + let(:device_response) do + ' + + 7.1.0 + off + PA-VM + + ' + end + let(:fact_hash) do + { + 'operatingsystem' => 'PA-VM', + 'operatingsystemrelease' => '7.1.0', + 'multi-vsys' => 'off', + } + end + + before(:each) do + allow(context).to receive(:debug) + end + + it 'parses facts correctly' do + expect(transport.parse_device_facts(xml_doc)).to eq(fact_hash) + end + + describe '#new' do + # TODO: validation functionality should be tested in puppet-resource_api, not here + let(:transport) { Puppet::ResourceApi::Transport.connect('panos', connection_info) } + + context 'when address is not provided' do + let(:connection_info) { { username: 'admin', password: 'password' } } + + it { expect { transport }.to raise_error Puppet::ResourceError, %r{The following mandatory attributes were not provided:.*address}m } + end + context 'when port is provided but not valid' do + let(:connection_info) { { address: 'www.example.com', port: 'foo', username: 'admin', password: 'password' } } + + # TODO: rsapi should be checking this and raising an error + pending { expect { transport }.to raise_error Puppet::ResourceError, 'The port attribute in the configuration is not an integer' } + end + context 'when valid user credentials are not provided' do + [ + { address: 'www.example.com', username: 'admin' }, + { address: 'www.example.com', password: 'password' }, + { address: 'www.example.com' }, + ].each do |config| + let(:connection_info) { config } + + it { expect { transport }.to raise_error Puppet::ResourceError, 'Could not find "username"/"password" or "apikey" in the configuration' } + end + end + context 'when apikey is provided' do + let(:connection_info) { { address: 'www.example.com', apikey: 'foo' } } + + it { expect { transport }.not_to raise_error Puppet::ResourceError } + end + context 'when correct credentials are provided' do + let(:connection_info) { { address: 'www.example.com', username: 'foo', password: 'password' } } + + it { expect { transport }.not_to raise_error Puppet::ResourceError } + end + end + + context 'with the internal api mocked' do + before(:each) do + allow(transport).to receive(:api).with(no_args).and_return(api) + end + + describe '#facts' do + context 'when the response returns valid data' do + it 'parses device facts' do + expect(api).to receive(:request).with('version').and_return(REXML::Document.new(device_response)) + expect(transport.facts(context)).to eq(fact_hash) + end + end + end + + describe 'helper functions' do + let(:xpath) { '/some/xpath' } + let(:document) { 'test' } + + it '#get_config(xpath)' do + expect(api).to receive(:request).with('config', action: 'get', xpath: xpath) + transport.get_config(xpath) + end + + it '#set_config(xpath, document)' do + expect(api).to receive(:request).with('config', action: 'set', xpath: xpath, element: document) + transport.set_config(xpath, document) + end + + it '#edit_config(xpath, document)' do + expect(api).to receive(:request).with('config', action: 'edit', xpath: xpath, element: document) + transport.edit_config(xpath, document) + end + + it '#delete_config(xpath)' do + expect(api).to receive(:request).with('config', action: 'delete', xpath: xpath) + transport.delete_config(xpath) + end + end + + describe '#import(file_path, category)' do + let(:file_path) { '/some/file/path/file.txt' } + let(:category) { 'foo' } + + it 'calls the api correctly' do + expect(api).to receive(:upload).with('import', file_path, category: category) + transport.import(file_path, category) + end + end + + describe '#load_config(file_name)' do + let(:file_name) { 'file.txt' } + + it 'calls the api correctly' do + expect(api).to receive(:request).with('op', cmd: %r{#{file_name}}) + transport.load_config(file_name) + end + end + + describe '#show_config' do + it 'calls the api correctly' do + expect(api).to receive(:request).with('op', cmd: '') + transport.show_config + end + end + + describe '#outstanding_changes?' do + context 'when there are outstanding changes' do + let(:xml_response) { REXML::Document.new('yes') } + + it { + expect(api).to receive(:request).with('op', anything).and_return(xml_response) + expect(transport).to be_outstanding_changes + } + end + context 'when there are no outstanding changes' do + let(:xml_response) { REXML::Document.new('no') } + + it { + expect(api).to receive(:request).with('op', anything).and_return(xml_response) + expect(transport).not_to be_outstanding_changes + } + end + end + + describe '#validate' do + it 'calls the api correctly' do + expect(api).to receive(:job_request).with('op', anything) + transport.validate + end + end + + describe '#commit' do + it 'calls the api correctly' do + expect(api).to receive(:job_request).with('commit', anything) + transport.commit + end + end + end + + context 'without the internal api mocked' do + it 'makes a webcall' do + stub_request(:get, 'https://www.example.com/api/?password=password&type=keygen&user=admin') + .to_return(status: 200, body: "SOMEKEY") + + stub_request(:get, 'https://www.example.com/api/?key=SOMEKEY&type=version') + .to_return(status: 200, body: device_response) + + expect(transport.facts(context)).to eq(fact_hash) + end + end + end + + describe Puppet::Transport::Panos::API do + subject(:instance) { described_class.new(credentials) } + + let(:credentials) { { address: 'www.example.com' } } + + def stub_keygen_request(**options) + stub_request(:get, 'https://www.example.com/api/?password=password&type=keygen&user=user') + .to_return(options) + end + + def stub_api_request(**options) + stub_request(:get, 'https://www.example.com/api/?type=THETYPE&key=APIKEY&option_a=ANOPTION') + .to_return(options) + end + + def stub_upload_request(**options) + stub_request(:post, 'https://www.example.com/api/?key=APIKEY&type=THETYPE&category=CATEGORY') + .to_return(options) + # Error: "WebMock does not support matching body for multipart/form-data requests yet" + # .with(body: /filename=\"#{file_name}\".*#{Regexp.escape(file_content)}/m, + # headers: { + # 'Content-Type' => /multipart\/form-data/ + # }) + end + + describe '#fetch_apikey(user, password)' do + context 'with valid username and password' do + it 'fetches the API key' do + stub_keygen_request(status: 200, body: "SOMEKEY") + + expect(instance.fetch_apikey('user', 'password')).to eq 'SOMEKEY' + + expect(a_request(:get, 'https://www.example.com/api/?password=password&type=keygen&user=user')).to have_been_made.once + end + end + + context 'with invalid username and password' do + it 'raises a helpful error' do + stub_keygen_request(status: 403) + + expect { instance.fetch_apikey('user', 'password') }.to raise_error RuntimeError, %r{forbidden}i + + expect(a_request(:get, 'https://www.example.com/api/?password=password&type=keygen&user=user')).to have_been_made.once + end + end + end + + describe '#apikey' do + let(:credentials) { super().merge(username: 'user', password: 'password') } + + it 'makes only a single HTTP call' do + stub_keygen_request(status: 200, body: "SOMEKEY") + + expect(instance.apikey).to eq 'SOMEKEY' + expect(instance.apikey).to eq 'SOMEKEY' + + expect(a_request(:get, 'https://www.example.com/api/?password=password&type=keygen&user=user')).to have_been_made.once + end + end + + describe '#upload(file_name, file_content, **options)' do + let(:credentials) { super().merge(apikey: 'APIKEY') } + let(:doc) { instance.upload('THETYPE', '/path/to/file/test.txt', category: 'CATEGORY') } + let(:file_content) { 'some config info' } + + before(:each) do + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:open).and_return(file_content) + end + + context 'when the API returns success' do + before(:each) do + stub_upload_request(status: 200, body: "test.txt saved") + end + + it { + doc + expect(a_request(:post, 'https://www.example.com/api/?key=APIKEY&type=THETYPE&category=CATEGORY')).to have_been_made.once + } + it { expect(doc).to be_a REXML::Document } + it { expect(doc).to have_xml('response[@status="success"]') } + end + + context 'when the file provided does not exist' do + it do + allow(File).to receive(:exist?).and_return(false) + expect { doc }.to raise_error Puppet::ResourceError, 'File: `/path/to/file/test.txt` does not exist' + end + end + + context 'when the API returns an HTTP error' do + it do + stub_upload_request(status: 400, body: "TESTMESSAGE.") + + expect { doc }.to raise_error RuntimeError, %r{HTTPBadRequest} + end + end + context 'when the API returns a semantic error' do + it do + stub_upload_request(status: 200, body: "Malformed Request") + + expect { doc }.to raise_error Puppet::ResourceError, %r{Malformed Request} + end + end + end + + describe '#request(type, **options)' do + let(:credentials) { super().merge(apikey: 'APIKEY') } + let(:doc) { instance.request('THETYPE', option_a: 'ANOPTION') } + + context 'when the API returns success' do + before(:each) do + stub_api_request(status: 200, body: "") + end + + it { + doc + expect(a_request(:get, 'https://www.example.com/api/?type=THETYPE&key=APIKEY&option_a=ANOPTION')).to have_been_made.once + } + it { expect(doc).to be_a REXML::Document } + it { expect(doc).to have_xml('response[@status="success"]') } + end + + context 'when the API returns an HTTP error' do + it do + stub_api_request(status: 400, body: "TESTMESSAGE.") + + expect { doc }.to raise_error RuntimeError, %r{HTTPBadRequest} + end + end + context 'when the API returns a semantic error' do + it do + stub_api_request(status: 200, body: "Malformed Request") + + expect { doc }.to raise_error Puppet::ResourceError, %r{Malformed Request} + end + end + end + + describe '#job_request(type, **options)' do + let(:credentials) { super().merge(apikey: 'APIKEY') } + + before(:each) do + # disable sleeping, due to how this is called (objects inheriting from Kernel) this requires a lot of wrangling + # See https://stackoverflow.com/a/27749263/4918 + allow_any_instance_of(Object).to receive(:sleep) # rubocop:disable RSpec/AnyInstance + end + + # this part is a bit wonky, because "commit" is currently the only job we're using/testing + # other async jobs might never return this, or return something different + context 'when the job is not required' do + before(:each) do + stub_request(:get, 'https://www.example.com/api/?cmd=&key=APIKEY&type=commit') + .to_return(status: 200, body: 'There are no changes to commit.') + end + + it 'returns immediately' do + instance.job_request('commit', cmd: '') + end + end + + context 'straight to success' do + it do + stub_request(:get, 'https://www.example.com/api/?cmd=&key=APIKEY&type=commit') + .to_return(status: 200, body: 'Commit job enqueued with jobid 22') + # rubocop:disable Metrics/LineLength + stub_request(:get, 'https://www.example.com/api/?cmd=2&key=APIKEY&type=op') + .to_return(status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitFINNOnoOK04:36:570100
Configuration committed successfully
') + # rubocop:enable Metrics/LineLength + + instance.job_request('commit', cmd: '') + end + end + context 'waiting for it' do + it do + stub_request(:get, 'https://www.example.com/api/?cmd=&key=APIKEY&type=commit') + .to_return(status: 200, body: 'Commit job enqueued with jobid 22') + # rubocop:disable Metrics/LineLength + stub_request(:get, 'https://www.example.com/api/?cmd=2&key=APIKEY&type=op') + .to_return([ + { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOyesPENDStill Active00
' }, + { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOyesPENDStill Active00
' }, + { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOyesPENDStill Active075
' }, + { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOnoPENDStill Active099
' }, + { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitFINNOnoOK04:36:570100
Configuration committed successfully
' }, + ]) + # rubocop:enable Metrics/LineLength + + instance.job_request('commit', cmd: '') + end + end + + context 'when the job fails' do + it do + stub_request(:get, 'https://www.example.com/api/?cmd=&key=APIKEY&type=op') + .to_return(status: 200, body: 'Validate job enqueued with jobid 22') + # rubocop:disable Metrics/LineLength + stub_request(:get, 'https://www.example.com/api/?cmd=2&key=APIKEY&type=op') + .to_return([ + { status: 200, body: '2018/06/12 09:26:4509:26:458adminValidateACTNOyesPENDStill Active00
' }, + { status: 200, body: '2018/06/12 09:26:4509:26:458adminValidateACTNOyesPENDStill Active00
' }, + { status: 200, body: '2018/06/12 09:26:4509:26:458adminValidateFINNOnoFAIL09:26:470100
Validation Error: address-3 Node ip-netmask(line 30226) and ip-range(line 30227) are mutually exclusive]]> address-3 Node ip-netmask(line 30226) and fqdn(line 30228) are mutually exclusive]]>
' }, + ]) + # rubocop:enable Metrics/LineLength + + expect { instance.job_request('op', cmd: '') }.to raise_error Puppet::ResourceError, %r{Validation Error:} + end + end + end + end +end diff --git a/spec/unit/puppet/util/network_device/panos/device_spec.rb b/spec/unit/puppet/util/network_device/panos/device_spec.rb index ad80ffa5..1ea1dbb5 100644 --- a/spec/unit/puppet/util/network_device/panos/device_spec.rb +++ b/spec/unit/puppet/util/network_device/panos/device_spec.rb @@ -1,409 +1,9 @@ -require 'spec_helper' require 'puppet/util/network_device/panos/device' -require 'support/matchers/have_xml' -RSpec.describe Puppet::Util::NetworkDevice::Panos do - describe Puppet::Util::NetworkDevice::Panos::Device do - let(:device) { described_class.new(device_config) } - let(:device_config) { { 'host' => 'www.example.com', 'user' => 'admin', 'password' => 'password' } } - let(:api) { instance_double('Puppet::Util::NetworkDevice::Panos::API', 'api') } - let(:xml_doc) { REXML::Document.new(device_response) } - let(:device_response) do - ' - - 7.1.0 - off - PA-VM - - ' - end - let(:fact_hash) do - { - 'operatingsystem' => 'PA-VM', - 'operatingsystemrelease' => '7.1.0', - 'multi-vsys' => 'off', - } - end +RSpec.describe Puppet::Util::NetworkDevice::Panos::Device do + let(:connection_info) { { address: 'www.example.com', username: 'foo', password: 'password' } } - it 'parses facts correctly' do - expect(device.parse_device_facts(xml_doc)).to eq(fact_hash) - end - - context 'with the internal api mocked' do - before(:each) do - allow(device).to receive(:api).with(no_args).and_return(api) - end - - describe '#facts' do - context 'when the response returns valid data' do - it 'parses device facts' do - expect(api).to receive(:request).with('version').and_return(REXML::Document.new(device_response)) - expect(device.facts).to eq(fact_hash) - end - end - end - - describe '#config' do - context 'when host is not provided' do - let(:device_config) { { 'user' => 'admin', 'password' => 'password' } } - - it { expect { device.config }.to raise_error Puppet::ResourceError, 'Could not find host or address in the configuration' } - end - context 'when port is provided but not valid' do - let(:device_config) { { 'host' => 'www.example.com', 'port' => 'foo', 'user' => 'admin', 'password' => 'password' } } - - it { expect { device.config }.to raise_error Puppet::ResourceError, 'The port attribute in the configuration is not an integer' } - end - context 'when valid user credentials are not provided' do - [ - { 'host' => 'www.example.com', 'user' => 'admin' }, - { 'host' => 'www.example.com', 'password' => 'password' }, - { 'host' => 'www.example.com', 'username' => 'admin' }, - { 'host' => 'www.example.com' }, - ].each do |config| - let(:device_config) { config } - - it { expect { device.config }.to raise_error Puppet::ResourceError, 'Could not find user/password or apikey in the configuration' } - end - end - context 'when apikey is provided' do - let(:device_config) { { 'host' => 'www.example.com', 'apikey' => 'foo' } } - - it { expect { device.config }.not_to raise_error Puppet::ResourceError } - end - context 'when `user` and password is provided' do - let(:device_config) { { 'host' => 'www.example.com', 'user' => 'foo', 'password' => 'password' } } - - it { expect { device.config }.not_to raise_error Puppet::ResourceError } - end - context 'when `username` and password is provided' do - let(:device_config) { { 'host' => 'www.example.com', 'username' => 'foo', 'password' => 'password' } } - - it { expect { device.config }.not_to raise_error Puppet::ResourceError } - end - context 'when `host` and `address` and password is provided' do - let(:device_config) { { 'host' => 'www.example.com', 'address' => 'www.example.com', 'username' => 'foo', 'password' => 'password' } } - - it { expect { device.config }.to raise_error Puppet::ResourceError, 'Host and address are mutually exclusive' } - end - context 'when `address` is provided' do - let(:device_config) { { 'address' => 'www.example.com', 'username' => 'foo', 'password' => 'password' } } - - it { expect { device.config }.not_to raise_error } - end - context 'when `user` and `username` and password is provided' do - let(:device_config) { { 'host' => 'www.example.com', 'user' => 'foo', 'username' => 'foo', 'password' => 'password' } } - - it { expect { device.config }.to raise_error Puppet::ResourceError, 'User and username are mutually exclusive' } - end - end - - describe 'helper functions' do - let(:xpath) { '/some/xpath' } - let(:document) { 'test' } - - it '#get_config(xpath)' do - expect(api).to receive(:request).with('config', action: 'get', xpath: xpath) - device.get_config(xpath) - end - - it '#set_config(xpath, document)' do - expect(api).to receive(:request).with('config', action: 'set', xpath: xpath, element: document) - device.set_config(xpath, document) - end - - it '#edit_config(xpath, document)' do - expect(api).to receive(:request).with('config', action: 'edit', xpath: xpath, element: document) - device.edit_config(xpath, document) - end - - it '#delete_config(xpath)' do - expect(api).to receive(:request).with('config', action: 'delete', xpath: xpath) - device.delete_config(xpath) - end - end - - describe '#import(file_path, category)' do - let(:file_path) { '/some/file/path/file.txt' } - let(:category) { 'foo' } - - it 'calls the api correctly' do - expect(api).to receive(:upload).with('import', file_path, category: category) - device.import(file_path, category) - end - end - - describe '#load_config(file_name)' do - let(:file_name) { 'file.txt' } - - it 'calls the api correctly' do - expect(api).to receive(:request).with('op', cmd: %r{#{file_name}}) - device.load_config(file_name) - end - end - - describe '#show_config' do - it 'calls the api correctly' do - expect(api).to receive(:request).with('op', cmd: '') - device.show_config - end - end - - describe '#outstanding_changes?' do - context 'when there are outstanding changes' do - let(:xml_response) { REXML::Document.new('yes') } - - it { - expect(api).to receive(:request).with('op', anything).and_return(xml_response) - expect(device).to be_outstanding_changes - } - end - context 'when there are no outstanding changes' do - let(:xml_response) { REXML::Document.new('no') } - - it { - expect(api).to receive(:request).with('op', anything).and_return(xml_response) - expect(device).not_to be_outstanding_changes - } - end - end - - describe '#validate' do - it 'calls the api correctly' do - expect(api).to receive(:job_request).with('op', anything) - device.validate - end - end - - describe '#commit' do - it 'calls the api correctly' do - expect(api).to receive(:job_request).with('commit', anything) - device.commit - end - end - end - - context 'without the internal api mocked' do - it 'makes a webcall' do - stub_request(:get, 'https://www.example.com/api/?password=password&type=keygen&user=admin') - .to_return(status: 200, body: "SOMEKEY") - - stub_request(:get, 'https://www.example.com/api/?key=SOMEKEY&type=version') - .to_return(status: 200, body: device_response) - - expect(device.facts).to eq(fact_hash) - end - end - end - - describe Puppet::Util::NetworkDevice::Panos::API do - subject(:instance) { described_class.new(credentials) } - - let(:credentials) { { 'host' => 'www.example.com' } } - - def stub_keygen_request(**options) - stub_request(:get, 'https://www.example.com/api/?password=password&type=keygen&user=user') - .to_return(options) - end - - def stub_api_request(**options) - stub_request(:get, 'https://www.example.com/api/?type=THETYPE&key=APIKEY&option_a=ANOPTION') - .to_return(options) - end - - def stub_upload_request(**options) - stub_request(:post, 'https://www.example.com/api/?key=APIKEY&type=THETYPE&category=CATEGORY') - .to_return(options) - # Error: "WebMock does not support matching body for multipart/form-data requests yet" - # .with(body: /filename=\"#{file_name}\".*#{Regexp.escape(file_content)}/m, - # headers: { - # 'Content-Type' => /multipart\/form-data/ - # }) - end - - describe '#fetch_apikey(user, password)' do - context 'with valid username and password' do - it 'fetches the API key' do - stub_keygen_request(status: 200, body: "SOMEKEY") - - expect(instance.fetch_apikey('user', 'password')).to eq 'SOMEKEY' - - expect(a_request(:get, 'https://www.example.com/api/?password=password&type=keygen&user=user')).to have_been_made.once - end - end - - context 'with invalid username and password' do - it 'raises a helpful error' do - stub_keygen_request(status: 403) - - expect { instance.fetch_apikey('user', 'password') }.to raise_error RuntimeError, %r{forbidden}i - - expect(a_request(:get, 'https://www.example.com/api/?password=password&type=keygen&user=user')).to have_been_made.once - end - end - end - - describe '#apikey' do - let(:credentials) { super().merge('user' => 'user', 'password' => 'password') } - - it 'makes only a single HTTP call' do - stub_keygen_request(status: 200, body: "SOMEKEY") - - expect(instance.apikey).to eq 'SOMEKEY' - expect(instance.apikey).to eq 'SOMEKEY' - - expect(a_request(:get, 'https://www.example.com/api/?password=password&type=keygen&user=user')).to have_been_made.once - end - end - - describe '#upload(file_name, file_content, **options)' do - let(:credentials) { super().merge('apikey' => 'APIKEY') } - let(:doc) { instance.upload('THETYPE', '/path/to/file/test.txt', category: 'CATEGORY') } - let(:file_content) { 'some config info' } - - before(:each) do - allow(File).to receive(:exist?).and_return(true) - allow(File).to receive(:open).and_return(file_content) - end - - context 'when the API returns success' do - before(:each) do - stub_upload_request(status: 200, body: "test.txt saved") - end - - it { - doc - expect(a_request(:post, 'https://www.example.com/api/?key=APIKEY&type=THETYPE&category=CATEGORY')).to have_been_made.once - } - it { expect(doc).to be_a REXML::Document } - it { expect(doc).to have_xml('response[@status="success"]') } - end - - context 'when the file provided does not exist' do - it do - allow(File).to receive(:exist?).and_return(false) - expect { doc }.to raise_error Puppet::ResourceError, 'File: `/path/to/file/test.txt` does not exist' - end - end - - context 'when the API returns an HTTP error' do - it do - stub_upload_request(status: 400, body: "TESTMESSAGE.") - - expect { doc }.to raise_error RuntimeError, %r{HTTPBadRequest} - end - end - context 'when the API returns a semantic error' do - it do - stub_upload_request(status: 200, body: "Malformed Request") - - expect { doc }.to raise_error Puppet::ResourceError, %r{Malformed Request} - end - end - end - - describe '#request(type, **options)' do - let(:credentials) { super().merge('apikey' => 'APIKEY') } - let(:doc) { instance.request('THETYPE', option_a: 'ANOPTION') } - - context 'when the API returns success' do - before(:each) do - stub_api_request(status: 200, body: "") - end - - it { - doc - expect(a_request(:get, 'https://www.example.com/api/?type=THETYPE&key=APIKEY&option_a=ANOPTION')).to have_been_made.once - } - it { expect(doc).to be_a REXML::Document } - it { expect(doc).to have_xml('response[@status="success"]') } - end - - context 'when the API returns an HTTP error' do - it do - stub_api_request(status: 400, body: "TESTMESSAGE.") - - expect { doc }.to raise_error RuntimeError, %r{HTTPBadRequest} - end - end - context 'when the API returns a semantic error' do - it do - stub_api_request(status: 200, body: "Malformed Request") - - expect { doc }.to raise_error Puppet::ResourceError, %r{Malformed Request} - end - end - end - - describe '#job_request(type, **options)' do - let(:credentials) { super().merge('apikey' => 'APIKEY') } - - before(:each) do - # disable sleeping, due to how this is called (objects inheriting from Kernel) this requires a lot of wrangling - # See https://stackoverflow.com/a/27749263/4918 - allow_any_instance_of(Object).to receive(:sleep) # rubocop:disable RSpec/AnyInstance - end - - # this part is a bit wonky, because "commit" is currently the only job we're using/testing - # other async jobs might never return this, or return something different - context 'when the job is not required' do - before(:each) do - stub_request(:get, 'https://www.example.com/api/?cmd=&key=APIKEY&type=commit') - .to_return(status: 200, body: 'There are no changes to commit.') - end - - it 'returns immediately' do - instance.job_request('commit', cmd: '') - end - end - - context 'straight to success' do - it do - stub_request(:get, 'https://www.example.com/api/?cmd=&key=APIKEY&type=commit') - .to_return(status: 200, body: 'Commit job enqueued with jobid 22') - # rubocop:disable Metrics/LineLength - stub_request(:get, 'https://www.example.com/api/?cmd=2&key=APIKEY&type=op') - .to_return(status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitFINNOnoOK04:36:570100
Configuration committed successfully
') - # rubocop:enable Metrics/LineLength - - instance.job_request('commit', cmd: '') - end - end - context 'waiting for it' do - it do - stub_request(:get, 'https://www.example.com/api/?cmd=&key=APIKEY&type=commit') - .to_return(status: 200, body: 'Commit job enqueued with jobid 22') - # rubocop:disable Metrics/LineLength - stub_request(:get, 'https://www.example.com/api/?cmd=2&key=APIKEY&type=op') - .to_return([ - { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOyesPENDStill Active00
' }, - { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOyesPENDStill Active00
' }, - { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOyesPENDStill Active075
' }, - { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOnoPENDStill Active099
' }, - { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitFINNOnoOK04:36:570100
Configuration committed successfully
' }, - ]) - # rubocop:enable Metrics/LineLength - - instance.job_request('commit', cmd: '') - end - end - - context 'when the job fails' do - it do - stub_request(:get, 'https://www.example.com/api/?cmd=&key=APIKEY&type=op') - .to_return(status: 200, body: 'Validate job enqueued with jobid 22') - # rubocop:disable Metrics/LineLength - stub_request(:get, 'https://www.example.com/api/?cmd=2&key=APIKEY&type=op') - .to_return([ - { status: 200, body: '2018/06/12 09:26:4509:26:458adminValidateACTNOyesPENDStill Active00
' }, - { status: 200, body: '2018/06/12 09:26:4509:26:458adminValidateACTNOyesPENDStill Active00
' }, - { status: 200, body: '2018/06/12 09:26:4509:26:458adminValidateFINNOnoFAIL09:26:470100
Validation Error: address-3 Node ip-netmask(line 30226) and ip-range(line 30227) are mutually exclusive]]> address-3 Node ip-netmask(line 30226) and fqdn(line 30228) are mutually exclusive]]>
' }, - ]) - # rubocop:enable Metrics/LineLength - - expect { instance.job_request('op', cmd: '') }.to raise_error Puppet::ResourceError, %r{Validation Error:} - end - end - end + it 'initialises correctly' do + expect(described_class.new(connection_info).transport).to be_instance_of(Puppet::Transport::Panos) end end diff --git a/tasks/apikey.rb b/tasks/apikey.rb index ccbe2d98..ad234742 100755 --- a/tasks/apikey.rb +++ b/tasks/apikey.rb @@ -17,8 +17,8 @@ #### the real task ### require 'json' -require 'puppet/util/network_device/panos/device' +require 'puppet/transport/panos' params = JSON.parse(ENV['PARAMS'] || STDIN.read) -device = Puppet::Util::NetworkDevice::Panos::Device.new(params) -puts JSON.generate(apikey: device.apikey) +transport = Puppet::ResourceApi::Transport::Panos.new(params) +puts JSON.generate(apikey: transport.apikey) diff --git a/tasks/commit.rb b/tasks/commit.rb index 7fd839e0..b3d24ae0 100755 --- a/tasks/commit.rb +++ b/tasks/commit.rb @@ -17,11 +17,11 @@ #### the real task ### require 'json' -require 'puppet/util/network_device/panos/device' +require 'puppet/transport/panos' params = JSON.parse(ENV['PARAMS'] || STDIN.read) -device = Puppet::Util::NetworkDevice::Panos::Device.new(params['credentials_file']) +transport = Puppet::ResourceApi::Transport::Panos.new(params['credentials_file']) -if device.outstanding_changes? - device.commit +if transport.outstanding_changes? + transport.commit end diff --git a/tasks/set_config.rb b/tasks/set_config.rb index 51018dea..17d1d9bd 100755 --- a/tasks/set_config.rb +++ b/tasks/set_config.rb @@ -17,13 +17,13 @@ #### the real task ### require 'json' -require 'puppet/util/network_device/panos/device' +require 'puppet/transport/panos' params = JSON.parse(ENV['PARAMS'] || STDIN.read) -device = Puppet::Util::NetworkDevice::Panos::Device.new(params['credentials_file']) +transport = Puppet::ResourceApi::Transport::Panos.new(params['credentials_file']) file = params['config_file'] -device.import(file, 'configuration') +transport.import(file, 'configuration') if params['apply'] - device.load_config(File.basename(file)) + transport.load_config(File.basename(file)) end diff --git a/tasks/store_config.rb b/tasks/store_config.rb index a817eb04..1980d69b 100755 --- a/tasks/store_config.rb +++ b/tasks/store_config.rb @@ -17,13 +17,13 @@ #### the real task ### require 'json' -require 'puppet/util/network_device/panos/device' +require 'puppet/transport/panos' params = JSON.parse(ENV['PARAMS'] || STDIN.read) -device = Puppet::Util::NetworkDevice::Panos::Device.new(params['credentials_file']) +transport = Puppet::ResourceApi::Transport::Panos.new(params['credentials_file']) file_name = params['config_file'] -config = device.show_config +config = transport.show_config config.elements.collect('/response/result/config') do |entry| # rubocop:disable Style/CollectionMethods config = entry