Chef Blogs

Chef-12 Dynamic Resource and Provider Resolution

Lamont Granquist | Posted on | community | Releases

Background

In Chef 12 the old Chef::Platform hashmap located in `lib/chef/platform/provider_mapping.rb` has been deprecated. In its place is a dynamic provider and resolver resolution mechanism which is preferred and which can be manipulated via DSL methods on the resource and provider. In Chef 11 it was common to add functionality for platforms in the Chef::Platform hashmap which looks like this:

[ruby]
class Chef
class Platform

class << self
attr_writer :platforms

def platforms
@platforms ||= begin
require ‘chef/providers’

{
:mac_os_x => {
:default => {
:package => Chef::Provider::Package::Macports,
:service => Chef::Provider::Service::Macosx,
:user => Chef::Provider::User::Dscl,
:group => Chef::Provider::Group::Dscl
}
},
:mac_os_x_server => {
:default => {
:package => Chef::Provider::Package::Macports,
:service => Chef::Provider::Service::Macosx,
:user => Chef::Provider::User::Dscl,
:group => Chef::Provider::Group::Dscl
}
},
:freebsd => {
:default => {
:group => Chef::Provider::Group::Pw,
:service => Chef::Provider::Service::Freebsd,
:user => Chef::Provider::User::Pw,
:cron => Chef::Provider::Cron
}
},
[…etc for 400 lines…]
}
end
end
[…etc…]
[/ruby]

Examples of New Syntax

With chef-12 we are starting to wire up providers and resolvers via the `provides` method on the provider and resource classes. Some examples of this include:

Wiring up a resource on all platforms

This is the most trivial example where all platforms get the same cookbook_file resource when the user types ‘cookbook_file’ in a recipe:

[ruby]
class Chef
class Resource
class CookbookFile < Chef::Resource::File
provides :cookbook_file
[…etc…]
end
end
end
[/ruby]

Wiring up a resource on an os

This only wires up the ips_package resource when the node[‘os’] attribute is ‘solaris2’

[ruby]
class Chef
class Resource
class IpsPackage < ::Chef::Resource::Package

provides :ips_package, os: "solaris2"
end
end
end
[/ruby]

Wiring up a resource on multiple platform_families

This is a more complicated example, showing that the provides line supports node[‘platform_family’] and that arrays of values can be used. This also wires up the the yum_package resource to whenever the user types ‘yum_package’ in a recipe no matter which platform (so even on Solaris if you type ‘yum_package’ in a recipe you’ll get this kind of resource), but also on the redhat-like platform_families if the user types ‘package’ we wire that up to resolve to the ‘yum_package’ resource. This is a slight change from Chef 11 where if you typed ‘package “foo”‘ on redhat you would get a vanilla Chef::Resource::Package object which would do vanilla package validation checking and any yum-specific options would be rejected. In Chef 12 on redhat you will get a Chef::Resource::YumPackage object which will do the correct validation for the YumPackage provider.

[ruby]
class Chef
class Resource
class YumPackage < Chef::Resource::Package

provides :yum_package
provides :package, os: "linux", platform_family: [ "rhel", "fedora" ]
end
end
end
[/ruby]

Wiring up a Resource based on arbitrary node attributes

On Solaris2 for platform_version of <= 5.10 we need to use solaris_package while on platform_version of >= 5.11 we need to use ips_package so our provides line looks like this:

[ruby]
class Chef
class Resource
class SolarisPackage < Chef::Resource::Package

provides :solaris_package
provides :package, os: "solaris2", platform_family: "nexentacore"
provides :package, os: "solaris2", platform_family: "solaris2" do |node|
# on >= Solaris 11 we default to IPS packages instead
node[:platform_version].to_f <= 5.10
end

end
end
end
[/ruby]

Resource and Provider Provides Lines

For every provides line in a Resource file there should generally be a corresponding provides line in the Provider file. Resources should no longer set the provider explicitly in the constructor of the Resource. It still works to explicitly define the provider in the Resource but this will bypass dynamic provider resolution. It also still works to not have a provides line in the provider file and mangling based on the resource name will still be able to determine the provider, but this is deprecated and soon Chef will warn and then eventually fail if you don’t have matching provides lines in both the Resource and Provider.

Supported Provides Syntax

The provides line has ‘os’, ‘platform’ and ‘platform_family’ options which match either arrays or strings. It will also take a block that the node object is passed to and which is expect to return true if the wiring should be done on the node. When multiple matchers are present all of the conditionals must be true. Multiple provides lines can be used for multiple conditions, and the array syntax also matches any of the array components.

[ruby]
provides :cookbook_file
provides :package, os: "windows"
provides :rpm_package, os: [ "linux", "aix" ]
provides :package, os: "solaris2", platform_family: "smartos"
provides :package, platform: "freebsd"
provides :package, os: "linux", platform_family: [ "rhel", "fedora" ]
provides :package, os: "solaris2", platform_family: "solaris2" do |node|
node[:platform_version].to_f <= 5.10
end
[/ruby]

The implementation of the syntax is contained within the lib/chef/node_map.rb file. A Chef::NodeMap object is a key-value store where the values can be inserted with conditions based on the node object (and then only if the node object matches will they be retrieved).

Dynamic Provider Resolution

Providers also do dynamic resolution. They also have additional methods that they can override to implement Chef::Provider.provides? and Chef::Provider.supports? methods to determine if the platform supports a given provider (e.g. “is systemd the init system or not?”) and if the provider provides? a given resource (e.g. “is service ‘foo’ managed by sysv init scripts or upstart?”). This is almost entirely designed to dynamically handle the use case of Linux init script systems, and the details are out of the scope of this blog post for today. Adventuresome users can poke around the service providers.

LWRP usage

This can be used to wire up LWRPs to arbitrary names! You are no longer bound by the ‘[cookbook_name]_[provider_filename]’ default and can even wire up your own LWRPs to the package provider if you want to (although there be dragons — consider that if we ever implement the package provider on your platform in core chef that your custom package provider will collide with the new core chef one and you may break in a minor release since this is an API extension for us, and not a breaking change for our API).

A simple example:

resources/default.rb:

[ruby]
actions :run
default_action :run

provides :foo_bar

attribute :thing, kind_of: String, name_attribute: true
[/ruby]

providers/default.rb:

[ruby]
use_inline_resources

provides :foo_bar

action :run do
Chef::Log.warn new_resource.thing
end
[/ruby]

recipes/default.rb:

[ruby]
foo_bar "baz"
[/ruby]

LWRP Chef-11 BackCompat

It turns out that Chef-11 supports the ‘provides’ syntax on resources, so that this feature can be used in community cookbooks and other places where Chef-11 backcompat is still important. Chef-12 simply improved on the API which was already present for Resources. It does not allow the ‘provides’ syntax on Providers, and it only takes an “on_platform:” argument (Chef-12 also supports “on_platform” as an alias for “platform” for back-compat). To rename LWRPs and maintain Chef-11 backcompat simply drop the ‘provides’ line from the Provider, or ideally protect it with an ‘if respond_to?(:provides)’ check.

Status

The dynamic Provider and Resolver features are still under development and were not completed with Chef 12. There are still entries in the Chef::Platform platform_map which need to be converted into dynamic resolution and emptied out. Eventually that hash table needs to be completely dropped. There is magic name-to-class mangling that occurs in both Resources and Providers that will be dropped. There are useful helper modules for determining the init system the host is using which need to be exposed as DSL helper methods to assist in writing cookbook code that needs to switch behavior based on the init system actually being used.

There are also currently no docs at docs.chef.io for any of these APIs (if your name is James Scott you should ping me about fixing this).