diff --git a/.gitignore b/.gitignore
index ca782544..a46ff252 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,9 @@
/convert_report.txt
/update_report.txt
.DS_Store
+.project
+.vscode/
+.envrc
spec/fixtures/acceptance-credentials.conf
spec/fixtures/acceptance-device.conf
spec/fixtures/config-acceptance.xml
diff --git a/.pdkignore b/.pdkignore
index 44692851..fe4861a5 100644
--- a/.pdkignore
+++ b/.pdkignore
@@ -22,6 +22,9 @@
/convert_report.txt
/update_report.txt
.DS_Store
+.project
+.vscode/
+.envrc
/appveyor.yml
/.fixtures.yml
/Gemfile
diff --git a/.project b/.project
deleted file mode 100644
index 9823ba40..00000000
--- a/.project
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
- puppetlabs-panos
-
-
-
-
-
- com.puppetlabs.geppetto.pp.dsl.ui.modulefileBuilder
-
-
-
-
- org.eclipse.xtext.ui.shared.xtextBuilder
-
-
-
-
-
- com.puppetlabs.geppetto.pp.dsl.ui.puppetNature
- org.eclipse.xtext.ui.shared.xtextNature
-
-
diff --git a/.puppet-lint.rc b/.puppet-lint.rc
index e69de29b..cc96ece0 100644
--- a/.puppet-lint.rc
+++ b/.puppet-lint.rc
@@ -0,0 +1 @@
+--relative
diff --git a/.sync.yml b/.sync.yml
index 49c7e812..862bc746 100644
--- a/.sync.yml
+++ b/.sync.yml
@@ -9,8 +9,9 @@ Gemfile:
git: 'https://github.com/puppetlabs/puppet-strings.git'
ref: 'master'
- gem: 'puppet-resource_api'
- git: 'https://github.com/puppetlabs/puppet-resource_api.git'
- ref: 'master'
+ version: '>= 1.8.1'
+ # git: 'https://github.com/puppetlabs/puppet-resource_api.git'
+ # ref: 'master'
# required for internal pipelines
- gem: 'beaker-hostgenerator'
# the first version to contain Palo Alto support
@@ -46,4 +47,4 @@ spec/spec_helper.rb:
appveyor.yml:
delete: true
.gitlab-ci.yml:
- delete: true
\ No newline at end of file
+ delete: true
diff --git a/.travis.yml b/.travis.yml
index 431a5987..f282d897 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,9 +26,6 @@ matrix:
-
env: PUPPET_GEM_VERSION="~> 5.0" CHECK=parallel_spec
rvm: 2.4.4
- -
- env: PUPPET_GEM_VERSION="~> 4.0" CHECK=parallel_spec RUBYGEMS_VERSION=2.7.8
- rvm: 2.1.9
branches:
only:
- master
diff --git a/Gemfile b/Gemfile
index 07618086..01effa83 100644
--- a/Gemfile
+++ b/Gemfile
@@ -21,7 +21,8 @@ group :development do
gem "fast_gettext", require: false if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.1.0')
gem "json_pure", '<= 2.0.1', require: false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.0.0')
gem "json", '= 1.8.1', require: false if Gem::Version.new(RUBY_VERSION.dup) == Gem::Version.new('2.1.9')
- gem "json", '<= 2.0.4', require: false if Gem::Version.new(RUBY_VERSION.dup) == Gem::Version.new('2.4.4')
+ gem "json", '= 2.0.4', require: false if Gem::Requirement.create('~> 2.4.2').satisfied_by?(Gem::Version.new(RUBY_VERSION.dup))
+ gem "json", '= 2.1.0', require: false if Gem::Requirement.create(['>= 2.5.0', '< 2.7.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup))
gem "puppet-module-posix-default-r#{minor_version}", require: false, platforms: [:ruby]
gem "puppet-module-posix-dev-r#{minor_version}", require: false, platforms: [:ruby]
gem "puppet-module-win-default-r#{minor_version}", require: false, platforms: [:mswin, :mingw, :x64_mingw]
@@ -29,7 +30,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", '>= 1.8.1', require: false
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/README.md b/README.md
index c5721957..4601f220 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
The PANOS module configures Palo Alto firewalls running PANOS 7.1.0 or PANOS 8.1.0.
-When committing changes to resources, include `panos_commit` in your manifest, or execute the `commit` task. You must do this before they can be made available to the running configuration.
+When committing changes to resources, include `panos_commit` in your manifest, or execute the `commit` task. You must do this before they can be made available to the running configuration.
The module provides a Puppet task to manually `commit`, `store_config` to a file, and `set_config` from a file.
@@ -70,13 +70,27 @@ __Note:__ v0.1.0 requires `host` instead of `address`
__Note:__ v0.1.0 requires `user` instead of `username`
-To obtain an API key for the device, it is possible to use the `panos::apikey` task. The required creditials file should be in the format of (a) above. After which you can discard it. Before running this task, install the module on your machine, along with [Puppet Bolt](https://puppet.com/docs/bolt/0.x/bolt_installing.html). When complete, execute the following command:
+To obtain an API key for the device, it is possible to use the `panos::apikey` task. Before running this task, install the module on your machine, along with [Puppet Bolt](https://puppet.com/docs/bolt/latest/bolt_installing.html). When complete, execute the following command:
```
-bolt task run panos::apikey --nodes localhost --transport local --modulepath --params @credentials.json
+bolt task run panos::apikey --nodes pan --modulepath --inventoryfile
```
-The `--modulepath` param can be retrieved by typing `puppet config print modulepath`. The credentials file needs to be valid JSON containing host, username and password for the Palo Alto firewall.
+The following [inventory file](https://puppet.com/docs/bolt/latest/inventory_file.html) can be used to connect to your firewall.
+```yaml
+# inventory.yaml
+nodes:
+ - name: firewall.example.com
+ alias: pan
+ config:
+ transport: remote
+ remote:
+ remote-transport: panos
+ user: admin
+ password: admin
+```
+
+The `--modulepath` param can be retrieved by typing `puppet config print modulepath`.
Test your setup and get the certificate signed:
diff --git a/REFERENCE.md b/REFERENCE.md
index 00af7160..c2ab6831 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -29,7 +29,7 @@
**Tasks**
-* [`apikey`](#apikey): Retrieve a PAN-OS apikey using PAN-OS host, username and password.
+* [`apikey`](#apikey): Retrieve a PAN-OS apikey
* [`commit`](#commit): Commit a candidate configuration to a firewall.
* [`set_config`](#set_config): upload and/or apply a configuration to a firewall.
* [`store_config`](#store_config): Retrieve the configuration running on the firewall.
@@ -1711,44 +1711,16 @@ The display-name of the zone.
### apikey
-Retrieve a PAN-OS apikey using PAN-OS host, username and password.
+Retrieve a PAN-OS apikey
**Supports noop?** false
-#### Parameters
-
-##### `host`
-
-Data type: `String`
-
-The host to connect to
-
-##### `user`
-
-Data type: `String`
-
-The user name
-
-##### `password`
-
-Data type: `String`
-
-The password
-
### commit
Commit a candidate configuration to a firewall.
**Supports noop?** false
-#### Parameters
-
-##### `credentials_file`
-
-Data type: `String`
-
-The filename of the credentials file (as referenced in device.conf)
-
### set_config
upload and/or apply a configuration to a firewall.
@@ -1757,12 +1729,6 @@ upload and/or apply a configuration to a firewall.
#### Parameters
-##### `credentials_file`
-
-Data type: `String`
-
-The filename of the credentials file (as referenced in device.conf)
-
##### `config_file`
Data type: `String`
diff --git a/Rakefile b/Rakefile
index fd66fff3..b3222ca3 100644
--- a/Rakefile
+++ b/Rakefile
@@ -23,7 +23,7 @@ end
def changelog_future_release
return unless Rake.application.top_level_tasks.include? "changelog"
- returnVal = JSON.load(File.read('metadata.json'))['version']
+ returnVal = "v%s" % JSON.load(File.read('metadata.json'))['version']
raise "unable to find the future_release (version) in metadata.json" if returnVal.nil?
puts "GitHubChangelogGenerator future_release:#{returnVal}"
returnVal
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..701d4ad9
--- /dev/null
+++ b/lib/puppet/transport/panos.rb
@@ -0,0 +1,291 @@
+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 "user"/"password" or "apikey" in the configuration' unless (connection_info.key?(:user) && 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
+
+ def apikey
+ api.apikey
+ 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].unwrap unless connection_info[:password].nil?
+ @apikey = connection_info[:apikey].unwrap unless connection_info[:apikey].nil?
+ 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..a22be7e0
--- /dev/null
+++ b/lib/puppet/transport/schema/panos.rb
@@ -0,0 +1,38 @@
+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: {
+ host: {
+ 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.',
+ },
+ user: {
+ 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]',
+ sensitive: true,
+ 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]',
+ sensitive: true,
+ desc: <<-EOS,
+The API key to use for authenticating all connections to the firewall.
+Only one of `user`/`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/lib/puppet/util/task_helper.rb b/lib/puppet/util/task_helper.rb
new file mode 100644
index 00000000..7335eb3d
--- /dev/null
+++ b/lib/puppet/util/task_helper.rb
@@ -0,0 +1,40 @@
+require 'puppet'
+require 'json'
+
+# Sets up the transport for a remote task
+class Puppet::Util::TaskHelper
+ def initialize(transport_name)
+ @transport_name = transport_name
+ @transport = {}
+
+ return unless params.key? '_installdir'
+ add_plugin_paths(params['_installdir'])
+ end
+
+ def transport
+ require 'puppet/resource_api/transport'
+
+ @transport[@transport_name] ||= Puppet::ResourceApi::Transport.connect(@transport_name, credentials)
+ end
+
+ def params
+ @params ||= JSON.parse(ENV['PARAMS'] || STDIN.read)
+ end
+
+ def target
+ @target ||= params['_target']
+ end
+
+ def credentials
+ @credentials ||= target.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
+ end
+
+ private
+
+ # Syncs across anything from the module lib
+ def add_plugin_paths(install_dir)
+ Dir.glob(File.join([install_dir, '*'])).each do |mod|
+ $LOAD_PATH << File.join([mod, 'lib'])
+ end
+ end
+end
diff --git a/metadata.json b/metadata.json
index a78072eb..292e3e92 100644
--- a/metadata.json
+++ b/metadata.json
@@ -28,7 +28,7 @@
"version_requirement": ">= 4.7.0 < 7.0.0"
}
],
- "pdk-version": "1.8.0",
+ "pdk-version": "1.9.0",
"template-url": "https://github.com/puppetlabs/pdk-templates/",
- "template-ref": "heads/master-0-g20af4c6"
+ "template-ref": "heads/master-0-gfde5699"
}
\ No newline at end of file
diff --git a/spec/acceptance/basic_spec.rb b/spec/acceptance/basic_spec.rb
index 0712d4aa..90d7ad96 100644
--- a/spec/acceptance/basic_spec.rb
+++ b/spec/acceptance/basic_spec.rb
@@ -25,13 +25,19 @@
context 'when it gets the current running config' do
it 'will get the current running config and store to file' do
params = {
- 'credentials_file' => "file://#{Dir.getwd}/spec/fixtures/acceptance-credentials.conf",
+ '_target' => {
+ 'host' => RSpec.configuration.host,
+ 'user' => RSpec.configuration.user,
+ 'password' => RSpec.configuration.password,
+ },
'config_file' => 'spec/fixtures/config-acceptance.xml',
}
ENV['PARAMS'] = JSON.generate(params)
puts "Executing store_config.rb task with `#{ENV['PARAMS']}`" if debug_output?
- Open3.capture2e('bundle exec ruby -Ilib tasks/store_config.rb')
+ result = Open3.capture2e('bundle exec ruby -Ilib tasks/store_config.rb')
+ expect(result[0]).not_to match(%r{_error})
+ expect(File).to be_exist(params['config_file'])
end
context 'when running an idempotency check' do
@@ -68,13 +74,18 @@
context 'when it gets the current running config' do
it 'will get the current running config and store to file' do
params = {
- 'credentials_file' => "file://#{Dir.getwd}/spec/fixtures/acceptance-credentials.conf",
+ '_target' => {
+ 'host' => RSpec.configuration.host,
+ 'user' => RSpec.configuration.user,
+ 'password' => RSpec.configuration.password,
+ },
'config_file' => 'spec/fixtures/config-reset.xml',
}
ENV['PARAMS'] = JSON.generate(params)
puts "Executing store_config.rb task with `#{ENV['PARAMS']}`" if debug_output?
Open3.capture2e('bundle exec ruby -Ilib tasks/store_config.rb')
+ expect(File).to be_exist(params['config_file'])
end
end
end
diff --git a/spec/acceptance/tasks/config_spec.rb b/spec/acceptance/tasks/config_spec.rb
index 1ec97067..3c3ed020 100644
--- a/spec/acceptance/tasks/config_spec.rb
+++ b/spec/acceptance/tasks/config_spec.rb
@@ -4,7 +4,11 @@
describe 'Config task' do
before(:each) do
params = {
- 'credentials_file' => "file://#{Dir.getwd}/spec/fixtures/acceptance-credentials.conf",
+ '_target' => {
+ 'host' => RSpec.configuration.host,
+ 'user' => RSpec.configuration.user,
+ 'password' => RSpec.configuration.password,
+ },
'config_file' => config,
'apply' => apply,
}
@@ -21,7 +25,11 @@
after(:all) do
params = {
- 'credentials_file' => "file://#{Dir.getwd}/spec/fixtures/acceptance-credentials.conf",
+ '_target' => {
+ 'host' => RSpec.configuration.host,
+ 'user' => RSpec.configuration.user,
+ 'password' => RSpec.configuration.password,
+ },
'config_file' => 'spec/fixtures/config-reset.xml',
'apply' => true,
}
@@ -35,8 +43,8 @@
let(:apply) { false }
it 'will upload the configuration file but not load it' do
- expect(stdout_str).not_to match %r{Loading Config}
- expect(stdout_str).to match %r{Importing configuration}
+ expect(stdout_str).to match %r{\{\}}
+ expect(stdout_str).not_to match %r{_error}
puts stdout_str if debug_output?
expect(status.exitstatus).to eq 0
end
@@ -46,8 +54,8 @@
let(:apply) { true }
it 'will upload the configuration file and load it' do
- expect(stdout_str).to match %r{Loading Config}
- expect(stdout_str).to match %r{Importing configuration}
+ expect(stdout_str).to match %r{\{\}}
+ expect(stdout_str).not_to match %r{_error}
puts stdout_str if debug_output?
expect(status.exitstatus).to eq 0
end
@@ -74,8 +82,8 @@
let(:apply) { true }
it 'will upload the configuration file and load it' do
- expect(stdout_str).to match %r{Loading Config}
- expect(stdout_str).to match %r{Importing configuration}
+ expect(stdout_str).to match %r{\{\}}
+ expect(stdout_str).not_to match %r{_error}
puts stdout_str if debug_output?
expect(status.exitstatus).to eq 0
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index d8661e37..92fecd42 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -23,7 +23,7 @@
next unless File.exist?(f) && File.readable?(f) && File.size?(f)
begin
- default_facts.merge!(YAML.safe_load(File.read(f)))
+ default_facts.merge!(YAML.safe_load(File.read(f), [], [], true))
rescue => e
RSpec.configuration.reporter.message "WARNING: Unable to load #{f}: #{e}"
end
@@ -36,6 +36,9 @@
# by default Puppet runs at warning level
Puppet.settings[:strict] = :warning
end
+ c.filter_run_excluding(bolt: true) unless ENV['GEM_BOLT']
+ c.after(:suite) do
+ end
end
def ensure_module_defined(module_name)
diff --git a/spec/spec_helper_acceptance.rb b/spec/spec_helper_acceptance.rb
index 9fce6ef3..0861daa6 100644
--- a/spec/spec_helper_acceptance.rb
+++ b/spec/spec_helper_acceptance.rb
@@ -1,3 +1,4 @@
+require 'fileutils'
require 'json'
require 'net/http'
require 'open3'
@@ -45,11 +46,15 @@ def debug_output?
puts "Detected #{@platform} config for #{@hostname}"
+ c.add_setting :host, default: @hostname
+ c.add_setting :user, default: (ENV['PANOS_TEST_USER'] || 'admin')
+ c.add_setting :password, default: (ENV['PANOS_TEST_PASSWORD'] || 'admin')
+
File.open('spec/fixtures/acceptance-credentials.conf', 'w') do |file|
file.puts < { 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<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<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..e3efa552
--- /dev/null
+++ b/spec/unit/puppet/transport/panos_spec.rb
@@ -0,0 +1,410 @@
+require 'spec_helper'
+require 'puppet/transport/panos'
+require 'puppet/resource_api'
+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(:pass) { Puppet::Pops::Types::PSensitiveType::Sensitive.new('password') }
+ let(:apikey) { Puppet::Pops::Types::PSensitiveType::Sensitive.new('APIKEY') }
+ let(:connection_info) { { host: 'www.example.com', user: 'admin', password: pass } }
+ 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 host is not provided' do
+ let(:connection_info) { { user: 'admin', password: 'password' } }
+
+ it { expect { transport }.to raise_error Puppet::ResourceError, %r{The following mandatory attributes were not provided:.*host}m }
+ end
+ context 'when port is provided but not valid' do
+ let(:connection_info) { { host: 'www.example.com', port: 'foo', user: '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
+ [
+ { host: 'www.example.com', user: 'admin' },
+ { host: 'www.example.com', password: 'password' },
+ { host: 'www.example.com' },
+ ].each do |config|
+ let(:connection_info) { config }
+
+ it { expect { transport }.to raise_error Puppet::ResourceError, 'Could not find "user"/"password" or "apikey" in the configuration' }
+ end
+ end
+ context 'when apikey is provided' do
+ let(:connection_info) { { host: 'www.example.com', apikey: 'APIKEY' } }
+
+ it { expect { transport }.not_to raise_error Puppet::ResourceError }
+ end
+ context 'when correct credentials are provided' do
+ let(:connection_info) { { host: 'www.example.com', user: '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
+
+ describe '#apikey' do
+ it 'calls the api correctly' do
+ expect(api).to receive(:apikey)
+ transport.apikey
+ 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(:pass) { Puppet::Pops::Types::PSensitiveType::Sensitive.new('password') }
+ let(:apikey) { Puppet::Pops::Types::PSensitiveType::Sensitive.new('APIKEY') }
+
+ 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 user 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 user 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: pass) }
+
+ 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:570Configuration 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 Active0 ' },
+ { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOyesPENDStill Active0 ' },
+ { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOyesPENDStill Active0 ' },
+ { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOnoPENDStill Active0 ' },
+ { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitFINNOnoOK04:36:570Configuration 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 Active0 ' },
+ { status: 200, body: '2018/06/12 09:26:4509:26:458adminValidateACTNOyesPENDStill Active0 ' },
+ { status: 200, body: '2018/06/12 09:26:4509:26:458adminValidateFINNOnoFAIL09:26:470Validation 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..e1129aa9 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) { { host: 'www.example.com', user: '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:570Configuration 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 Active0 ' },
- { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOyesPENDStill Active0 ' },
- { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOyesPENDStill Active0 ' },
- { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitACTNOnoPENDStill Active0 ' },
- { status: 200, body: '2018/06/12 04:36:4504:36:452adminCommitFINNOnoOK04:36:570Configuration 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 Active0 ' },
- { status: 200, body: '2018/06/12 09:26:4509:26:458adminValidateACTNOyesPENDStill Active0 ' },
- { status: 200, body: '2018/06/12 09:26:4509:26:458adminValidateFINNOnoFAIL09:26:470Validation 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/spec/unit/puppet/util/task_helper_spec.rb b/spec/unit/puppet/util/task_helper_spec.rb
new file mode 100644
index 00000000..a669748e
--- /dev/null
+++ b/spec/unit/puppet/util/task_helper_spec.rb
@@ -0,0 +1,110 @@
+require 'json'
+require 'puppet/util/task_helper'
+require 'puppet/resource_api/transport'
+
+RSpec.describe Puppet::Util::TaskHelper do
+ let(:helper) { described_class.new('panos') }
+ let(:params) { {} }
+
+ before(:each) do
+ allow(STDIN).to receive(:read).and_return(JSON.generate(params))
+ end
+
+ it 'does not throw' do
+ expect { helper }.not_to raise_error
+ end
+
+ context 'when `_installdir` is present' do
+ let(:params) do
+ {
+ '_installdir' => '.',
+ }
+ end
+
+ it 'does not throw' do
+ expect { helper }.not_to raise_error
+ end
+ end
+
+ describe '#transport' do
+ let(:creds) do
+ {
+ host: '1.2.3.4',
+ user: 'admin',
+ password: 'admin',
+ }
+ end
+ let(:params) do
+ {
+ '_target' => creds,
+ }
+ end
+
+ it 'returns a transport object' do
+ expect(Puppet::ResourceApi::Transport).to receive(:connect).with('panos', creds)
+ helper.transport
+ end
+ end
+
+ describe '#params' do
+ context 'when params are provided through STDIN' do
+ let(:params) do
+ {
+ 'foo' => 'wibble',
+ }
+ end
+
+ it { expect(helper.params).to eq('foo' => 'wibble') }
+ end
+ context 'when params are provided through ENV' do
+ let(:env) { '{"wibble": "foo"}' }
+
+ it {
+ allow(ENV).to receive(:[]).with('PARAMS').and_return(env)
+ expect(helper.params).to eq('wibble' => 'foo')
+ }
+ end
+ end
+
+ describe '#target' do
+ let(:creds) do
+ {
+ host: '1.2.3.4',
+ user: 'admin',
+ password: 'admin',
+ }
+ end
+ let(:params) do
+ {
+ '_target' => creds,
+ }
+ end
+
+ it 'returns the creds with string keys' do
+ expect(helper.target).to eq('host' => '1.2.3.4',
+ 'password' => 'admin',
+ 'user' => 'admin')
+ end
+ end
+
+ describe '#credentials' do
+ let(:creds) do
+ {
+ host: '1.2.3.4',
+ user: 'admin',
+ password: 'admin',
+ }
+ end
+ let(:params) do
+ {
+ '_target' => creds,
+ }
+ end
+
+ it 'returns the creds with symbolised keys' do
+ expect(helper.credentials).to eq(host: '1.2.3.4',
+ password: 'admin',
+ user: 'admin')
+ end
+ end
+end
diff --git a/tasks/apikey.json b/tasks/apikey.json
index dc0dc702..c7c67f8f 100644
--- a/tasks/apikey.json
+++ b/tasks/apikey.json
@@ -1,19 +1,13 @@
{
"puppet_task_version": 1,
"supports_noop": false,
- "description": "Retrieve a PAN-OS apikey using PAN-OS host, username and password.",
+ "remote": true,
+ "description": "Retrieve a PAN-OS apikey",
"parameters": {
- "host": {
- "description": "The host to connect to",
- "type": "String"
- },
- "user": {
- "description": "The user name",
- "type": "String"
- },
- "password": {
- "description": "The password",
- "type": "String"
- }
- }
+ },
+ "files": [
+ "panos/lib/puppet/util/task_helper.rb",
+ "panos/lib/puppet/transport/panos.rb",
+ "panos/lib/puppet/transport/schema/panos.rb"
+ ]
}
diff --git a/tasks/apikey.rb b/tasks/apikey.rb
index ccbe2d98..583f516e 100755
--- a/tasks/apikey.rb
+++ b/tasks/apikey.rb
@@ -1,24 +1,15 @@
#!/opt/puppetlabs/puppet/bin/ruby
-# work around the fact that bolt (for now, see BOLT-132) is not able to transport additional code from the module
-# this requires that the panos module is pluginsynced to the node executing the task
-require 'puppet'
-Puppet.settings.initialize_app_defaults(
- Puppet::Settings.app_defaults_for_run_mode(
- Puppet::Util::RunMode[:agent],
- ),
-)
-$LOAD_PATH.unshift(Puppet[:plugindest])
-
-# setup logging to stdout/stderr which will be available to task executors
-Puppet::Util::Log.newdestination(:console)
-Puppet[:log_level] = 'debug'
-
-#### the real task ###
-
-require 'json'
-require 'puppet/util/network_device/panos/device'
-
-params = JSON.parse(ENV['PARAMS'] || STDIN.read)
-device = Puppet::Util::NetworkDevice::Panos::Device.new(params)
-puts JSON.generate(apikey: device.apikey)
+require_relative '../lib/puppet/util/task_helper'
+task = Puppet::Util::TaskHelper.new('panos')
+result = {}
+
+begin
+ result['apikey'] = task.transport.apikey
+rescue Exception => e # rubocop:disable Lint/RescueException
+ result[:_error] = { msg: e.message,
+ kind: 'puppetlabs-panos/unknown',
+ details: { class: e.class.to_s } }
+end
+
+puts result.to_json
diff --git a/tasks/commit.json b/tasks/commit.json
index cd011f4c..6ce38077 100644
--- a/tasks/commit.json
+++ b/tasks/commit.json
@@ -1,11 +1,13 @@
{
"puppet_task_version": 1,
+ "remote": true,
"supports_noop": false,
"description": "Commit a candidate configuration to a firewall.",
"parameters": {
- "credentials_file": {
- "description": "The filename of the credentials file (as referenced in device.conf)",
- "type": "String"
- }
- }
+ },
+ "files": [
+ "panos/lib/puppet/util/task_helper.rb",
+ "panos/lib/puppet/transport/panos.rb",
+ "panos/lib/puppet/transport/schema/panos.rb"
+ ]
}
diff --git a/tasks/commit.rb b/tasks/commit.rb
index 7fd839e0..8079140e 100755
--- a/tasks/commit.rb
+++ b/tasks/commit.rb
@@ -1,27 +1,17 @@
#!/opt/puppetlabs/puppet/bin/ruby
-# work around the fact that bolt (for now, see BOLT-132) is not able to transport additional code from the module
-# this requires that the panos module is pluginsynced to the node executing the task
-require 'puppet'
-Puppet.settings.initialize_app_defaults(
- Puppet::Settings.app_defaults_for_run_mode(
- Puppet::Util::RunMode[:agent],
- ),
-)
-$LOAD_PATH.unshift(Puppet[:plugindest])
+require_relative '../lib/puppet/util/task_helper'
+task = Puppet::Util::TaskHelper.new('panos')
+result = {}
-# setup logging to stdout/stderr which will be available to task executors
-Puppet::Util::Log.newdestination(:console)
-Puppet[:log_level] = 'debug'
-
-#### the real task ###
-
-require 'json'
-require 'puppet/util/network_device/panos/device'
-
-params = JSON.parse(ENV['PARAMS'] || STDIN.read)
-device = Puppet::Util::NetworkDevice::Panos::Device.new(params['credentials_file'])
-
-if device.outstanding_changes?
- device.commit
+begin
+ if task.transport.outstanding_changes?
+ task.transport.commit
+ end
+rescue Exception => e # rubocop:disable Lint/RescueException
+ result[:_error] = { msg: e.message,
+ kind: 'puppetlabs-panos/unknown',
+ details: { class: e.class.to_s } }
end
+
+puts result.to_json
diff --git a/tasks/set_config.json b/tasks/set_config.json
index 60201ce8..1d02f850 100644
--- a/tasks/set_config.json
+++ b/tasks/set_config.json
@@ -1,12 +1,9 @@
{
"puppet_task_version": 1,
"supports_noop": false,
+ "remote": true,
"description": "upload and/or apply a configuration to a firewall.",
"parameters": {
- "credentials_file": {
- "description": "The filename of the credentials file (as referenced in device.conf)",
- "type": "String"
- },
"config_file": {
"description": "The filename of the configuration file to upload",
"type": "String"
@@ -15,5 +12,10 @@
"description": "true: upload and immediately apply the config. false: upload the config, without applying",
"type": "Boolean"
}
- }
+ },
+ "files": [
+ "panos/lib/puppet/util/task_helper.rb",
+ "panos/lib/puppet/transport/panos.rb",
+ "panos/lib/puppet/transport/schema/panos.rb"
+ ]
}
diff --git a/tasks/set_config.rb b/tasks/set_config.rb
index 51018dea..fb2bc127 100755
--- a/tasks/set_config.rb
+++ b/tasks/set_config.rb
@@ -1,29 +1,20 @@
#!/opt/puppetlabs/puppet/bin/ruby
-# work around the fact that bolt (for now, see BOLT-132) is not able to transport additional code from the module
-# this requires that the panos module is pluginsynced to the node executing the task
-require 'puppet'
-Puppet.settings.initialize_app_defaults(
- Puppet::Settings.app_defaults_for_run_mode(
- Puppet::Util::RunMode[:agent],
- ),
-)
-$LOAD_PATH.unshift(Puppet[:plugindest])
+require_relative '../lib/puppet/util/task_helper'
+task = Puppet::Util::TaskHelper.new('panos')
+result = {}
-# setup logging to stdout/stderr which will be available to task executors
-Puppet::Util::Log.newdestination(:console)
-Puppet[:log_level] = 'debug'
+begin
+ file = task.params['config_file']
+ task.transport.import(file, 'configuration')
-#### the real task ###
-
-require 'json'
-require 'puppet/util/network_device/panos/device'
-
-params = JSON.parse(ENV['PARAMS'] || STDIN.read)
-device = Puppet::Util::NetworkDevice::Panos::Device.new(params['credentials_file'])
-
-file = params['config_file']
-device.import(file, 'configuration')
-if params['apply']
- device.load_config(File.basename(file))
+ if task.params['apply']
+ task.transport.load_config(File.basename(file))
+ end
+rescue Exception => e # rubocop:disable Lint/RescueException
+ result[:_error] = { msg: e.message,
+ kind: 'puppetlabs-panos/unknown',
+ details: { class: e.class.to_s } }
end
+
+puts result.to_json
diff --git a/tasks/store_config.json b/tasks/store_config.json
index accc4f10..79a82f63 100644
--- a/tasks/store_config.json
+++ b/tasks/store_config.json
@@ -3,10 +3,6 @@
"supports_noop": false,
"description": "Retrieve the configuration running on the firewall.",
"parameters": {
- "credentials_file": {
- "description": "The filename of the credentials file (as referenced in device.conf)",
- "type": "String"
- },
"config_file": {
"description": "The filename to save the configuration too",
"type": "String"
diff --git a/tasks/store_config.rb b/tasks/store_config.rb
index a817eb04..ba3a37d6 100755
--- a/tasks/store_config.rb
+++ b/tasks/store_config.rb
@@ -1,32 +1,23 @@
#!/opt/puppetlabs/puppet/bin/ruby
-# work around the fact that bolt (for now, see BOLT-132) is not able to transport additional code from the module
-# this requires that the panos module is pluginsynced to the node executing the task
-require 'puppet'
-Puppet.settings.initialize_app_defaults(
- Puppet::Settings.app_defaults_for_run_mode(
- Puppet::Util::RunMode[:agent],
- ),
-)
-$LOAD_PATH.unshift(Puppet[:plugindest])
-
-# setup logging to stdout/stderr which will be available to task executors
-Puppet::Util::Log.newdestination(:console)
-Puppet[:log_level] = 'debug'
-
-#### the real task ###
-
-require 'json'
-require 'puppet/util/network_device/panos/device'
-
-params = JSON.parse(ENV['PARAMS'] || STDIN.read)
-device = Puppet::Util::NetworkDevice::Panos::Device.new(params['credentials_file'])
-
-file_name = params['config_file']
-config = device.show_config
-
-config.elements.collect('/response/result/config') do |entry| # rubocop:disable Style/CollectionMethods
- config = entry
+require_relative '../lib/puppet/util/task_helper'
+task = Puppet::Util::TaskHelper.new('panos')
+result = {}
+
+begin
+ file_name = task.params['config_file']
+ config = task.transport.show_config
+
+ config.elements.collect('/response/result/config') do |entry| # rubocop:disable Style/CollectionMethods
+ config = entry
+ end
+
+ File.open(file_name, 'w+') { |file| file.write(config) }
+rescue Exception => e # rubocop:disable Lint/RescueException
+ result[:_error] = { msg: e.message,
+ kind: 'puppetlabs-panos/unknown',
+ details: { class: e.class.to_s },
+ backtrace: e.backtrace }
end
-File.open(file_name, 'w+') { |file| file.write(config) }
+puts result.to_json