Skip to content

Commit

Permalink
(PDK-1185) Implement allowances for device-specific providers
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidS committed Sep 25, 2018
1 parent 1b08a12 commit ee642a4
Show file tree
Hide file tree
Showing 12 changed files with 334 additions and 5 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ The provider needs to specify the `remote_resource` feature to enable the second

After this, `puppet device` will be able to use the new provider, and supply it (through the device class) with the URL specified in the [`device.conf`](https://puppet.com/docs/puppet/5.3/config_file_device.html).

#### Device-specific providers

To allow modules to deal with different backends independently of each other, the Resource API also implements a mechanism to use different API providers side-by-side. For a given device type (see above), the Resource API will first try to load a `Puppet::Provider::TypeName::DeviceType` class from `lib/puppet/provider/type_name/device_type.rb`, before falling back to the regular provider at `Puppet::Provider::TypeName::TypeName`.

### Further Reading

The [Resource API](https://github.com/puppetlabs/puppet-specifications/blob/master/language/resource-api/README.md) describes details of all the capabilities of this gem.
Expand Down
44 changes: 40 additions & 4 deletions lib/puppet/resource_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'puppet/resource_api/type_definition'
require 'puppet/resource_api/version'
require 'puppet/type'
require 'puppet/util/network_device'

module Puppet::ResourceApi
@warning_count = 0
Expand Down Expand Up @@ -54,7 +55,8 @@ def register_type(definition)

# Keeps a copy of the provider around. Weird naming to avoid clashes with puppet's own `provider` member
define_singleton_method(:my_provider) do
@my_provider ||= Puppet::ResourceApi.load_provider(definition[:name]).new
@my_provider ||= Hash.new { |hash, key| hash[key] = Puppet::ResourceApi.load_provider(definition[:name]).new }
@my_provider[Puppet::Util::NetworkDevice.current.class]
end

# make the provider available in the instance's namespace
Expand Down Expand Up @@ -514,14 +516,48 @@ def self.parse_title_patterns(patterns)
def load_provider(type_name)
class_name = class_name_from_type_name(type_name)
type_name_sym = type_name.to_sym
device_name = if Puppet::Util::NetworkDevice.current.nil?
nil
else
# extract the device type from the currently loaded device's class
Puppet::Util::NetworkDevice.current.class.name.split('::')[-2].downcase
end
device_class_name = class_name_from_type_name(device_name)

if device_name
device_name_sym = device_name.to_sym if device_name
load_device_provider(class_name, type_name_sym, device_class_name, device_name_sym)
else
load_default_provider(class_name, type_name_sym)
end
rescue NameError
if device_name # line too long # rubocop:disable Style/GuardClause
raise Puppet::DevError, "Found neither the device-specific provider class Puppet::Provider::#{class_name}::#{device_class_name} in puppet/provider/#{type_name}/#{device_name}"\
" nor the generic provider class Puppet::Provider::#{class_name}::#{class_name} in puppet/provider/#{type_name}/#{type_name}"
else
raise Puppet::DevError, "provider class Puppet::Provider::#{class_name}::#{class_name} not found in puppet/provider/#{type_name}/#{type_name}"
end
end
module_function :load_provider # rubocop:disable Style/AccessModifierDeclarations

def load_default_provider(class_name, type_name_sym)
# loads the "puppet/provider/#{type_name}/#{type_name}" file through puppet
Puppet::Type.type(type_name_sym).provider(type_name_sym)
Puppet::Provider.const_get(class_name).const_get(class_name)
rescue NameError
raise Puppet::DevError, "class #{class_name} not found in puppet/provider/#{type_name}/#{type_name}"
end
module_function :load_provider # rubocop:disable Style/AccessModifierDeclarations
module_function :load_default_provider # rubocop:disable Style/AccessModifierDeclarations

def load_device_provider(class_name, type_name_sym, device_class_name, device_name_sym)
# loads the "puppet/provider/#{type_name}/#{device_name}" file through puppet
Puppet::Type.type(type_name_sym).provider(device_name_sym)
provider_module = Puppet::Provider.const_get(class_name)
if provider_module.const_defined?(device_class_name)
provider_module.const_get(device_class_name)
else
load_default_provider(class_name, type_name_sym)
end
end
module_function :load_device_provider # rubocop:disable Style/AccessModifierDeclarations

def self.class_name_from_type_name(type_name)
type_name.to_s.split('_').map(&:capitalize).join
Expand Down
92 changes: 92 additions & 0 deletions spec/acceptance/multi_device_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
require 'open3'
require 'puppet/version'
require 'spec_helper'
require 'tempfile'

RSpec.describe 'exercising a type with device-specific providers' do
let(:common_args) { '--verbose --trace --strict=error --modulepath spec/fixtures' }

before(:all) do
FileUtils.mkdir_p(File.expand_path('~/.puppetlabs/opt/puppet/cache/devices/some_node/state'))
FileUtils.mkdir_p(File.expand_path('~/.puppetlabs/opt/puppet/cache/devices/other_node/state'))
end

describe 'using `puppet device`' do
let(:common_args) { super() + " --deviceconfig #{device_conf.path} --target some_node --target other_node" }
let(:device_conf) { Tempfile.new('device.conf') }
let(:device_conf_content) do
<<DEVICE_CONF
[some_node]
type some_device
url file:///etc/credentials.txt
[other_node]
type other_device
url file:///etc/credentials.txt
DEVICE_CONF
end

def is_device_apply_supported?
Gem::Version.new(Puppet::PUPPETVERSION) >= Gem::Version.new('5.3.6') && Gem::Version.new(Puppet::PUPPETVERSION) != Gem::Version.new('5.4.0')
end

before(:each) do
skip "No device --apply in puppet before v5.3.6 nor in v5.4.0 (v#{Puppet::PUPPETVERSION} is installed)" unless is_device_apply_supported?
device_conf.write(device_conf_content)
device_conf.close
end

after(:each) do
device_conf.unlink
end

it 'applies a catalog successfully' do
pending "can't really test this without a puppetserver; when initially implementing this, it was tested using a hacked `puppet device` command allowing multiple --target params"

# diff --git a/lib/puppet/application/device.rb b/lib/puppet/application/device.rb
# index 5e7a5cd473..2d39527b47 100644
# --- a/lib/puppet/application/device.rb
# +++ b/lib/puppet/application/device.rb
# @@ -70,7 +70,8 @@ class Puppet::Application::Device < Puppet::Application
# end

# option("--target DEVICE", "-t") do |arg|
# - options[:target] = arg.to_s
# + options[:target] ||= []
# + options[:target] << arg.to_s
# end

# def summary
# @@ -232,7 +233,7 @@ Licensed under the Apache 2.0 License
# require 'puppet/util/network_device/config'
# devices = Puppet::Util::NetworkDevice::Config.devices.dup
# if options[:target]
# - devices.select! { |key, value| key == options[:target] }
# + devices.select! { |key, value| options[:target].include? key }
# end
# if devices.empty?
# if options[:target]

# david@davids:~/git/puppet-resource_api$ bundle exec puppet device --verbose --trace --strict=error --modulepath spec/fixtures \
# --target some_node --target other_node --resource multi_device multi_device multi_device
# ["multi_device", "multi_device", "multi_device"]
# Info: retrieving resource: multi_device from some_node at file:///etc/credentials.txt
# multi_device { 'multi_device':
# ensure => 'absent',
# }
# ["multi_device"]
# Info: retrieving resource: multi_device from other_node at file:///etc/credentials.txt

# david@davids:~/git/puppet-resource_api$

Tempfile.create('apply_success') do |f|
f.write 'multi_device { "foo": }'
f.close

stdout_str, _status = Open3.capture2e("puppet device #{common_args} --apply #{f.path}")
expect(stdout_str).to match %r{Compiled catalog for some_node}
expect(stdout_str).to match %r{Compiled catalog for other_node}
expect(stdout_str).not_to match %r{Error:}
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'puppet/resource_api/simple_provider'

# Implementation for the multi_device type using the Resource API.
# default provider class
class Puppet::Provider::MultiDevice::MultiDevice < Puppet::ResourceApi::SimpleProvider
def get(_context)
[]
end

def create(context, name, should)
context.notice("Creating '#{name}' with #{should.inspect}")
end

def update(context, name, should)
context.notice("Updating '#{name}' with #{should.inspect}")
end

def delete(context, name)
context.notice("Deleting '#{name}'")
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'puppet/resource_api/simple_provider'

# Implementation for the multi_device type using the Resource API.
# device-specific class for `other_device`
class Puppet::Provider::MultiDevice::OtherDevice < Puppet::ResourceApi::SimpleProvider
def get(_context)
[]
end

def create(context, name, should)
context.notice("Creating '#{name}' with #{should.inspect}")
end

def update(context, name, should)
context.notice("Updating '#{name}' with #{should.inspect}")
end

def delete(context, name)
context.notice("Deleting '#{name}'")
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'puppet/resource_api/simple_provider'

# Implementation for the multi_device type using the Resource API.
# device-specific class for `some_device`
class Puppet::Provider::MultiDevice::SomeDevice < Puppet::ResourceApi::SimpleProvider
def get(_context)
[]
end

def create(context, name, should)
context.notice("Creating '#{name}' with #{should.inspect}")
end

def update(context, name, should)
context.notice("Updating '#{name}' with #{should.inspect}")
end

def delete(context, name)
context.notice("Deleting '#{name}'")
end
end
21 changes: 21 additions & 0 deletions spec/fixtures/test_module/lib/puppet/type/multi_device.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'puppet/resource_api'

Puppet::ResourceApi.register_type(
name: 'multi_device',
docs: <<-EOS,
This type provides Puppet with the capabilities to manage ...
EOS
features: [ 'remote_resource' ],
attributes: {
ensure: {
type: 'Enum[present, absent]',
desc: 'Whether this resource should be present or absent on the target system.',
default: 'present',
},
name: {
type: 'String',
desc: 'The name of the resource you want to manage.',
behaviour: :namevar,
},
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require 'puppet/util/network_device/simple/device'

module Puppet::Util::NetworkDevice::Other_device # rubocop:disable Style/ClassAndModuleCamelCase
# A simple test device returning hardcoded facts
class Device < Puppet::Util::NetworkDevice::Simple::Device
def facts
{ 'foo' => 'bar' }
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require 'puppet/util/network_device/simple/device'

module Puppet::Util::NetworkDevice::Some_device # rubocop:disable Style/ClassAndModuleCamelCase
# A simple test device returning hardcoded facts
class Device < Puppet::Util::NetworkDevice::Simple::Device
def facts
{ 'foo' => 'bar' }
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require 'spec_helper'

ensure_module_defined('Puppet::Provider::MultiDevice')
require 'puppet/provider/multi_device/multi_device'

RSpec.describe Puppet::Provider::MultiDevice::MultiDevice do
subject(:provider) { described_class.new }

let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') }

describe '#get' do
it 'processes resources' do
expect(provider.get(context)).to eq [
{
name: 'foo',
ensure: 'present',
},
{
name: 'bar',
ensure: 'present',
},
]
end
end

describe 'create(context, name, should)' do
it 'creates the resource' do
expect(context).to receive(:notice).with(%r{\ACreating 'a'})

provider.create(context, 'a', name: 'a', ensure: 'present')
end
end

describe 'update(context, name, should)' do
it 'updates the resource' do
expect(context).to receive(:notice).with(%r{\AUpdating 'foo'})

provider.update(context, 'foo', name: 'foo', ensure: 'present')
end
end

describe 'delete(context, name, should)' do
it 'deletes the resource' do
expect(context).to receive(:notice).with(%r{\ADeleting 'foo'})

provider.delete(context, 'foo')
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'spec_helper'
require 'puppet/type/multi_device'

RSpec.describe 'the multi_device type' do
it 'loads' do
expect(Puppet::Type.type(:multi_device)).not_to be_nil
end
end
38 changes: 37 additions & 1 deletion spec/puppet/resource_api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1253,14 +1253,50 @@ def set(_context, _changes); end
context 'when loading a provider that doesn\'t create the correct class' do
let(:definition) { { name: 'no_class', attributes: {} } }

it { expect { described_class.load_provider('no_class') }.to raise_error Puppet::DevError, %r{NoClass} }
it { expect { described_class.load_provider('no_class') }.to raise_error Puppet::DevError, %r{Puppet::Provider::NoClass::NoClass} }
end

context 'when loading a provider that creates the correct class' do
let(:definition) { { name: 'test_provider', attributes: {} } }

it { expect(described_class.load_provider('test_provider').name).to eq 'Puppet::Provider::TestProvider::TestProvider' }
end

context 'with a device configured' do
let(:definition) { { name: 'multi_provider', attributes: {} } }
let(:device) { instance_double('Puppet::Util::NetworkDevice::Simple::Device', 'device') }
let(:device_class) { instance_double(Class, 'device_class') }

before(:each) do
allow(Puppet::Util::NetworkDevice).to receive(:current).with(no_args).and_return(device)
allow(device).to receive(:class).with(no_args).and_return(device_class)
allow(device_class).to receive(:name).with(no_args).and_return(device_class_name)

module ::Puppet::Provider::MultiProvider
class MultiProvider; end
class SomeDevice; end
class OtherDevice; end
end
end

context 'with no provider' do
let(:device_class_name) { 'Puppet::Util::NetworkDevice::Some_device::Device' }

it { expect { described_class.load_provider('no_class') }.to raise_error Puppet::DevError, %r{device-specific provider class Puppet::Provider::NoClass::SomeDevice} }
end

context 'with no device-specific provider' do
let(:device_class_name) { 'Puppet::Util::NetworkDevice::Default_device::Device' }

it('loads the default provider') { expect(described_class.load_provider('multi_provider').name).to eq 'Puppet::Provider::MultiProvider::MultiProvider' }
end

context 'with a device-specific provider' do
let(:device_class_name) { 'Puppet::Util::NetworkDevice::Some_device::Device' }

it('loads the device provider') { expect(described_class.load_provider('multi_provider').name).to eq 'Puppet::Provider::MultiProvider::SomeDevice' }
end
end
end

context 'with a provider that does canonicalization', agent_test: true do
Expand Down

0 comments on commit ee642a4

Please sign in to comment.