Skip to content

Commit 90ae3d8

Browse files
authored
Merge pull request #126 from DavidS/pdk-1185-multi-device-providers
(PDK-1185) Implement allowances for device-specific providers
2 parents cecd90b + ee642a4 commit 90ae3d8

File tree

12 files changed

+334
-5
lines changed

12 files changed

+334
-5
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ The provider needs to specify the `remote_resource` feature to enable the second
180180

181181
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).
182182

183+
#### Device-specific providers
184+
185+
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`.
186+
183187
### Further Reading
184188

185189
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.

lib/puppet/resource_api.rb

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'puppet/resource_api/type_definition'
55
require 'puppet/resource_api/version'
66
require 'puppet/type'
7+
require 'puppet/util/network_device'
78

89
module Puppet::ResourceApi
910
@warning_count = 0
@@ -54,7 +55,8 @@ def register_type(definition)
5455

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

6062
# make the provider available in the instance's namespace
@@ -514,14 +516,48 @@ def self.parse_title_patterns(patterns)
514516
def load_provider(type_name)
515517
class_name = class_name_from_type_name(type_name)
516518
type_name_sym = type_name.to_sym
519+
device_name = if Puppet::Util::NetworkDevice.current.nil?
520+
nil
521+
else
522+
# extract the device type from the currently loaded device's class
523+
Puppet::Util::NetworkDevice.current.class.name.split('::')[-2].downcase
524+
end
525+
device_class_name = class_name_from_type_name(device_name)
526+
527+
if device_name
528+
device_name_sym = device_name.to_sym if device_name
529+
load_device_provider(class_name, type_name_sym, device_class_name, device_name_sym)
530+
else
531+
load_default_provider(class_name, type_name_sym)
532+
end
533+
rescue NameError
534+
if device_name # line too long # rubocop:disable Style/GuardClause
535+
raise Puppet::DevError, "Found neither the device-specific provider class Puppet::Provider::#{class_name}::#{device_class_name} in puppet/provider/#{type_name}/#{device_name}"\
536+
" nor the generic provider class Puppet::Provider::#{class_name}::#{class_name} in puppet/provider/#{type_name}/#{type_name}"
537+
else
538+
raise Puppet::DevError, "provider class Puppet::Provider::#{class_name}::#{class_name} not found in puppet/provider/#{type_name}/#{type_name}"
539+
end
540+
end
541+
module_function :load_provider # rubocop:disable Style/AccessModifierDeclarations
517542

543+
def load_default_provider(class_name, type_name_sym)
518544
# loads the "puppet/provider/#{type_name}/#{type_name}" file through puppet
519545
Puppet::Type.type(type_name_sym).provider(type_name_sym)
520546
Puppet::Provider.const_get(class_name).const_get(class_name)
521-
rescue NameError
522-
raise Puppet::DevError, "class #{class_name} not found in puppet/provider/#{type_name}/#{type_name}"
523547
end
524-
module_function :load_provider # rubocop:disable Style/AccessModifierDeclarations
548+
module_function :load_default_provider # rubocop:disable Style/AccessModifierDeclarations
549+
550+
def load_device_provider(class_name, type_name_sym, device_class_name, device_name_sym)
551+
# loads the "puppet/provider/#{type_name}/#{device_name}" file through puppet
552+
Puppet::Type.type(type_name_sym).provider(device_name_sym)
553+
provider_module = Puppet::Provider.const_get(class_name)
554+
if provider_module.const_defined?(device_class_name)
555+
provider_module.const_get(device_class_name)
556+
else
557+
load_default_provider(class_name, type_name_sym)
558+
end
559+
end
560+
module_function :load_device_provider # rubocop:disable Style/AccessModifierDeclarations
525561

526562
def self.class_name_from_type_name(type_name)
527563
type_name.to_s.split('_').map(&:capitalize).join
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
require 'open3'
2+
require 'puppet/version'
3+
require 'spec_helper'
4+
require 'tempfile'
5+
6+
RSpec.describe 'exercising a type with device-specific providers' do
7+
let(:common_args) { '--verbose --trace --strict=error --modulepath spec/fixtures' }
8+
9+
before(:all) do
10+
FileUtils.mkdir_p(File.expand_path('~/.puppetlabs/opt/puppet/cache/devices/some_node/state'))
11+
FileUtils.mkdir_p(File.expand_path('~/.puppetlabs/opt/puppet/cache/devices/other_node/state'))
12+
end
13+
14+
describe 'using `puppet device`' do
15+
let(:common_args) { super() + " --deviceconfig #{device_conf.path} --target some_node --target other_node" }
16+
let(:device_conf) { Tempfile.new('device.conf') }
17+
let(:device_conf_content) do
18+
<<DEVICE_CONF
19+
[some_node]
20+
type some_device
21+
url file:///etc/credentials.txt
22+
[other_node]
23+
type other_device
24+
url file:///etc/credentials.txt
25+
DEVICE_CONF
26+
end
27+
28+
def is_device_apply_supported?
29+
Gem::Version.new(Puppet::PUPPETVERSION) >= Gem::Version.new('5.3.6') && Gem::Version.new(Puppet::PUPPETVERSION) != Gem::Version.new('5.4.0')
30+
end
31+
32+
before(:each) do
33+
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?
34+
device_conf.write(device_conf_content)
35+
device_conf.close
36+
end
37+
38+
after(:each) do
39+
device_conf.unlink
40+
end
41+
42+
it 'applies a catalog successfully' do
43+
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"
44+
45+
# diff --git a/lib/puppet/application/device.rb b/lib/puppet/application/device.rb
46+
# index 5e7a5cd473..2d39527b47 100644
47+
# --- a/lib/puppet/application/device.rb
48+
# +++ b/lib/puppet/application/device.rb
49+
# @@ -70,7 +70,8 @@ class Puppet::Application::Device < Puppet::Application
50+
# end
51+
52+
# option("--target DEVICE", "-t") do |arg|
53+
# - options[:target] = arg.to_s
54+
# + options[:target] ||= []
55+
# + options[:target] << arg.to_s
56+
# end
57+
58+
# def summary
59+
# @@ -232,7 +233,7 @@ Licensed under the Apache 2.0 License
60+
# require 'puppet/util/network_device/config'
61+
# devices = Puppet::Util::NetworkDevice::Config.devices.dup
62+
# if options[:target]
63+
# - devices.select! { |key, value| key == options[:target] }
64+
# + devices.select! { |key, value| options[:target].include? key }
65+
# end
66+
# if devices.empty?
67+
# if options[:target]
68+
69+
# david@davids:~/git/puppet-resource_api$ bundle exec puppet device --verbose --trace --strict=error --modulepath spec/fixtures \
70+
# --target some_node --target other_node --resource multi_device multi_device multi_device
71+
# ["multi_device", "multi_device", "multi_device"]
72+
# Info: retrieving resource: multi_device from some_node at file:///etc/credentials.txt
73+
# multi_device { 'multi_device':
74+
# ensure => 'absent',
75+
# }
76+
# ["multi_device"]
77+
# Info: retrieving resource: multi_device from other_node at file:///etc/credentials.txt
78+
79+
# david@davids:~/git/puppet-resource_api$
80+
81+
Tempfile.create('apply_success') do |f|
82+
f.write 'multi_device { "foo": }'
83+
f.close
84+
85+
stdout_str, _status = Open3.capture2e("puppet device #{common_args} --apply #{f.path}")
86+
expect(stdout_str).to match %r{Compiled catalog for some_node}
87+
expect(stdout_str).to match %r{Compiled catalog for other_node}
88+
expect(stdout_str).not_to match %r{Error:}
89+
end
90+
end
91+
end
92+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
require 'puppet/resource_api/simple_provider'
2+
3+
# Implementation for the multi_device type using the Resource API.
4+
# default provider class
5+
class Puppet::Provider::MultiDevice::MultiDevice < Puppet::ResourceApi::SimpleProvider
6+
def get(_context)
7+
[]
8+
end
9+
10+
def create(context, name, should)
11+
context.notice("Creating '#{name}' with #{should.inspect}")
12+
end
13+
14+
def update(context, name, should)
15+
context.notice("Updating '#{name}' with #{should.inspect}")
16+
end
17+
18+
def delete(context, name)
19+
context.notice("Deleting '#{name}'")
20+
end
21+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
require 'puppet/resource_api/simple_provider'
2+
3+
# Implementation for the multi_device type using the Resource API.
4+
# device-specific class for `other_device`
5+
class Puppet::Provider::MultiDevice::OtherDevice < Puppet::ResourceApi::SimpleProvider
6+
def get(_context)
7+
[]
8+
end
9+
10+
def create(context, name, should)
11+
context.notice("Creating '#{name}' with #{should.inspect}")
12+
end
13+
14+
def update(context, name, should)
15+
context.notice("Updating '#{name}' with #{should.inspect}")
16+
end
17+
18+
def delete(context, name)
19+
context.notice("Deleting '#{name}'")
20+
end
21+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
require 'puppet/resource_api/simple_provider'
2+
3+
# Implementation for the multi_device type using the Resource API.
4+
# device-specific class for `some_device`
5+
class Puppet::Provider::MultiDevice::SomeDevice < Puppet::ResourceApi::SimpleProvider
6+
def get(_context)
7+
[]
8+
end
9+
10+
def create(context, name, should)
11+
context.notice("Creating '#{name}' with #{should.inspect}")
12+
end
13+
14+
def update(context, name, should)
15+
context.notice("Updating '#{name}' with #{should.inspect}")
16+
end
17+
18+
def delete(context, name)
19+
context.notice("Deleting '#{name}'")
20+
end
21+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
require 'puppet/resource_api'
2+
3+
Puppet::ResourceApi.register_type(
4+
name: 'multi_device',
5+
docs: <<-EOS,
6+
This type provides Puppet with the capabilities to manage ...
7+
EOS
8+
features: [ 'remote_resource' ],
9+
attributes: {
10+
ensure: {
11+
type: 'Enum[present, absent]',
12+
desc: 'Whether this resource should be present or absent on the target system.',
13+
default: 'present',
14+
},
15+
name: {
16+
type: 'String',
17+
desc: 'The name of the resource you want to manage.',
18+
behaviour: :namevar,
19+
},
20+
},
21+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
require 'puppet/util/network_device/simple/device'
2+
3+
module Puppet::Util::NetworkDevice::Other_device # rubocop:disable Style/ClassAndModuleCamelCase
4+
# A simple test device returning hardcoded facts
5+
class Device < Puppet::Util::NetworkDevice::Simple::Device
6+
def facts
7+
{ 'foo' => 'bar' }
8+
end
9+
end
10+
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
require 'puppet/util/network_device/simple/device'
2+
3+
module Puppet::Util::NetworkDevice::Some_device # rubocop:disable Style/ClassAndModuleCamelCase
4+
# A simple test device returning hardcoded facts
5+
class Device < Puppet::Util::NetworkDevice::Simple::Device
6+
def facts
7+
{ 'foo' => 'bar' }
8+
end
9+
end
10+
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
require 'spec_helper'
2+
3+
ensure_module_defined('Puppet::Provider::MultiDevice')
4+
require 'puppet/provider/multi_device/multi_device'
5+
6+
RSpec.describe Puppet::Provider::MultiDevice::MultiDevice do
7+
subject(:provider) { described_class.new }
8+
9+
let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') }
10+
11+
describe '#get' do
12+
it 'processes resources' do
13+
expect(provider.get(context)).to eq [
14+
{
15+
name: 'foo',
16+
ensure: 'present',
17+
},
18+
{
19+
name: 'bar',
20+
ensure: 'present',
21+
},
22+
]
23+
end
24+
end
25+
26+
describe 'create(context, name, should)' do
27+
it 'creates the resource' do
28+
expect(context).to receive(:notice).with(%r{\ACreating 'a'})
29+
30+
provider.create(context, 'a', name: 'a', ensure: 'present')
31+
end
32+
end
33+
34+
describe 'update(context, name, should)' do
35+
it 'updates the resource' do
36+
expect(context).to receive(:notice).with(%r{\AUpdating 'foo'})
37+
38+
provider.update(context, 'foo', name: 'foo', ensure: 'present')
39+
end
40+
end
41+
42+
describe 'delete(context, name, should)' do
43+
it 'deletes the resource' do
44+
expect(context).to receive(:notice).with(%r{\ADeleting 'foo'})
45+
46+
provider.delete(context, 'foo')
47+
end
48+
end
49+
end

0 commit comments

Comments
 (0)