8

Can someone please explain to me how chef works? That is a pretty broad question, so to narrow it down I have this very simple recipe that loops over a list of users and creates each one if they do not already exist. It does not work.

From what I can tell the loop seems to be happening as I would expect. Once the loop has completed my bash commands to create each user are executed, once for each iteration in the loop. However, when the bash commands are executed they only seem to have the user value from the first loop iteration.

What is the correct way to write a recipe that loops over variable data similar to this example?

Here is the recipe:

node[:users].each do |user|
  puts "in loop for #{user['username']}"
  bash "create_user" do
    user "root"
    code do
      puts "running 'useradd' for #{user['username']}"
      "useradd #{user['username']}"
    end
    not_if do
      puts "checking /etc/passwd for #{user['username']}"
      "cat /etc/passwd | grep #{user['username']}"
    end
  end
end

I'm testing this using Vagrant with the following setup:

Vagrant::Config.run do |config|
  config.vm.box = "precise32"
  config.vm.box_url = "http://files.vagrantup.com/precise32.box"
  config.vm.provision :chef_solo do |chef|
    chef.add_recipe "sample"
    chef.json = {
      :users => [
        {:username => 'testA'},
        {:username => 'testB'},
        {:username => 'testC'},
        {:username => 'testD'},
        {:username => 'testE'},
      ],
    }
  end
end

The messages that are generated by the puts statements in the recipe look like this:

2013-03-08T01:03:46+00:00] INFO: Start handlers complete.
in loop for testA

in loop for testB

in loop for testC

in loop for testD

in loop for testE

[2013-03-08T01:03:46+00:00] INFO: Processing bash[create_user] action run (sample::default line 5)
checking /etc/passwd for testA

[2013-03-08T01:03:46+00:00] INFO: Processing bash[create_user] action run (sample::default line 5)
checking /etc/passwd for testA

[2013-03-08T01:03:46+00:00] INFO: Processing bash[create_user] action run (sample::default line 5)
checking /etc/passwd for testA

[2013-03-08T01:03:46+00:00] INFO: Processing bash[create_user] action run (sample::default line 5)
checking /etc/passwd for testA

[2013-03-08T01:03:46+00:00] INFO: Processing bash[create_user] action run (sample::default line 5)
checking /etc/passwd for testA

[2013-03-08T01:03:46+00:00] INFO: Chef Run complete in 0.026071 seconds
Matthew J Morrison
  • 165
  • 1
  • 1
  • 8

2 Answers2

10

The behaviour you're seeing can be explained by understanding the difference between two of the key stages in a Chef client run: compilation, and convergence.

During the "compile" phase, the Chef client runs the code in your recipes to build a resource collection. This is a list of the Resources that you've told Chef to manage on your system, along with their target state. For example, a Directory resource to say that /tmp/foo should exist, and be owned by root:

directory "/tmp/foo" do
  owner "root"
end

During the "converge" phase, the Chef client uses providers to load the current state of each resource, then compares this to the target state. If they're different, Chef will update the system. For our Directory resource, Chef would create the directory if it didn't exist, and change its owner to "root" if necessary.

Resources are uniquely identified by their name and type - our Directory would be directory[/tmp/foo]. Strange things will happen when you have two resources with the same name but different attributes - that explains your problem, and it can be fixed using Darrin Holst's answer:

node[:users].each do |user|
  puts "in loop for #{user['username']}"
  bash "create_user_#{user}" do
    user "root"
    code do
      puts "running 'useradd' for #{user['username']}"
      "useradd #{user['username']}"
    end
    not_if do
      puts "checking /etc/passwd for #{user['username']}"
      "cat /etc/passwd | grep #{user['username']}"
    end
  end
end

However, in this particular case, you would benefit from using Chef's User resource. Here's a replacement for your recipe (without the debugging messages):

node[:users].each do |u|
  user u['username'] do
    action :create
  end
end

Why is this better than a set of bash resources?

  1. The User resource works the same way across various platforms - the same recipe will work on operating systems that use something other than "useradd" to create users.
  2. The providers responsible for this know how to check whether the user already exists, so you don't need not_if.
  3. The same providers can also be used to remove users, lock or unlock their passwords, and update other attributes on existing user accounts.

But the best reason to use the right resources is that it more clearly communicates your intent. Your goal probably isn't to run a bunch of shell commands - it's to ensure that some users are present on your system. The commands used to do that are just an implementation detail.

Providers encapsulate those details, leaving us free to focus on describing what we want.

zts
  • 945
  • 5
  • 8
  • 1
    Thanks, this clarifies things quite a bit. The user creation recipe was just an example that I could play with to better understand why the loops that I had elsewhere did not work as I expected. – Matthew J Morrison Mar 08 '13 at 13:35
5

make your script name unique...

bash "create_user_#{user}" do

FWIW, I've used https://github.com/fnichol/chef-user several times which allows you to create/remove users based on attributes and databags.

Darrin Holst
  • 166
  • 2
  • That seems so simple, thanks! The creating users recipe was just an example I was using to get my head around why my loop wasn't working in other recipes. Thanks again! – Matthew J Morrison Mar 08 '13 at 13:30