Fixed Issues and Customizations in Chef Recipes from Opscode

During testing and evaluation we identified a couple of issues in the community-maintained Chef cookbooks downloaded from Opscode. The issues were related to either

  1. required customizations which are necessary to run the cookbook (i.e., the cookbook fails if executed with the default values), or
  2. minor bugs and issues such as temporarily undownloadable Web resources, incorrect assumptions about pre-installed packages on the target machine, etc.
The encountered issues prevented the recipes from executing successfully in our environment, and hence had to be fixed in order to obtain meaningful results for our actual testing efforts (testing idempotence and convergence). Our implementation introduces two types of recipes which we apply as "patches" to the broken recipes: The tasks in a pre-processing recipe are executed before the actual recipe under test, and the tasks in a post-processing recipe are executed after the actual recipe under test. Moreover, post-processing recipes can be used to overwrite an incorrect task declaration with a corrected version of this task (resources in a post-processing replace all previous resources with the same name). The list below contains an excerpt of the key patches that were applied. It should be noted that our testing framework helped us tremendously in automatically identifying these bugs and issues.

Patch Script 'pre__vmware__tools'

# fix a statement in recipe vmware::tools: 
# "if node.virtualization.system == 'vmware'" 
# (fails without this attribute definition)
node.set["virtualization"]["system"] = 'vmware'

Patch Script 'pre__gitosis__default'

# Dependencies for recipe "gitosis::default"

# Create Chef data_bags directory, if it does not yet exist (fixes an
# issue where Chef was complaining that /var/chef/data_bags does not exist)
require 'fileutils'
databags_dir = "/var/chef/data_bags"
FileUtils.mkpath(databags_dir) if !File.directory?(databags_dir)

# Create user/group gitosis (recipe fails if they don't exist)
user "gitosis" do
  action :create
end
group "gitosis" do
  action :create
  members "gitosis"
  append false
end

Patch Script 'pre__sol__default'

# Pre-processing tasks for recipe sol::default
node.set['dmi']['system']['manufacturer'] = "foobar"

Patch Script 'pre__drbd__default'

# overwrite Node#save to make this work under Chef-solo
#module MySaveOverwrite
#  def save
#  end
#end
#node.extend(MySaveOverwrite)
node.define_singleton_method(:save) { }

Patch Script 'pre__xen__default'

# Dependencies for recipe "xen::default"

# Create Chef data_bags directory, if it does not yet exist (fixes an
# issue where Chef was complaining that /var/chef/data_bags does not exist)
require 'fileutils'
databags_dir = "/var/chef/data_bags"
FileUtils.mkpath(databags_dir) if !File.directory?(databags_dir)

Patch Script 'pre__eaccelerator__default'

# we need to include the required recipe "apache2"
include_recipe "apache2"

Patch Script 'pre__pxe_install_server__default'

#
# If the required parameter node[:pxe_install_server][:releases] 
# is not set, the recipe fails with an exception.
#

node.set[:pxe_install_server][:releases] = {}

Patch Script 'pre__munin__server'

# This pre-processing recipe fixes requirements for munin::server.

# Chef complains if /var/chef/data_bags does not exist
require 'fileutils'
databags_dir = "/var/chef/data_bags"
FileUtils.mkpath(databags_dir) if !File.directory?(databags_dir)

Patch Script 'pre__netkernel__default'

# recipe netkernel::default uses an illegal statement which we fix here:
# "source defaults.erb" (string should be under apostrophe)
class MyFoo
  def erb
    "defaults.erb"
  end
end
class ::Chef
  class Node
    def defaults
      return MyFoo.new
    end
  end
end
service "apache" do
  supports :restart => true, :reload => true
  action :nothing
end
directory "/opt/netkernel/bin/" do
  action :create
  owner "root"
  group "root"
  recursive true
end

Patch Script 'pre__sonar__default'

# recipe sonar::default fails without Maven version
node.set['maven']['version'] = "2"

Patch Script 'pre__app__default'

# Without gem "bluepill" the recipe app::default 
# complains about not finding /bin/bluepill
gem_package "bluepill"

Patch Script 'pre__nagios__default'

# nagios::default uses search(...) which is only available 
# with chef server. Cookbook chef-solo-search fixes this.
include_recipe "chef-solo-search::default"

Patch Script 'pre__wordpress__default'

#
# This pre-processing recipe fixes some requirements for wordpress.
# 

# package required for mysql gem
package "mysql devel package" do
  if ["ubuntu","debian"].include?(node[:platform]) 
    package_name "libmysqlclient-dev"
  else
    package_name "mysql-devel"
  end
  action :install
end

# fix mysql root password (mysql-server uses an 
# empty password by default under Ubuntu)
node.set['mysql']['server_root_password'] = ""

Patch Script 'pre__sensu__default'

# Dependencies for recipe "pxe_dust::default"

# Create Chef data_bags directory, if it does not yet exist (fixes an
# issue where Chef was complaining that /var/chef/data_bags/.. does not exist)
require 'fileutils'
databags_dir = "/var/chef/data_bags/sensu"
FileUtils.mkpath(databags_dir) if !File.directory?(databags_dir)
databag_file = File.join(databags_dir, "ssl.json")
`echo '{"id":"ssl"}' > #{databag_file}`

Patch Script 'pre__cube__default'

#
# This pre-processing recipe fixes some requirements for cube.
# 

# create Chef data_bags directory, if it does not yet exist (fixes an 
# issue where Chef was complaining that /var/chef/data_bags does not exist)
require 'fileutils'
databags_dir = "/var/chef/data_bags"
FileUtils.mkpath(databags_dir) if !File.directory?(databags_dir)

Patch Script 'pre__znc__default'

# Dependencies for recipe "znc::default"

# Create Chef data_bags directory, if it does not yet exist (fixes an
# issue where Chef was complaining that /var/chef/data_bags does not exist)
require 'fileutils'
databags_dir = "/var/chef/data_bags"
FileUtils.mkpath(databags_dir) if !File.directory?(databags_dir)

Patch Script 'pre__solr__default'

# Pre-processing tasks for recipe solr::default

# solr::default calls openldap::default, which tries 
# to access node['domain'].length, but node['domain'] is 
# nil (not automatically set by our Chef installation).
node.set['domain'] = ''

Patch Script 'pre__postgresql__server'

# postgresql::server version 2.1.0 fails with nil 
# pointer exception if this attribute is not set
node.set['postgresql']['password']['postgres'] = "foobar"

Patch Script 'pre__elasticsearch-head__default'

#
# pre-processing customizations for recipe elasticsearch-head::default
#

# apparently, Squid proxy by default cannot cache this https file from github: 
# original (defined in attributes/default.rb):
# default[:elastichead][:src_mirror] = 
#  "https://github.com/mobz/elasticsearch-head/tarball/#{node[:elastichead][:src_branch]}"
# fixed (changed from https to http):
node.set[:elastichead][:src_mirror] =
  "http://github.com/mobz/elasticsearch-head/tarball/#{node[:elastichead][:src_branch]}"

Patch Script 'pre__elasticsearch__default'

#
# This pre-processing recipe fixes some requirements for elasticsearch.
# 

# template generation fails with an exception if this attribute is not set
node.set[:elasticsearch][:seeds] = []

Patch Script 'pre__riak__default'

# recipe riak::default uses some illegal statements 
# (under Chef 11) which we fix here using dynamic property lookup:
node.set["riak"]["kv"] = Hash.new(node["riak"]["kv"])
node.set["riak"]["kv"]["storage_backend"] = :riak_kv_bitcask_backend
node.set["riak"]["sasl"]["errlog_type"] = :error
class DynamicAttrLookup
  def initialize(node_value)
    @node_value = node_value
  end
  def method_missing(m, *args, &block)
    if @node_value.respond_to?(m)
      @node_value.send(m, *args)
    else
      DynamicAttrLookup.new(@node_value[m])
    end
  end
end
class ::Chef
  class Node
    def riak
      DynamicAttrLookup.new(self["riak"])
    end
    def ip_address
      DynamicAttrLookup.new(self["ip_address"])
    end
    class ImmutableMash
      def delete(arg1)
        super(arg1)
      end
    end
  end
    module DSL
      module Recipe
        def default
          puts "Called default!! #{node.set}"
          DynamicAttrLookup.new(node.set)
        end
      end
    end
end

Patch Script 'pre__disco__default'

#
# Default user is "disco" and recipe disco::default assumes that 
# this user already exists, hence the recipe fails by default. To 
# fix this, we can either:
# * create user/group 'disco' before the actual recipe runs.
# * tell the recipe to use the 'root' user.
# 

node.set["disco"]["user"] = "root"
node.set["disco"]["group"] = "root"

#user "disco" do
#  action :create
#end
#
#group "disco" do
#  action :create
#  members "disco"
#  system false
#  append false
#end

Patch Script 'pre__cakephp__default'

# 
# Recipe cakephp::default contains a "require 'mysql'" statement which fails 
# because the mysql gem is not installed by default. Let's do this here...
#

# update packages
execute "apt-get update" do
  ignore_failure true
  action :nothing
end.run_action(:run) if node['platform_family'] == "debian"
# mysql gem requires build-essential
node.set['build_essential']['compiletime'] = true
include_recipe "build-essential"
# mysql gem requires libmysqlclient-dev
package "libmysqlclient-dev" do
  action :nothing
end.run_action(:install) if node['platform_family'] == "debian"
# install mysql gem
chef_gem "mysql"


# Additionally, fix mysql root password (mysql-server uses an 
# empty password by default under Ubuntu)
node.set['mysql']['server_root_password'] = ""

Patch Script 'pre__storm__default'

# Search query fails due to missing role name, and download URL
node.set['storm']['cluster_role'] = "stormCluster"
node.set['storm']['download_url'] = "http://dsg.tuwien.ac.at/staff/hummer/tmp/"

Patch Script 'pre__kafka__default'

# Need to provide a version and download URL
node.set[:kafka][:version] = "kafka-0.7.2-incubating-src"
node.set[:kafka][:download_url] = "http://www.us.apache.org/dist/incubator/kafka/kafka-0.7.2-incubating/"

Patch Script 'pre__aegir__default'

# Recipe aegir::default attempts to notify service "php5-fpm", which is 
# actually defined under the name "php-fpm" in cookbook php-fpm::default.
# --> hence, define it under the new name here!
php_fpm_service_name = "php5-fpm"
if platform_family?("rhel")
  php_fpm_service_name = "php-fpm"
end
service "php5-fpm" do
  service_name php_fpm_service_name
  supports :start => true, :stop => true, :restart => true, :reload => true
  action [ :enable, :restart ]
end

Patch Script 'pre__pxe_dust__default'

# Dependencies for recipe "pxe_dust::default"

# Create Chef data_bags directory, if it does not yet exist (fixes an
# issue where Chef was complaining that /var/chef/data_bags/.. does not exist)
require 'fileutils'
databags_dir = "/var/chef/data_bags/pxe_dust"
FileUtils.mkpath(databags_dir) if !File.directory?(databags_dir)
databag_file = File.join(databags_dir, "default.json")
`echo '{"id":"default"}' > #{databag_file}`

Patch Script 'post__drupal__default'

#
# At the time of writing, the drupal::default recipe fails because 
# server_aliases has to be an array (and not a string, as below), 
# otherwise the Apache2 config file templating fails.
# This post-processing recipe should fix the bug.
#

web_app "drupal" do
  template "drupal.conf.erb"
  docroot node['drupal']['dir']
  server_name server_fqdn
  # original
  #server_aliases node['fqdn']
  # fixed
  server_aliases [node['fqdn']]
end

Patch Script 'post__virtualbox__default'

#
# bug fix for virtualbox::default recipe, which calls a 
# resource "add Oracle key" that does not exist..
#

template "/etc/apt/sources.list.d/oracle-virtualbox.list" do
  source "oracle-virtualbox.list.erb"
  mode 0644
  # original:
  #notifies :run, resources(:bash => "add Oracle key"), :immediately
  # fixed (removed)
end

Patch Script 'post__cakephp__default'

#
# server_aliases has to be an array (and not a string, as below), 
# otherwise the Apache2 config file templating fails.
#

app = web_app "cakephp" do
  template "cakephp.conf.erb"
  docroot "#{node[:cakephp][:dir]}/app/webroot"
  server_name server_fqdn
  # original:
  #server_aliases node.fqdn
  # fixed:
  server_aliases [node.fqdn]
end

# this is required, otherwise Chef fills in the wrong cookbook/recipe name
app.cookbook_name = "cakephp"
app.recipe_name = "default"

# Fix mysql command for use with empty password.
execute "mysql-install-cakephp-privileges" do
  # original:
  #command "/usr/bin/mysql -u root -p#{node[:mysql][:server_root_password]} < /etc/mysql/cakephp-grants.sql"
  # fixed:
  command "/usr/bin/mysql -u root #{node['mysql']['server_root_password'].empty? ? '' : '-p' }#{node[:mysql][:server_root_password]} < /etc/mysql/cakephp-grants.sql"
  action :nothing
end

Patch Script 'post__cube__default'

#
# Here, we only want to reduce the verbosity of "warning" 
# outputs during compilation of node.js.
#

bash "compile node.js" do
  cwd "/usr/local/src/node-v#{node['nodejs']['version']}"
  code <<-EOH
    # original:
    #./configure --prefix=#{node['nodejs']['dir']} && \
    #make
    # fixed:
    ./configure --prefix=#{node['nodejs']['dir']} && \
    make | grep -v "warning:"
  EOH
  creates "/usr/local/src/node-v#{node['nodejs']['version']}/node"
end

execute "nodejs make install" do
  # original:
  #command "make install"
  # fixed:
  command "make install | grep -v 'warning:'"
  cwd "/usr/local/src/node-v#{node['nodejs']['version']}"
  not_if {File.exists?("#{node['nodejs']['dir']}/bin/node") && `#{node['nodejs']['dir']}/bin/node --version`.chomp == "v#{node['nodejs']['version']}" }
end

Patch Script 'post__icinga__default'

# Avoid EnclosingDirectoryDoesNotExist exception.
# --> add "recursive=true" parameter
directory "#{node['icinga']['config_dir']}" do
  owner node['icinga']['user']
  group node['icinga']['group']
  mode "0755"
  # new:
  recursive true
end

Patch Script 'post__node__default'

#
# At the time of writing, this recipe fails because curl cannot download
# the given file (curl does not follow HTTP redirects by default). 
# This post-processing recipe should fix the bug.
#

bash "install_npm" do
  user "root"
    cwd "/tmp/"
    code <<-EOH
    # original:
    # curl http://npmjs.org/install.sh | clean=no sh
    # fixed:
    curl -L http://npmjs.org/install.sh | clean=no sh
    EOH
end

Patch Script 'post__wordpress__default'

#
# If the root passwort is empty, the "-p" option should NOT be 
# passed to the mysql command.
#

execute "mysql-install-wp-privileges" do
  # original:
  #command "/usr/bin/mysql -u root -p\"#{node['mysql']['server_root_password']}\" < #{node['mysql']['conf_dir']}/wp-grants.sql"
  # fixed:
  command "/usr/bin/mysql -u root #{node['mysql']['server_root_password'].empty? ? '' : '-p' }\"#{node['mysql']['server_root_password']}\" < #{node['mysql']['conf_dir']}/wp-grants.sql"
  action :nothing
end