About Libraries

A library allows arbitrary Ruby code to be included in a cookbook, either as a way of extending the classes that are built-in to the chef-client—Chef::Recipe, for example—or for implementing entirely new functionality, similar to a mixin in Ruby. A library file is a Ruby file that is located within a cookbook’s /libraries directory. Because a library is built using Ruby, anything that can be done with Ruby can be done in a library file.

Use a library to:

  • Define a custom resource
  • Create a custom class or module; for example, create a subclass of Chef::Recipe
  • Access attributes that are stored in files
  • Connect to a database
  • Talk to an LDAP provider
  • Do anything that can be done with Ruby

Syntax

The syntax for a library varies because library files are created using Ruby and are designed to handle custom situations. See the Examples section below for some ideas. Also, the https://github.com/chef-cookbooks/database and https://github.com/chef-cookbooks/chef-splunk cookbooks contain more detailed and complex examples.

Chef::Provider

Cookbooks that contain custom resources in the /libraries directory of a cookbook should:

  • Be inspected for instances of a) the Chef::Provider base class, and then b) for the presence of any core resources from the chef-client
  • Be updated to use the LWRPBase base class

For example:

class Chef
  class Provider
    class LvmLogicalVolume < Chef::Provider::LWRPBase
      include Chef::Mixin::ShellOut

      ...
      if new_resource.mount_point
        if new_resource.mount_point.is_a?(String)
          mount_spec = { :location => new_resource.mount_point }
        else
          mount_spec = new_resource.mount_point
        end

        dir_resource = directory mount_spec[:location] do
          mode 0755
          owner 'root'
          group 'root'
          recursive true
          action :nothing
          not_if { Pathname.new(mount_spec[:location]).mountpoint? }
        end
        dir_resource.run_action(:create)
        updates << dir_resource.updated?

        mount_resource = mount mount_spec[:location] do
          options mount_spec[:options]
          dump mount_spec[:dump]
          pass mount_spec[:pass]
          device device_name
          fstype fs_type
          action :nothing
        end
        mount_resource.run_action(:mount)
        mount_resource.run_action(:enable)
        updates << mount_resource.updated?
      end
      new_resource.updated_by_last_action(updates.any?)
    end

Dynamic Resolution

Resources and providers are resolved dynamically and can handle multiple provides lines for a specific platform. When multiple provides lines exist, such as Homebrew and MacPorts packages for the Mac OS X platform, then one is selected based on resource priority mapping performed by the chef-client during the chef-client run.

Use the following helpers in a library file to get and/or set resource and/or provider priority mapping before any recipes are compiled:

Chef.get_provider_priority_array(resource_name)
Get the priority mapping for a provider.
Chef.get_resource_priority_array(resource_name)
Get the priority mapping for a resource.
Chef.set_provider_priority_array(resource_name, Array<Class>, *filter)
Set the priority mapping for a provider.
Chef.set_resource_priority_array(resource_name, Array<Class>, *filter)
Set the priority mapping for a resource.

For example:

Chef.set_resource_priority_array(:package, [ Chef::Resource::MacportsPackage ], os: 'darwin')

Template Helper Modules

A template helper module can be defined in a library. This is useful when extensions need to be reused across recipes or to make it easier to manage code that would otherwise be defined inline on a per-recipe basis.

template '/path/to/template.erb' do
  helpers(MyHelperModule)
end

Examples

The following examples show how to use cookbook libraries.

Library Resources

A resource can also be defined in /libraries directory. Some advantages of this approach include more control over how resources behave in the provider, the ability to control the name of the resource directly, and more options available for writing tests. The resources and providers for a library resource, similar to lightweight resources (defined in the /resources and /providers folders) typically have a separate file for the resource and the provider, but this is not requirement. The main disadvantage of this approach is that resources defined in the /libraries directory may not use the Recipe DSL.

A resource that is defined in the /libraries directory may leverage core chef-client resources by using the following syntax:

Chef::Resource::name_of_resource.new('name', run_context)

or:

Chef::Resource::name_of_resource.new(:action)

For example, the following definition leverages the directory resource to create a new directory, and then evaluate that within the context of the custom resource:

def env_dir
  return @env_dir unless @env_dir.nil?
  @env_dir = Chef::Resource::Directory.new(::File.join(sv_dir_name, 'env'), run_context)
  @env_dir.owner(new_resource.owner)
  @env_dir.group(new_resource.group)
  @env_dir.mode(00755)
  @env_dir
end

The following definition uses the template resource for Debian-specific cases, but then the link resource for everything else:

def lsb_init
  return @lsb_init unless @lsb_init.nil?
  initfile = ::File.join(new_resource.lsb_init_dir, new_resource.service_name)
  if node['platform'] == 'debian'
    ::File.unlink(initfile) if ::File.symlink?(initfile)
    @lsb_init = Chef::Resource::Template.new(initfile, run_context)
    @lsb_init.owner('root')
    @lsb_init.group('root')
    @lsb_init.mode(00755)
    @lsb_init.cookbook('runit')
    @lsb_init.source('init.d.erb')
    @lsb_init.variables(:name => new_resource.service_name)
  else
    @lsb_init = Chef::Resource::Link.new(initfile, run_context)
    @lsb_init.to(new_resource.sv_bin)
  end
  @lsb_init
end

Otherwise, a resource defined in the /libraries directory is done using Ruby, is added to recipes as if it were any other resource, and is processed by the chef-client in the same way as any other resource. See the /libraries directory in the database and runit cookbooks for complete examples of how to use this approach when defining a resource.

Create a Namespace

A database can contain a list of virtual hosts that are used by customers. A custom namespace could be created that looks something like:

# Sample provided by "Arjuna (fujin)". Thank you!

require 'sequel'

class Chef::Recipe::ISP
  # We can call this with ISP.vhosts
  def self.vhosts
    v = []
    @db = Sequel.mysql(
      'web',
      :user => 'example',
      :password => 'example_pw',
      :host => 'dbserver.example.com'
    )
    @db[
      "SELECT virtualhost.domainname,
           usertable.userid,
           usertable.uid,
           usertable.gid,
           usertable.homedir
       FROM usertable, virtualhost
       WHERE usertable.userid = virtualhost.user_name"
      ].all do |query|
      vhost_data = {
        :servername   => query[:domainname],
        :documentroot => query[:homedir],
        :uid          => query[:uid],
        :gid          => query[:gid],
      }
      v.push(vhost_data)
    end
    Chef::Log.debug('About to provision #{v.length} vhosts')
    v
  end
end

After the custom namespace is created, it could then be used in a recipe, like this:

ISP.vhosts.each do |vhost|
  directory vhost[:documentroot] do
    owner 'vhost[:uid]'
    group 'vhost[:gid]'
    mode '0755'
    action :create
  end

  directory '#{vhost[:documentroot]}/#{vhost[:domainname]}' do
    owner 'vhost[:uid]'
    group 'vhost[:gid]'
    mode '0755'
    action :create
  end
end

Extend a Recipe

A customer record is stored in an attribute file that looks like this:

mycompany_customers({
  :bob => {
    :homedir => '/home/bob',
    :webdir => '/home/bob/web'
  }
}
)

A simple recipe may contain something like this:

directory node[:mycompany_customers][:bob][:webdir] do
  owner 'bob'
  group 'bob'
  action :create
end

Or a less verbose version of the same simple recipe:

directory customer(:bob)[:webdir] do
  owner 'bob'
  group 'bob'
  action :create
end

A simple library could be created that extends Chef::Recipe::, like this:

class Chef
  class Recipe
    # A shortcut to a customer
    def customer(name)
      node[:mycompany_customers][name]
    end
  end
end

Loop Over a Record

A customer record is stored in an attribute file that looks like this:

mycompany_customers({
  :bob => {
    :homedir => '/home/bob',
    :webdir => '/home/bob/web'
  }
}
)

If there are many customer records in an environment, a simple recipe can be used to loop over every customer, like this:

all_customers do |name, info|
  directory info[:webdir] do
    owner 'name'
    group 'name'
    action :create
  end
end

A simple library could be created that extends Chef::Recipe::, like this:

class Chef
  class Recipe
    def all_customers(&block)
      node[:mycompany_customers].each do |name, info|
        block.call(name, info)
      end
    end
  end
end

© Chef Software, Inc.
Licensed under the Creative Commons Attribution 3.0 Unported License.
The Chef™ Mark and Chef Logo are either registered trademarks/service marks or trademarks/servicemarks of Chef, in the United States and other countries and are used with Chef Inc's permission.
We are not affiliated with, endorsed or sponsored by Chef Inc.
https://docs-archive.chef.io/release/12-13/libraries.html