Getting to know the Chef shell
The Chef shell, previously called shef
, provides an interactive tool or read-eval-print-loop (REPL) to work with Chef resources. Much in the same way IRB or any other language's REPL shell works, chef-shell
is a way to interact with knife
. This is handy for experimenting with resources while writing recipes so that you can see what happens interactively rather than having to upload your cookbook to a server and then executing the chef-client on a target node. Additionally, the Chef shell provides a resource to add breakpoints to recipe execution so that it can be used to debug recipe execution, which is a very handy feature.
Using the Chef shell
As of 11.x, shef
has been replaced with chef-shell
and can be used in three different modes: standalone, solo, and client mode. Each of these has a slightly different set of functionalities and expected use cases.
The standalone mode is used to run Chef in an interactive mode with nothing loaded; this is almost like running an REPL such as irb
or python
on the command line. This is also the default behavior of chef-shel
l
if nothing is specified.
The solo mode is invoked using the -s
or --solo
command-line flag and is a way to use chef-shell as a chef-solo client. It will load any cookbooks using the same mechanism that chef-solo users would, and it will use any chef-solo JSON file provided to it using the -j
command-line option.
The following are examples of using the solo mode:
chef-shell -s chef-shell -s -j /home/myuser/chef/chef-solo.json
The client mode is enabled with the -z
or --client
command-line flag; this mode causes chef-shell
to act as though you invoked chef-client on the host. The shell will read the local client configuration and perform the normal duties of chef-client: connecting to your Chef server and downloading any required run lists, attributes, and cookbooks. However, it will allow for interactive execution so that it is possible to debug or diagnose issues with recipes on the endhost. When using the client mode, you can use an alternate configuration file with the -c
command-line option, or specify a different Chef server URL via the -s
command-line option.
The example uses the following:
chef-shell --client -c /etc/chef/alternate.conf chef-shell --client -s http://test.server.url:8080/
Interacting with the Chef server using the shell
The Chef shell provides you with the ability to interact with the server quickly in the same way you would use knife
, but without the overhead of typing knife search node... or knife node list, and so on. It is a very convenient way to query the data stored in the Chef server interactively. In order to interact with the server from your workstation, you need to make sure that your shell's configuration file, located in ~/.chef/chef_shell.rb
, is configured properly. If you are connecting with chef-shell
from a node, then the configuration in /etc/chef/client.rb
(or similar on Windows) will be used instead.
This file, similar to the knife.rb
or client.rb
file, contains the required certificate data and configuration data to connect to the Chef server. An example configuration file will resemble the following, with paths, organization, and client names updated accordingly:
node_name 'myorg' client_key File.expand_path('~/.chef/client.pem') validation_key File.expand_path('~/.chef/validator.pem') validation_client_name "myorg-validator" chef_server_url 'https://api.opscode.com/organizations/myorg'
All of these files are present if your knife
installation is operational, and the configuration file closely resembles that of knife.rb
—if you need values for these on your workstation, take a look at the ~/.chef/knife.rb
file. Once you have configured your shell, you can pass the -z
command-line flag to connect as the chef-client would:
[jewart]% chef-shell -z loading configuration: /Users/jewart/.chef/chef_shell.rb Session type: client Loading......resolving cookbooks for run list: [] Synchronizing Cookbooks: done. This is the chef-shell. Chef Version: 11.12.8 http://www.opscode.com/chef http://docs.opscode.com/ run 'help' for help, 'exit' or ^D to quit. Ohai2u jewart@! chef >
From here, you can interact with the Chef server in a variety of ways, including searching, modifying, and displaying any data elements (roles, nodes, data bags, environments, cookbooks, and clients), performing a client run (including stepping through it, one step at a time), assuming the identity of another node, and printing the attributes of the local node. For example, listing the roles on the Chef server can be performed with the roles.all
method, shown as follows:
chef > roles.all => [role[umbraco_cms], role[umbraco], role[base_server], role[web_server], role[postgresql_server]]
Searching the data elements is also supported, as each data type has a find
method attached to it. The find
method takes a map of the attribute and pattern to look for and returns the results. For example, you can find all roles on the Chef server that begin with "um"
with the following command:
chef > umbraco_roles = roles.find(:name => "um*") => [role[umbraco_cms], role[umbraco]]
Any object in your Chef server can be edited directly from the Chef shell using the edit
command from inside the shell. This will invoke your favorite editor to edit the raw JSON of the object in question, which provides a more direct mechanism over using knife (node|role|data bag) edit
on the command line, as you can quickly manipulate a number of records a lot more easily. For example, to edit all of the roles that contain the name "apache"
and save the results, you can use the following Ruby code:
chef > apache_roles = roles.find(:name => "*apache*") > [... some list... ] chef> apache_roles.each do |r| chef> updated = edit r chef> updated.save chef> end
This will find all roles whose name contains "apache"
. Then for each record, edit the JSON, storing the results in variable named updated
, and then save that record back to the Chef server.
In this way, you can interact with any of the resources that are available to you, allowing you to quickly find and manipulate any data stored in Chef directly using the Ruby code. For example, to find all clients with a given string in their name and disable their administrative access, you can use the following code:
clients.transform("*:*") do |client| if client.name =~ /bad_user/i client.admin(false) true else nil end end
Tip
Use caution
when transforming your data from the Chef shell; it is an incredibly powerful tool, but its effects are destructive. These changes are not reversible (at least not without forethought or backups) and could damage your data if you are not careful. For example, if the previous code was transcribed incorrectly, it could potentially render all users unable to administer the system.
Executing recipes with Chef shell
Two great features of chef-shell
are the ability to rewind a run (to step backwards) and to be able to step forward in the run one resource at a time. As an example, let's look at how to define a simple recipe in chef-shell
interactively and then run it, start it over, and step through it.
First, let's fire up chef-shell
with the following command:
[jewart]% chef-shell loading configuration: none (standalone session) Session type: standalone Loading......done. This is the chef-shell. Chef Version: 11.12.8 http://www.opscode.com/chef http://docs.opscode.com/ run 'help' for help, 'exit' or ^D to quit. Ohai2u jewart@! chef >
The chef-shell
prompt will change based on the state you are in. If you are working with a recipe, the prompt will change to be chef:recipe >
.
The Chef shell has a number of modes—recipe mode and attribute mode. Recipe mode is activated when working with recipes and will be what we use here. In order to activate it, type recipe_mode
at the prompt:
chef > recipe_mode chef:recipe >
Here, we will create resources to create a file in the current directory interactively using a file
resource with no associated configuration block, only the name:
chef:recipe > file "foo.txt" => <file[foo.txt] @name: "foo.txt" @noop: nil @before: nil @params: {} @provider: Chef::Provider::File @allowed_actions: [:nothing, :create, :delete, :touch, :create_if_missing] @action: "create" @updated: false @updated_by_last_action: false @supports: {} @ignore_failure: false @retries: 0 @retry_delay: 2 @source_line: "(irb#1):1:in 'irb_binding'" @guard_interpreter: :default @elapsed_time: 0 @resource_name: :file @path: "foo.txt" @backup: 5 @atomic_update: true @force_unlink: false @manage_symlink_source: nil @diff: nil @sensitive: false @cookbook_name: nil @recipe_name: nil>
Tip
One thing to note here is that the shell will print out the results of the last operation executed in the shell. This is part of an REPL shell's implicit behavior; it is the print part of REPL: input is read and evaluated, then the results are printed out, and the shell loops to wait for more input from the user. This can be controlled by enabling or disabling the echo state; echo off
will prevent the printed output and echo on
will turn it back on.
It is critical to note that, at this point, nothing has been executed; we have only described a file
resource that will be acted upon if the recipe is run. You can verify this by making sure that there is no file named foo.txt
in the directory you executed chef-shell
from. The recipe can be run by issuing the run_chef
command, which will execute all of the steps in the recipe from start to finish. Here is an example of this:
chef:recipe > run_chef INFO: Processing file[foo.txt] action create ((irb#1) line 1) DEBUG: touching foo.txt to create it INFO: file[foo.txt] created file foo.txt DEBUG: found current_mode == nil, so we are creating a new file, updating mode DEBUG: found current_mode == nil, so we are creating a new file, updating mode DEBUG: found current_uid == nil, so we are creating a new file, updating owner DEBUG: found current_gid == nil, so we are creating a new file, updating group DEBUG: found current_uid == nil, so we are creating a new file, updating owner INFO: file[foo.txt] owner changed to 501 DEBUG: found current_gid == nil, so we are creating a new file, updating group INFO: file[foo.txt] group changed to 20 DEBUG: found current_mode == nil, so we are creating a new file, updating mode INFO: file[foo.txt] mode changed to 644 DEBUG: selinux utilities can not be found. Skipping selinux permission fixup.
Just as in any recipe, attributes can be used in the recipes defined in the shell. However, in the standalone mode, there will be no attributes defined initially; solo and client modes will likely have attributes defined by their JSON file or the Chef server, respectively. In order to interact with the currently defined attributes, we must switch between the recipe mode and attribute mode. This is achieved using the attributes_mode
command as shown in the following code:
chef:recipe > attributes_mode chef:attributes >
Here we can perform two primary operations: getting and setting node attributes. These are ways of modifying the values that are accessed from inside the node
Mash in a recipe.
Tip
Remember that the node's attributes are accessed as a Mash, a key-insensitive hash that allows you to interchange string keys with symbol keys. The Mash class is not a built-in structure in Ruby—it is provided by Chef for convenience so that hash keys can be either symbols or strings and have the same effect.
Setting attributes is achieved using the set
command, which has the following form:
set[:key] = value
Here, :key
can be a single-level key or a multilevel key similar to any entry in the attributes/default.rb
file. As an example, we can construct an application configuration using the following:
set[:webapp][:path] = "/opt/webapp" set[:webapp][:db][:username] = "dbuser" set[:webapp][:db][:password] = "topsecret" set[:webapp][:user] = "webuser" set[:postgresql][:config][:listen] = "0.0.0.0"
Any parent keys that are non-existent are implicitly created on the fly, so you do not need to do something like the following:
set[:webapp] = {} set[:webapp][:path] = "/opt/webapp"
In order to display an attribute when in the attributes mode, simply type in the name of the key you are interested in. For example, if you had executed the set commands listed previously, then asking for the webapp
hash is as simple as typing webapp
, as follows:
chef:attributes > webapp => {"path"=>"/opt/webapp", "db"=>{"username"=>"dbuser", "password"=>"topsecret"}, "user"=>"webuser"}
However, if you wish to access these when in the recipe mode, they are accessed through the node
hash, as shown here:
chef:attributes > recipe_mode => :attributes chef:recipe > node[:webapp] => {"path"=>"/opt/webapp", "db"=>{"username"=>"dbuser", "password"=>"topsecret"}, "user"=>"webuser"}
They can be used via the node
hash in just the same way you would use them in a recipe. If you want to construct a file block that created a foo.txt
file located in the install path of our webapp
hash, you can easily use the following example inside your shell:
file "#{node[:webapp][:path]}/foo.txt"
This makes writing recipes using the interactive shell feel exactly the same as writing recipe files.
A resource in a recipe file can have a Ruby block with attributes, and you can do this in chef-shell
in exactly the same fashion. Simply insert do
after the resource name and the shell will behave as a multiline editor, allowing you to complete the block. The following example demonstrates providing a content attribute to a file
resource in this manner:
chef:recipe > file "not_empty.txt" do chef:recipe > content "Not empty!" chef:recipe ?> end => <file[not_empty.txt] @name: "not_empty.txt" @noop: nil @before: nil @params: {} @provider: Chef::Provider::File @allowed_actions: [:nothing, :create, :delete, :touch, :create_if_missing] @action: "create" @updated: false @updated_by_last_action: false @supports: {} @ignore_failure: false @retries: 0 @retry_delay: 2 @source_line: "(irb#1):2:in 'irb_binding'" @guard_interpreter: :default @elapsed_time: 0 @resource_name: :file @path: "not_empty.txt" @backup: 5 @atomic_update: true @force_unlink: false @manage_symlink_source: nil @diff: nil @sensitive: false @cookbook_name: nil @recipe_name: nil @content: "Not empty!">
Note that when the shell printed out the previous file
resource, @content
was not present. Here, everything but the name remains the same, and there is an additional property inside the object, @content
, as specified in our attributes block.
Running a recipe step by step is a good way of slowing down the execution of a recipe so that the state of the system can be inspected before proceeding with the next resource. This can be incredibly useful both for debugging (as will be discussed later) and for developing and exploring resources. It gives you a chance to see what has happened and what side effects your recipe has as the recipe is executed. To achieve this, the Chef shell allows you to rewind your recipe to the start and run from the beginning, execute your recipe one step at a time, and resume execution from the current point to the end.
Restarting our Chef shell, let's take a look at how we can use this:
recipe_mode echo off file "foo.txt" file "foo.txt" do action :delete end file "foo.txt" do content "Foo content" end
Here our recipe is quite simple—create an empty file, foo.txt
, remove it, and then recreate it with "Foo content". If we execute our recipe using run_chef
, the shell will perform all the operations in one pass without stopping and will not allow us to check whether the delete action occurred. Instead, we can run our recipe and then rewind and use the chef_run.step
method to interactively walk through our recipe:
chef:recipe > run_chef ... execution output ... chef:recipe > echo on => true chef:recipe > chef_run.rewind => 0 chef:recipe > chef_run.step INFO: Processing file[foo.txt] action create ((irb#1) line 3) => 1 chef:recipe > chef_run.step INFO: Processing file[foo.txt] action delete ((irb#1) line 4) INFO: file[foo.txt] backed up to /var/chef/backup/foo.txt.chef-20140615175124.279917 file[foo.txt] deleted file at foo.txt => 2 chef:recipe > chef_run.step INFO: Processing file[foo.txt] action create ((irb#1) line 7) INFO: file[foo.txt] created file foo.txt INFO: file[foo.txt] updated file contents foo.txt INFO: file[foo.txt] owner changed to 501 INFO: file[foo.txt] group changed to 20 INFO: file[foo.txt] mode changed to 644 => 3
As you can see, here we were able to rewind our recipe back to the first instruction (position 0, as the result of chef_run.rewind
indicates), and then walk through each resource step by step using chef_run.step
and see what happened. During this run, you can easily open a terminal after you rewind the recipe, delete the foo.txt
file from the previous run, and check that initially there is no foo.txt
file, then step through the next command in the recipe, validate that there is an empty foo.txt
file, and so on. This is a very good way to learn how resources work and to see what they do without having to formalize your recipe in a cookbook, provision and bootstrap a host, and so on.
Debugging with the Chef shell
Debugging is achieved in two different ways using chef-shell
: stepping interactively through a recipe or using a special breakpoint resource that is only used by chef-shell
. Running recipes interactively step by step is good to build recipes locally; experiment with resources to determine the effect of certain attributes, actions and notifications; or to inspect the state of the system after each resource has been acted upon. The breakpoints allow you to inject very specific stopping points into the client run so that the world can be inspected before continuing. Typically, once a breakpoint is encountered, you will want to step through the execution of your script (at least for a while) so that these are not mutually exclusive techniques.
The breakpoint resource is structured just like any other Chef resource. The resource's name attribute is the location where you want to insert the breakpoint, and it has only one action, :break
, which signals chef-shell
to interrupt execution of the current recipe and provide an interactive shell. Any breakpoint resources in recipes are ignored by the chef-client. That way, if they are forgotten about and left in a recipe, they will not cause havoc in production. That being said, they should only be used when actively debugging an issue and removed before releasing your recipes into your production environment.
The name attribute has the following structure:
when resource resource_name
Here, when
has the value of "before" or "after", to indicate whether the breakpoint should stop before or after execution, respectively and resource
is the type of resource that when combined with resource_name
is the unique identifier that will trigger the breakpoint. For example:
before file '/tmp/foo.txt'
This would cause the shell to interrupt execution of the recipes immediately before any file resource that was manipulating /tmp/foo.txt
. Another example, where we want to stop execution after installing the git
package, would look like the following:
after package 'git'
Using this, we will tell chef-shell
that execution was to be paused once the git
package was modified. Let's look at how we can form a simple recipe complete with breakpoint resources that would use these examples:
breakpoint "before file '/tmp/foo.txt'" do action :break end breakpoint "after package 'git'" do action :break end file '/tmp/foo.txt' do action :create end package 'git' do action :remove end
For those who have used gdb
or any other debugger, this will be easy to understand; if you have not used an interactive debugger, then try a few of the interactive examples, and you will get the hang of it in no time at all.
Chef shell provides a comprehensive way to interact with your recipes. Now that you see how to test out and debug your work, let's take a look at how we can go one step further in our testing to perform full end-to-end integration testing of our infrastructure.