17

What's the best way to do a little DRY within a chef recipe? I.e. just break out little bits of the Ruby code, so I'm not copying pasting it over and over again.

The following fails of course, with:

NoMethodError: undefined method `connect_root' for Chef::Resource::RubyBlock

I may have multiple ruby_blocks in one recipe, as they do different things and need to have different not_if blocks to be truley idempotent.

def connect_root(root_password)
  m = Mysql.new("localhost", "root", root_password)
  begin
    yield m
  ensure
    m.close
  end
end

ruby_block "set readonly" do
  block do
    connect_root node[:mysql][:server_root_password] do |connection|
      command = 'SET GLOBAL read_only = ON'
      Chef::Log.info "#{command}"
      connection.query(command)
    end
  end
  not_if do
    ro = nil
    connect_root node[:mysql][:server_root_password] do |connection|
      connection.query("SELECT @@read_only as ro") {|r| r.each_hash {|h| 
        ro = h['ro']
      } }
    end
    ro
  end
end
codeforester
  • 39,467
  • 16
  • 112
  • 140
DragonFax
  • 4,625
  • 2
  • 32
  • 35

2 Answers2

25

As you already figured out, you cannot define functions in recipes. For that libraries are provided. You should create a file (e.g. mysql_helper.rb ) inside libraries folder in your cookbook with the following:

module MysqlHelper
  def self.connect_root( root_password )
    m = Mysql.new("localhost", "root", root_password)
    begin
      yield m
    ensure
      m.close
    end
  end
end

It must be a module, not a class. Notice also we define it as static (using self.method_name). Then you will be able to use functions defined in this module in your recipes using module name with method name:

MysqlHelper.connect_root node[:mysql][:server_root_password] do |connection|
  [...]
end
Draco Ater
  • 20,820
  • 8
  • 62
  • 86
  • I didn't try it, but its a complete answer, with example. Thanks! I'm sad to see that I have to create a whole chef cookbook library for one little function that won't be used anywhere else. But whatever. – DragonFax Mar 28 '13 at 22:34
  • 1
    If you want to use instance methods so you don't have to prefix it with the class name, you would just do `::Chef::Recipe.send(:include, MysqlHelper)` and then you could just call `connect_root` directly. – John Morales Sep 23 '14 at 12:09
  • Thank you @JohnMorales. The accepted answer was perfect, however the line you added (shown below) got it working to completion: ::Chef::Recipe.send(:include, MysqlHelper) – KLaw Apr 27 '16 at 19:16
  • If you want to use module instance methods in recipes you can just add the line `extend MysqlHelper` in your recipe and then all the instance methods of MysqlHelper are available in this recipe. Calling `include` will instead add those methods to every recipe, which is sometimes too broad. – Draco Ater May 02 '16 at 06:54
  • I am using ChefDK `v0.16.28` and am able to define methods inside of recipes. Seems like this may have changed in the last few years. – sixty4bit Aug 11 '16 at 21:12
1

For the record, I just created a library with the following. But that seems overkill for DRY within one file. I also couldn't figure out how to get any other namespace for the module to use, to work.

class Chef
  class Resource
    def connect_root(root_password)
      ...
DragonFax
  • 4,625
  • 2
  • 32
  • 35
  • 9
    The documentation for libraries in chef cookbooks is woefully inadequate and heavily ambiguous. – DragonFax Mar 24 '13 at 05:03
  • 3
    You should not monkeypatch the Resource class. It is much better to create a module and address methods through the module. – Draco Ater Mar 27 '13 at 20:42