Tuesday 3 January 2017

Chapter 8. Attributes

Attributes represent information about your node. In addition to the information that can be automatically generated by ohai, you can set attributes in Chef recipes or in separate attribute files.
Attribute files are located in the attributes folder of a cookbook. Similar to recipes, the default attribute file is called:
<cookbook>
└── attributes
    └── default.rb
Figure 8-1 shows the format of an attribute when it is specified in a cookbook attribute file.
Setting attributes in attribute files
Figure 8-1. Setting attributes in attribute files
Attributes can also be set directly in recipes. Figure 8-2 shows the format of an attribute when it is set in a recipe. You must precede the attribute name with node. when you set an attribute directly in a recipe.
Setting attributes in recipes
Figure 8-2. Setting attributes in recipes
Because attributes can be defined in multiple places, all attribute values are composed together during a Chef run according to the priority levels as shown in Figure 8-3. Attributes defined by ohai have the highest priority, followed by attributes defined in a recipe, then attributes defined in an attribute file. In other words, recipe attributes have a higher priority than those defined in attribute file, and will override them by default. Attributes defined by ohai trump everything else.
Attribute priority
Figure 8-3. Attribute priority
We’ll talk more about precedence levels later in this chapter. However, when setting an attribute in a cookbook, it should (almost) always be a default attribute with default precedence.

Motd-Attributes Cookbook

Let’s experiment with attributes in a cookbook called motd-cookbook. This is an attribute-driven version of the motd cookbook we created in Chapter 7. We’re going to go through the cookbook creation steps quickly in this chapter. If you need a refresher on what each of these commands mean and the expected output, refer back to Chapter 7.
First, generate the motd-attributes cookbook using either the chef generate cookbook command in the Chef Development Kit or the knife cookbook create command in Chef Client, depending on which is installed on your Chef Development workstation.
Chef Development Kit:
$ chef generate cookbook motd-attributes
$ cd motd-attributes
Chef Client:
$ knife cookbook create motd-attributes --cookbook-path .
$ cd motd-attributes
$ kitchen init --create-gemfile
$ bundle install
Next, modify the .kitchen.yml file to use our favorite box image, as shown in Example 8-1.
Example 8-1. chefdk/motd-attributes/.kitchen.yml
---
driver:
  name: vagrant

provisioner:
  name: chef_zero

platforms:
  - name: centos65
    driver:
      box: learningchef/centos65
      box_url: learningchef/centos65

suites:
  - name: default
    run_list:
      - recipe[motd-attributes::default]
    attributes:
Create a recipe that uses the template resource to generate a /etc/motd file, but this time, we’ll use some attributes in the template file. The initial recipes/default.rb file you create should resemble Example 8-2.
Example 8-2. chefdk/motd-attributes/recipes/default.rb
#
# Cookbook Name:: motd-attributes
# Recipe:: default
#
# Copyright (C) 2014
#
#
#

template '/etc/motd' do
  source 'motd.erb'
  mode '0644'
end
Generate a template file to generate /etc/motd.
Chef Development Kit:
$ chef generate template motd
Chef Client - Linux/Mac OS X:
$ touch templates/default/motd.erb
Chef Client - Windows:
$ touch templates\default\motd.erb
Create a template /etc/motd that uses some node attributes generated by ohai, as shown in Example 8-3.
Example 8-3. chefdk/motd-attributes/templates/default/motd.erb
The hostname of this node is <%= node['hostname'] %>
The IP address of this node is <%= node['ipaddress'] %>
Perform a kitchen converge to apply the cookbook to your node, and then when you run kitchen login it should display the following, with the expected values for node["ipaddress"] and node["hostname"]. (Reminder: Once you have issued these commands and verified the output is correct, make sure you run the exitcommand to get back to the host prompt directory with your cookbook source.)
$ kitchen converge
$ kitchen login
Last login: Sun Jul 20 19:40:55 2014 from 10.0.2.2
The hostname of this node is default-centos65
The IP address of this node is 10.0.2.15
[vagrant@default-centos65 ~]$ exit
logout
Connection to 127.0.0.1 closed.

NOTE

If the last line with the IP address is missing, even though it is clearly in the motd.erb, add an extra newline in your text editor at the end of the file. The message of the day won’t display the last line if it does not have a carriage return in the text.

Setting Attributes

Now, let’s try setting some attributes in our cookbook. First, generate a default attributes file.
Chef Development Kit:
$ chef generate attribute default
Chef Client - Linux/Mac OS X:
$ touch attributes/default.rb
Chef Client - Windows:
$ touch attributes\default.rb
By default, the Chef Development Kit does not create an attributes directory until you tell it to generate an attribute file. Chef Client, on the other hand, always creates the directory, but leaves it up to you to create the default.rb attribute file by hand.
Now, let’s set an attribute in our attributes file following the form outlined in Figure 8-1. Before you edit attributes/default.rb the file will have no content, as shown in Example 8-4.
Example 8-4. chefdk/motd-attributes/attributes/default.rb
default['motd-attributes']['company'] = 'Chef'
By convention, when attributes are set in a cookbook’s attribute file, the values are expected to be namespacedunder a top-level key matching the cookbook name. Then all the key/value pairs are contained within the top-level key; for example, the default['motd-attributes']['company'] value is the string 'Chef' in this example, as the cookbook name in the metadata.rb file is motd-attributes. Also, following the form outlined in Figure 8-1, the attribute uses the default precedence level.
Let’s also set an attribute value in our recipe, as seen in Example 8-5, following the form in Figure 8-2.
Example 8-5. chefdk/motd-attributes/recipes/default.rb
node.default['motd-attributes']['message'] = "It's a wonderful day today!"

template '/etc/motd' do
  source 'motd.erb'
  mode '0644'
end
Update the motd.erb template as shown in Example 8-6. You can access attributes from any source under the node object: attribute file values, values set in recipes, or values set automatically by ohai. They’re all just the corresponding node values.
Example 8-6. chefdk/motd-attributes/templates/default/motd.erb
Welcome to <%= node['motd-attributes']['company'] %>
<%= node['motd-attributes']['message'] %>
The hostname of this node is <%= node['hostname'] %>
The IP address of this node is <%= node['ipaddress'] %>
Performing another Chef run and checking the message of the day should produce the following output (again, make sure to exit back out to the host prompt):
$ kitchen converge
$ kitchen login
Last login: Sun Jul 20 19:52:53 2014 from 10.0.2.2
Welcome to Chef
It's a wonderful day today!
The hostname of this node is default-centos65
The IP address of this node is 10.0.2.15
[vagrant@default-centos65 ~]$ exit
logout
Connection to 127.0.0.1 closed.

Basic Attribute Priority

Now let’s experiment with the basics of attribute priorities by trying to reset values set elsewhere. As shown in Example 8-7, modify the recipe/default.rb so that it tries to reset the value of a higher priority automatic attribute set by ohai, and a lower priority attribute defined in the attribute file.
Example 8-7. chefdk/motd-attributes/recipes/default.rb
node.default['ipaddress'] = '1.1.1.1'
node.default['motd-attributes']['company'] = 'My Company'
node.default['motd-attributes']['message'] = "It's a wonderful day today!"

template '/etc/motd' do
  source 'motd.erb'
  mode "0644"
end
Perform a Chef run and check to see the resulting values:
$ kitchen converge
$ kitchen login
Last login: Sun Jul 20 20:05:38 2014 from 10.0.2.2
Welcome to My Company
It's a wonderful day today!
The hostname of this node is default-centos65
The IP address of this node is 10.0.2.15
[vagrant@default-centos65 ~]$ exit
logout
Connection to 127.0.0.1 closed.
You should notice:
  • The node['motd-attributes']['company'] value set in the recipe ‘My Company’ has a higher prioritythan the value ‘Chef’ set in the attribute file, so this is the value displayed in the template.
  • The node['ipaddress'] value set in the recipe '1.1.1.1' has a lower priority than the automatic value 10.0.2.15 set by ohai, so the value set in the recipe is ignored, and the template displays the higher priority value.
These priorities represent how the variables are intended to be used. Values set in an attribute file are intended to be defaults that can be overridden by a recipe. On the other hand, the automatic values set by ohai should never be overridden as they represent important information about a system, such as the fact that it shouldn’t be easy to reset the node’s IP address, for example.

Include_Recipe

You might wonder why there is a need for this priority mechanism with Chef attributes. It’s because like in most other programming languages, a Chef recipe can reference other Chef recipe files using an “include” statement: include_recipe. So when your Chef code is processed during a Chef run, it could possibly include a chain of references to multiple recipe files that might even be in other cookbooks, as shown in Figure 8-4. Because you can use include_recipe, Chef code might contain conflicting attribute assignments, and there needs to be some guidelines for how these conflicts are resolved.
include_recipe could include a chain of references
Figure 8-4. include_recipe could include a chain of references

NOTE

A rule of thumb many Chef coders find useful is that a recipe file shouldn’t be longer than a “screenful” of code—between one dozen and two lines. Anything longer gets difficult to understand at a glance because you have to scroll up or down to see all the code. When your recipe code starts getting this long, consider breaking it up into multiple recipe files, stitching them together with include_recipestatements.
In order to use include_recipe, you reference a recipe in the exact same form that you reference a recipe in a run list, in the form "<cookbook>::<recipe>". For example, "motd-attributes::message".
Let’s add an include_recipe statement to our motd-attributes cookbook. Generate a new recipe called message.
Chef Development Kit:
$ chef generate recipe message
Chef Client - Linux/Mac OS X:
$ touch recipes/message.rb
Chef Client - Windows:
$ touch recipes\message.rb
Now edit the recipes/message.rb file containing the recipe as shown in Example 8-8. The message recipe will be used to set additional values related to the message of the day.
Example 8-8. chefdk/motd-attributes/recipes/message.rb
node.default['motd-attributes']['company'] = 'the best company in the universe'

TIP

Note that our .kitchen.yml file includes only a reference to the default recipe in its run list. We never intend for external consumers of our cookbook to refer to this message recipe directly. We’re just using the message recipe to organize our code, and it is assumed that include_recipe will be used inside the default recipe to include any other necessary code:
...
  run_list:
    - recipe[motd-attributes::default]
...
Some Chef coders use the convention of putting an underscore prefix (“_”) in the name of recipes that are “private”—recipes used merely to organize Chef code into smaller, more understandable chunks. They name the recipe file "_message.rb" to make this intent more clear in the cookbook file structure.
Also, you need to add an include_recipe statement to your default.rb. The include_recipe statement ensures that the message.rb file gets processed during the Chef run. Otherwise, only the recipe file default.rb will be evaluated when the run list is motd-attributes::default. We won’t remove any of the attribute values we set earlier, in order to illustrate a few more attribute priority concepts.
Example 8-9. chefdk/motd-attributes/recipes/default.rb
#
# Cookbook Name:: motd-attributes
# Recipe:: default
#
# Copyright (C) 2014
#
#
#

node.default['ipaddress'] = '1.1.1.1'
node.default['motd-attributes']['company'] = 'My Company'
node.default['motd-attributes']['message'] = "It's a wonderful day today!"

include_recipe 'motd-attributes::message'

template '/etc/motd' do
  source 'motd.erb'
  mode '0644'
end
include_recipe statements can be present anywhere in a recipe file. When the Chef code is evaluated, the include_recipe statement is replaced with an expansion of the recipe code that is referenced, as shown in Figure 8-5.
Chef expands any include_recipe references
Figure 8-5. Chef expands any include_recipe references
Perform a kitchen converge and inspect the resulting message. When there is a duplicate attribute value set at the same priority level, the last attribute value setting wins. In this case, with the include_recipeexpansion, node.default["motd-attributes"]["company"] is set twice; the last value set before the template resource is what is used—“the greatest company in the universe”:
$ kitchen converge
$ kitchen login
Last login: Mon Jul 21 11:34:05 2014 from 10.0.2.2
Welcome to the best company in the universe
It's a wonderful day today!
The hostname of this node is 10.0.2.15
The IP address of this node is default-centos65
[vagrant@default-centos65 ~]$ exit
logout
Connection to 127.0.0.1 closed.

Attribute Precedence

Figure 8-6 shows the three most commonly used levels of precedence that can be used in attribute definitions:
Automatic
attributes are those discovered by ohai.
Default
attributes are typically set in cookbooks and attribute files.
Override
attributes are the strongest way to set an attribute—use sparingly.
Precedence levels
Figure 8-6. Precedence levels
When setting an attribute in a cookbook, it should (almost) always be a default attribute. There is one exception to this rule, where it makes sense to use override precedence with environments. We’ll explore the reasoning behind this in Chapter 15.
Other than the special case of Chef environments, attribute precedence is most frequently used when you want to customize Chef, which is beyond the scope of this book. When you want to implement a new feature in Chef, such as a custom resource, being able to tinker with Chef’s powerful attribute precedence engine lets you add new features to the language that follow the general expectations for basic attribute priority outlined in this chapter. For more information on this topic, refer to Customizing Chef, by Jon Cowie.

Debugging Attributes

If you need to debug where attributes are being set, the node object exposes a helpful node.debug_value()method. Let’s say, for example, you did not know that ohai set node['ipaddress'] using automatic precedence. You could determine this by using node.debug_value().
Modify recipes/default.rb as shown in Example 8-10. Because we have access to the full power of the Ruby language in a Chef recipe, we also make use of the “pretty printer for Ruby objects” function included with the core Ruby library. This function will print out the contents of a Ruby object in a more readable format. The node.debug_value() returns the raw contents of an object; pp just makes the output look nicer.
Example 8-10. chefdk/motd-attributes/recipes/default.rb
#
# Cookbook Name:: motd-attributes
# Recipe:: default
#
# Copyright (C) 2014
#
#
#

require 'pp'

node.default['ipaddress'] = '1.1.1.1'
pp node.debug_value('ipaddress')

node.default['motd-attributes']['company'] = 'My Company'
node.default['motd-attributes']['message'] = "It's a wonderful day today!"

include_recipe 'motd-attributes::message'

template '/etc/motd' do
  source 'motd.erb'
  mode '0644'
end
When you run kitchen converge, you should see the following output:
$ kitchen converge
-----> Starting Kitchen (v1.2.2.dev)
...
       [["set_unless_enabled?", false],
        ["default", "1.1.1.1"],
        ["env_default", :not_present],
        ["role_default",
        :not_present],
        ["force_default", :not_present],
        ["normal", :not_present],
        ["override", :not_present],
        ["role_override", :not_present],
        ["env_override", :not_present],
        ["force_override", :not_present],
        ["automatic", "10.0.2.15"]]
...
From here, you could sort out that the node['ipaddress'] attribute was set to the value of "10.0.2.15" at the automatic precedence level and set to the value "1.1.1.1" at the default precedence level. So Chef did register that you set the value to "1.1.1.1", but the override took precedence.

NOTE

You might notice that there are more precedence levels in the node.debug_level() output than we have discussed so far in this book. We are intentionally simplifying attribute precedence in this book. If you follow the guideline of using default precedence, unless there is a specific need, you rarely have to deal with the full complexity of attribute precedence with basic Chef cookbook code.
A lot of the complexity of attributes arose as the precedence feature was introduced in Chef and cookbooks needed to be written to be backward-compatible with older versions. With Chef 11, most cookbooks follow the default precedence guideline.
If you’d like to learn more about the details of attribute precedence, refer to the aforementioned Customizing Chef.
Debugging when an attribute is set in two or more places at the same precedence level is a little more difficult to trace, but still not too complicated. You just need to sprinkle node.debug_value() statements before and after include_recipe calls.
For example, say we didn’t know that the motd-attributes::message recipe set node.default['motd-attributes']['message']. We could figure this out by sprinkling more node.debug_value() calls in our code. Change recipes/default.rb as shown in Example 8-11, adding a call to node.debug_value('motd-attributes', 'message') before and after include_recipe.
Example 8-11. chefdk/motd-attributes/recipes/default.rb
#
# Cookbook Name:: motd-attributes
# Recipe:: default
#
# Copyright (C) 2014
#
#
#

require 'pp'

node.default['ipaddress'] = '1.1.1.1'
pp node.debug_value('ipaddress')

node.default['motd-attributes']['company'] = 'My Company'
node.default['motd-attributes']['message'] = "It's a wonderful day today!"

pp node.debug_value('motd-attributes', 'company')
include_recipe 'motd-attributes::message'
pp node.debug_value('motd-attributes', 'company')

template '/etc/motd' do
  source 'motd.erb'
  mode '0644'
end
Run kitchen converge again, and the output of node.debug_value() should resemble the output shown in Figure 8-7, with the second-to-last dump of node.debug_value() being before include_recipe and the last being after include_recipe.
node.debug_value() output before and after include_recipe
Figure 8-7. node.debug_value() output before and after include_recipe
From this output, you could sort out that something in motd-attributes::message recipe set the attribute node['motd-attributes']['company'] to "the best company in the universe" using the default precedence, overriding what was set earlier.
Now we’re done with this cookbook and virtual machine in this chapter. Run the kitchen destroy command to shut down the virtual machine and release all the associated system resources:
$ kitchen destroy
-----> Starting Kitchen (v1.2.2.dev)
-----> Destroying <default-centos65>...
       ==> default: Forcing shutdown of VM...
       ==> default: Destroying VM and associated drives...
       Vagrant instance <default-centos65> destroyed.
       Finished destroying <default-centos65> (0m2.95s).
-----> Kitchen is finished. (0m3.43s)

Summary

In this chapter, we presented an overview of attributes and how Chef uses attributes to capture the state of a node. Attributes can be defined in multiple places:
  • Automatically via ohai
  • Attribute files
  • Recipes
  • Other cookbooks
Because attributes can be defined in multiple places, Chef defines a priority scheme for how attribute values are composed from multiple sources. You can tweak this priority scheme by manually providing different levels of precedence, including automaticdefault, and override. In general, you should use the default precedence, unless there is a specific need to extend or customize Chef, letting Chef choose the default priority in which attribute values are merged together.
In the next chapter, we’ll be introducing Chef Server. Chef Server is a useful central repository for all shared information necessary to manage multiple nodes effectively.

No comments:

Post a Comment