Advanced Chef: Writing Heavy Weight Resource Providers (HWRP)

The last in a short series of Chef related blog posts

Heavy Weight Resource Providers

This is a bit of a backwards term. When Chef first came out, there was no Light Weight Resource Provider (LWRP) syntax and any hardcore extension to Chef had to be written in Ruby. However, Opscode saw a need to be filled and created LWRP, making it easier to create your own Resources. The problem comes when LWRP cannot fulfill all of your needs. This means you need to fall back to writing pure ruby code. For lack of a better term, I’ll call this method a HWRP, or Heavy Weight Resource Provider.

While writing a LWRP is meant to be simple and elegant, writing a HWRP is meant to be flexible. It gives you the full power of ruby in exchange for elegance.

Let’s go over some interactions between LWRP and HWRP.

A few things you need to know

HWRPs and LWRPS are interchangeable

With LWRP you are taught to create a Resource and a Provider together. This is the simplest way. However, just because you need to convert a resource definition or a provider into a HWRP you do not need to convert both.

The LWRP syntax ’compiles’ into real ruby code, so Chef will not know the difference in how they were defined. A valid cookbook directory structure:

1
2
3
4
5
6
7
8
libraries/
    provider_default.rb
providers/
resources/
    default.rb
recipes/
    default.rb
metadata.rb

HWRPs live in library files

Anything you put in resources/ or providers/ Chef will attempt to parse at runtime. We don’t want Chef trying to read our HWRP as the Chef DSL, we want it to interpret it as code. Luckily, anything stored in the libraries/ folder Chef will try to import at runtime. A good example of this can be seen in the runit cookbook.

How to write a HWRP:

Let’s go through an example. We are going to create a HWRP that is very simple, it could easily be written as a LWRP. In fact, it will be. While we write the HWRP I will post examples of the analogous LWRP code when applicable.

  • Cookbook Name: cloud
  • Resource Name: magic

An example of calling this in a recipe:

1
2
3
4
5
cloud_magic "My Cloud Magic" do
  action :create
  cloud "My Cloud Magic"
  magic true
end

Resources

Class Structure

We need to inherit from the appropriate Chef classes in our HWRP. Note the class hierarchy as well as the inheritance:

HWRP

1
2
3
4
5
6
7
8
9
10
11
require 'chef/resource'

class Chef
  class Resource
    class CloudMagic < Chef::Resource

    # Some Magic Happens

    end
  end
end

LWRP

This has no counterpart in a LWRP as this part is done automagically by Chef when it reads your files.

Initialization Method

We need to override the initialize method to make sure we have some defaults. We aren’t defining all of our resource attributes here, just the ones that need defaults.

HWRP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require 'chef/resource'

class Chef
  class Resource
    class  CloudMagic < Chef::Resource

      def initialize(name, run_context=nil)
        super
        @resource_name = :cloud_magic          # Bind ourselves to the name with an underscore
        @provider = Chef::Provider::CloudMagic # We need to tie to our provider
        @action = :enable                     # Default Action Goes here
        @allowed_actions = [:create, :remove]

        # Now we need to set up any resource defaults
        @magic = true
        @cloud = name  # This is equivalent to setting :name_attribute => true
      end

    # Some Magic Happens

    end
  end
end

Here is a similar LWRP although it actually defines a bit more than the HWRP due to the terseness of the syntax.

LWRP

1
2
3
4
5
action :enable, :remove
default_action :enable

attribute :magic, kind_of => [TrueClass, FalseClass], :default => true
attribute :cloud, kind_of => String, :name_attribute => true

Attribute Methods

Now lets set up some attribute methods in our HWRP. Make sure to read the code comments for an explanation of what is going on.

HWRP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
require 'chef/resource'

class Chef
  class Resource
    class  CloudMagic < Chef::Resource

      def initialize(name, run_context=nil)
        super
        @resource_name = :cloud_magic          # Bind ourselves to the name with an underscore
        @provider = Chef::Provider::CloudMagic # We need to tie to our provider
        @action = :enable                      # Default Action Goes here
        @allowed_actions = [:create, :remove]

        # Now we need to set up any resource defaults
        @magic = true
        @cloud = name  # This is equivalent to setting :name_attribute => true
      end

      # Define the attributes we set defaults for
      def magic(arg=nil)
        set_or_return(:magic, arg, :kind_of => [TrueClass, FalseClass])
      end

      def cloud(arg=nil)
        # set_or_return is a magic function from Chef that does most of the heavy
        # lifting for attribute access.
        set_or_return(:cloud, arg, :kind_of => String)
        # For now all you need to know is that the method name should be given as
        # a symbol (like :cloud) and that you declare the type with :kind_of
      end

    end
  end
end

There are no more changes to our LWRP everything we just added with these two methods is already included in the LWRP syntax.

LWRP

1
2
3
4
5
action :enable, :remove
default_action :enable

attribute :magic, kind_of => [TrueClass, FalseClass], :default => true
attribute :cloud, kind_of => String, :name_attribute => true

Awesome, now we have defined our resource. Let’s move on to a basic provider.

Providers

Class Structure

Very similar to resources, here is the basic class structure for a provider.

HWRP

1
2
3
4
5
6
7
8
9
class Chef
  class Provider
    class CloudMagic < Chef::Provider

    # Magic Happens

    end
  end
end

LWRP

Once again, this is pure boilerplate with no analogy in a LWRP.

Initialization Method

While we don’t need to write an initialize method (we can), we do need to override load_current_resource.

HWRP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Chef
  class Provider
    class CloudMagic < Chef::Provider

      # We MUST override this method in our custom provider
      def load_current_resource
        # Here we keep the existing version of the resource
        # if none exists we create a new one from the resource we defined earlier
        @current_resource ||= Chef::Resource::CloudMagic.new(new_resource.name)

        # New resource represents the chef DSL block that is being run (from a recipe for example)
        @current_resource.magic(new_resource.magic)
        # Although you can reference @new_resource throughout the provider it is best to
        # only make modifications to the current version
        @current_resource.cloud(new_resource.cloud)
        @current_resource
      end

      # Magic Happens

    end
  end
end

LWRP

No LWRP changes yet, all we have done so far is set up some boilerplate for the HWRP.

Action Methods

Now it is time to define what we do in our actions, with our HWRP we need to define methods like action_create to define a :create action. Chef will do some introspection to find these methods and hook them up.

HWRP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Chef
  class Provider
    class CloudMagic < Chef::Provider

      # We MUST override this method in our custom provider
      def load_current_resource
        # Here we keep the existing version of the resource
        # if none exists we create a new one from the resource we defined earlier
        @current_resource ||= Chef::Resource::CloudMagic.new(new_resource.name)

        # New resource represents the chef DSL block that is being run (from a recipe for example)
        @current_resource.magic(new_resource.cloud)
        # Although you can reference @new_resource throughout the provider it is best to
        # only make modifications to the current version
        @current_resource.cloud(new_resource.magic)
        @current_resource
      end

      def action_create
        # Some ruby code
      end

      def action_remove
        # More ruby code
      end

    end
  end
end

For our LWRP, it is pretty simple

LWRP

1
2
3
4
5
6
7
action :create do
  # Some Chef Code
end

action :remove do
  # Some more Chef code
end

Off into the wild blue yonder

At this point you should have a functioning HWRP. Sure, it doesn’t do anything, but it is best to start small.

Also take a look at the swap cookbook, a hybrid cookbook with a provider written as a HWRP.

Now that you can read these, you should be able to start picking apart definitions inside Chef core as they are very similar.

Andrew Gross is a Developer at Yipit, you can find him as @awgross on Twitter

Comments