Chef:Powerful Infrastructure Automation
上QQ阅读APP看书,第一时间看更新

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

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

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

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 >

Interacting with data

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 your data

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]]

Editing your data

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.

Transforming data

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 >.

Creating a recipe in the shell

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.

Defining node attributes

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

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" 

Accessing attributes

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.

Using configuration blocks

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.

Interactively executing recipes

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.

Using the breakpoint resource

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.