Tuesday 3 January 2017

Chapter 3. Ruby and Chef Syntax

As briefly discussed in Chapter 2, both the Chef Development Kit and Chef Client are written and implemented in Ruby. However, prior experience with Ruby is not a requirement for writing Chef code. Most people who use Chef have no prior experience with the Ruby programming language. So let’s spend some time going over the basics of Ruby and how it relates to Chef syntax.

Overview of Ruby

Ruby is an object-oriented programming language that was originally designed in 1993 as a replacement for Perl.Yukihiro Matsumoto (or “Matz” for short) designed and created Ruby in Japan. Ruby became very popular in the United States after two things occurred: 1) Dave Thomas wrote the book “Programming Ruby” in English in 2000, as until then most of the documentation on Ruby was in Japanese, and 2) David Heinemeier Hansson created the Ruby on Rails framework in 2003, which came to be viewed as an incredibly productive way to build web applications. The rapid adoption of Ruby on Rails, along with great documentation written in English, led people outside Japan to appreciate the Ruby language for other purposes besides web development. As illustrated in the next section, Ruby boasts a very English-like syntax.
Although Ruby is object oriented, it also supports functional and imperative programming paradigms. Unlike C or Java, which implement static typing, Ruby is a dynamically typed language. In this way, Ruby is similar toPython and Lisp. Ruby is designed for programmer productivity and fun. Usability and interface design are often given preference over speed or concurrency:
Often people, especially computer engineers, focus on the machines. They think, “By doing this, the machine will run faster. By doing this, the machine will run more effectively. By doing this, the machine will something something something.” They are focusing on machines. But in fact we need to focus on humans, on how humans care about doing programming or operating the application of the machines. We are the masters. They are the slaves.
 Yukihiro Matsumoto
Similarly, Ruby follows the Principle of Least Surprise (also called the Principle of Least Astonishment), which aims to minimize confusion for both new and experienced users. The principle encourages consistency, common design patterns, and reusable code. The Ruby principles of simplicity and ease of use are echoed throughout Chef as well.
There is much more to the Ruby programming language, but that knowledge is not required to use Chef. For a more detailed explanation of Ruby and the Ruby programming language, check out Programming Ruby 1.9 & 2.0 (4th edition): The Pragmatic Programmers’ Guide by Dave Thomas with Chard Fowler and Andy Hunt, orLearning Ruby by Michael Fitzgerald (O’Reilly).

Ruby Syntax and Examples

Let’s cover the basics of Ruby syntax through the use of real-world examples.

CHECKING SYNTAX

Ruby provides a built-in mechanism to verify that a file contains valid Ruby syntax. You can check a file’s syntax by passing the -c flag and the path to the file to the Ruby interpreter.
$ ruby -c /path/to/ruby/file
This will return Syntax OK if the syntax is correct. Otherwise, it will print a stack trace pointing to the line where the error occurred.

COMMENTS

In Ruby, the hash character (#) is used to represent a comment. Comments contain documentation for your code. Everything after the \# on the same line is treated as a comment intended to be read by humans and ignored by the Ruby interpreter:
variable = 2 # This is a comment

# This is a multiple line comment because each
# line starts with a hash symbol
It is important to document why you are writing code. Use comments to explain why you chose to implement your code in the way you did and describe the alternative approaches you considered. Jeff Atwood, of Stack Exchange and Discourse fame, brilliantly explains the purpose of comments this way: “Code tells you how, comments tell you why.”
For example, consider the following Ruby code snippet, which buys bacon if there are currently fewer than five strips. By reading the code you can understand what the code does, but it is not clear why the code was written in the first place:
if bacon.strips < 5
  buy_bacon
end
The following code adds a simple comment explaining why we should buy more bacon when there are fewer than five strips. It is helpful to understand the context around this bacon purchase: Jake likes to eat five pieces of bacon in the morning, so we want to have enough bacon on hand at all times:
# Jake eats 5 pieces of bacon each morning
if bacon.strips < 5
  buy_bacon
end

VARIABLES

As we just saw in the preceding example, variables in Ruby are assigned from left to right:
variable = 2 # This assigns the value 2 to variable.
Because Ruby is not a statically typed language, you do not need to declare the type of variable when assigning it. The following examples assign very different kinds of values to variables: a number, the string hello, and even an object. There is no need to tell Ruby the kind of content that will be stored in a variable before assigning values:
a = 1
b = 'hello'
c = Object.new
In Ruby, a variable is accessible within the outermost scope of its declaration. Consider the following example that better demonstrates variable scope. First, a top-level variable named bacon_type is declared and assigned the value crispy. Next, the bacon is cooked twice and an additional variable named temperature is assigned the value 300. Finally, the script attempts to access the temperature variable, which is now out of scope:
bacon_type = 'crispy' 1

2.times do
  puts bacon_type 2
  temperature = 300 3
end

puts temperature 4
1
The bacon_type variable is declared at the top-level scope, so it will be accessible everywhere within this context.
2
We can access the bacon_type variable inside a more specific scope, such as a loop.
3
A variable declared inside a scope is accessible only from inside that scope.
4
Outside of the scope of declaration, attempting to access a variable will result in an exception (undefined local variable or method ‘temperature’).

WAIT… WHAT ARE YOU SAYING?

It’s worth noting that if you’re feeling a bit lost at this point, this book presumes a basic level of experience with object-oriented programming concepts. If you’re not clear about variables, scope, and loops, you’ll probably want to brush up on some of the basics of object-oriented programming and then head back here to get up to speed on Ruby and Chef. A great book on object-oriented programming with Ruby is Practical Object-Oriented Design in Ruby, by Sandi Metz.

MATHEMATICAL OPERATIONS

Ruby exposes basic arithmetic operations such as addition, multiplication, and division as a core feature of the language:
1 + 2        #=> 3
3 * 4        #=> 12
5 / 6.0      #=> 0.8333...
Let’s say that to ensure a productive and collaborative work environment, your company policy requires that your team take a bacon break every three hours, on the hour. Even though the policy is strict, the team often becomes engrossed in its work and forgets to check the clock. You could write a short Ruby script that alerts the team when it should stop and enjoy some freshly-cooked bacon:
# Bacon time is every third hour
if hour % 3 == 0
  puts "It's BACON time!"
end
More complex operations, such as rounding or polynomial distributions, are exposed in Ruby’s Math module. You can use the Math.hypot method in a Ruby script to calculate the length of a diagonal for a right triangle by returning the sum of the squares of its two sides:
Math.hypot(43, 57) #=> 71.40028011149536
Additional functions and constants such as logsinsqrte, and Ļ€ are also contained in the Math module.

STRINGS

There are two common ways to create strings in Ruby: with single quotes and with double quotes. Single-quoted strings have the advantage of one fewer keystroke and do not apply interpolation.
Double-quoted strings are useful when variables need to be evaluated as part of the string content—this evaluation process is known as string interpolation. A hash symbol is used as a placeholder within a double-quoted string to indicate to Ruby that the placeholder should be replaced with the evaluated content of a variable. In the following example, #{x} is used to insert the value of x within a string.
Both double and single-quoted strings use the backslash (\) character to escape special characters.
"double quoted string" #=> "double quoted string"
'single quoted string' #=> "single quoted string"

x = "hello"
"#{x} world" #=> "hello world" 1
'#{x} world' #=> '#{x} world' 2

"quotes in \"quotes\"" #=> "quotes in \"quotes\""
'quotes in \'quotes\'' #=> "quotes in \"quotes\""
1
Double-quoted strings interpolate
2
Single-quoted strings use the literal
You might need to escape special characters. For example, when storing the player name Jim O’Rourke as a string, you need to escape the single quote in his last name:
player = 'Jim O\'Rourke'
Alternatively, you can use double quotes and avoided escaping the character altogether:
player = "Jim O'Rourke"

HEREDOC NOTATION

In Chef, you might see the “heredoc” notation for strings. Heredoc is especially useful when having a multiline string. Heredoc notation starts with two “less than” symbols (<<) and then some identifier. The identifier can be any string, but should not be a string that is likely to appear in the body of text. In the following example, we chose METHOD_DESCRIPTION as the heredoc identifier:
<<METHOD_DESCRIPTION
This is a multiline string.

All the whitespace is preserved, and I can even apply #{interpolation} inside
this block.
METHOD_DESCRIPTION

TRUE AND FALSE

In addition to the literal true and false, Ruby supports “truthy” and “falsey” values. Expressions evaluate as true or false in a situation where a boolean result is expected (such as when they are used in conditionals like the if statement). Table 3-1 demonstrates the common truthy and falsey values and their actual evaluations.
Table 3-1. Truthy and falsey values
valueevaluates as
true
true
false
false
Bacon
true
Object
true
0
true
1
true
-1
true
nil
false
""
true
[]
true
{}
true
The results of those expressions can also be negated using the not keyword or the bang (!) operand:
!true      #=> false
not true   #=> false
not false  #=> true
!!true     #=> true
not nil    #=> true

ARRAYS

Ruby’s native array support allows you to create lists of items. Ruby’s arrays use the bracket notation as shown in the following example. Arrays are zero-indexed and the Ruby interpreter automatically allocates memory as new items are added; there is no need to worry about dynamically resizing arrays:
types = ['crispy', 'raw', 'crunchy', 'grilled']
types.length          #=> 4 1
types.size            #=> 4 2
types.push 'smoked'   #=> ["crispy", "raw", "crunchy", "grilled", "smoked"] 3
types << 'deep fried' #=> ["crispy", "raw", "crunchy", "grilled",
                           "smoked", "deep fried"] 4
types[0]     #=> "crispy" 5
types.first  #=> "crispy" 6
types.last   #=> "deep fried" 7
types[0..1]  #=> ["crispy", "raw"] 8
1
The length method tells us how many items are in the array.
2
You’ll sometimes see size used as a synonym for length; Ruby offers the choice to use either.
3
Add one or more items to the end of the array with push.
4
<< is a helpful alias for push when you want to add just one item to the end.
5
Arrays are zero-indexed, so we can access the first element using 0 as the index.
6
The first element is also accessible via the convenient first method.
7
To complement the first method, Ruby also exposes a last method.
8
Specifying a range inside the brackets will slice the array from the first index up to and including the second index.
For example, you could use Ruby arrays to store an ordered list of all employees in the order in which they were hired. Each time a new employee joins the team, he is added to the list. The first employee is at index 0, the second employee is at index 1, and so on:
employees[0] #=> Chase
employees[1] #=> Jake
When a new employee joins the team, his name is pushed onto the end of the array:
employees << 'Bob'
employees.last #=> Bob

HASHES

Ruby also supports hashes (sometimes called dictionaries or maps in other languages). Hashes are key-value pairs that behave similarly to arrays. Hashes are created using literal curly braces ({}):
prices = { oscar: 4.55, boars: 5.23, wright: 4.65, beelers: 6.99 }
prices[:oscar] #=> 4.55 1
prices[:boars] #=> 5.23

prices[:oscar] = 1.00 2
prices.values #=> [4.55, 5.23, 4.65, 6.99] 3
1
Individual elements are accessed using the bracket notation, much like arrays.
2
The same bracket notation can be used to set elements at a particular key.
3
Hashes respond to helpful methods like keys and values.
For example, you can use Ruby hashes to store information about popular baseball players and their current statistics. The key in this hash was the name of the baseball player. The value was another hash whose key is the name of the statistic and whose value is the number of that statistic. This is better illustrated by the following sample data:
players = {
  'McCutchen, Andrew' => {
    'AVG' => 0.311,
    'OBP' => 0.385,
    'SLG' => 0.507
  },
  'Alvarez, Pedro' => {
    'AVG' => 0.236,
    'OBP' => 0.297,
    'SLG' => 0.477
  }
}
An individual player is accessed using his name as the key in the players hash. Because players is a hash of hashes, the returned value is also a hash:
players['McCutchen, Andrew'] #=> { 'AVG' => 0.311, 'OBP' => 0.385, 'SLG' => 0.507 }
A particular statistic is accessible by nested bracket notation where the first key is the name of the player and the second key is the name of the statistic:
players['McCutchen, Andrew']['AVG'] #=> 0.311

REGULAR EXPRESSIONS

Ruby supports Perl-style regular expressions using the =~ operator:
"Bacon is good" =~ /lie/    #=> nil
"Bacon is good" =~ /bacon/  #=> 0
"Bacon is good" !~ /lie/    #=> true

REMEMBERING THE ORDER

It’s easy to forget the order of the equal-tilde matcher. Is it =~ or ~=? The easiest way to always get the order correct is to think in alphabetical order. “Equals” comes before “tilde” in the dictionary, so the equal sign comes before the tilde in the expression.
For example, you could try using regular expressions to find names in a list of players on a baseball team beginning with a certain letter of the alphabet. Regular expressions can be anchored using the caret (^), which starts a match at the beginning of the string. The following snippet searches for all players that have a last name beginning with the letter F:
players.select do |name, statistics|
  name =~ /^F/
end

CONDITIONALS AND FLOW

Like most programming languages, Ruby supports conditionals to manage the flow and control of application logic. The most common control gate is the if keyword. The if keyword is complemented by the unlesskeyword, which is the equivalent of if not:
# Using if
if some_condition
  puts "happened"
else
  puts "didn't happen"
end

# Using unless
unless some_condition
  puts "didn't happen"
else
  puts "happened"
end
It is also possible to have multiple levels of conditionals using the elsif keyword:
if false 1
  puts "this can't possibly happen"
elsif nil 2
  puts "this won't happen either"
elsif true
  puts "this will definitely happen"
else 3
  puts "this won't happen, because the method is short-circuited
end
1
If the result of an expression is falsey, the body of that block will not be executed.
2
nil is considered a falsey value, so this block will not be evaluated.
3
Because the former elsif block will always execute, this else block is unnecessary.
Less common in Ruby (but common in Chef) is the case statement. Very similar to the if statement, the casestatement applies more syntactic sugar and logic for the developer:
case some_condition
when "literal string" 1
  # ...
when /regular expression/ 2
  # ...
when list, of, items 3
  # ...
else 4
  # ...
end
1
If a literal object is supplied, the case statement will perform a pure equality match.
2
If a regular expression is supplied, Ruby will attempt to call match on the receiving object.
3
If multiple items are given, they are interpreted as “or” (if the item matches “list”, “of”, or “items”).
4
case statements also support a default case, in the event that nothing else matches.
For example, you could use Ruby’s case statement to classify players on a baseball team based on their age. Players younger than 12 are considered to be in the minor league. Players between 13 and 18 are developing. Players between 19 and 30 are in their prime. Players still in the league between 31 and 40 are on their decline, and anyone over 40 is in retirement. This logic is captured by the following Ruby case statement:
case player.age
when 0..12
  'Minor League'
when 13..18
  'Developing'
when 19..30
  'Prime'
when 31..40
  'Decline'
else
  'Retirement'
end

METHODS, CLASSES, AND MODULES

Although not necessary until more advanced interactions with Chef, Ruby also supports methods, classes, modules, and an object-oriented hierarchy. Ruby methods are defined using the def keyword. Ruby classes are defined using the class keyword. Ruby modules are defined using the module keyword:
class Bacon 1
  def cook(temperature) 2
    # ...
  end
end

module Edible 3
  # ...
end
1
Classes are created using the class keyword.
2
Methods are created using the def keyword and can accept arguments (or parameters).
3
Modules are created using the module keyword.
Methods may be invoked by name. Although optional, parentheses are highly recommended for readability and code portability:
my_method(5)          1
my_method 5           2
1
This will execute the method named my_method with the parameter 5.
2
my_method call without parentheses does the same thing.
Methods are also chainable in Ruby, so you can continue to call a method on a chain, provided the return value responds to the method:
"String".upcase.downcase.reverse  #=> "gnirts"

Chef Syntax and Examples

The Domain Specific Language (DSL) used by Chef is actually just a subset of Ruby. The full power of the Ruby programming language is accessible in Chef code. This allows developers to conditionally perform actions, perform mathematical operations, and communicate with other services easily from within Chef code. Before diving into the more advanced features of Chef’s DSL, we will explore the basic syntax first.
Here is an example of the Chef DSL in action, demonstrating how a user account can be created with a Chef resource. In Chef, resources are the building blocks used to define specific parts of your infrastructure. For example, the following statement manages a user account named alice with a user identifier (UID) of 503:
user 'alice' do
  uid '503'
end
The following code sample demonstrates the more abstract syntax for invoking a DSL method in Chef code, on which the previous example is based:
resource 'NAME' do
  parameter1   value1
  parameter2   value2
end
The first part is the name of the resource (such as templatepackage, or service). The next part is the name_attribute for that resource. The interpretation of this value changes from resource to resource. For example, in the package resource, the name_attribute is the name of the package you wish to install. In the template resource, the name_attribute is the path on the target node where the compiled file should reside. Next comes the Ruby keyword do. In Ruby, the do must always be accompanied by a closing end. Everything that resides between the do and end is called the block. Inside the block, resource parameters and their values are declared. This varies from resource to resource. A valid parameter for the package provider is version, whereas a valid parameter for the template provider is source.
It might help to think about the code in a more object-oriented approach as follows. The Chef DSL creates a new resource object for you, sets the correct attributes and parameters, and then executes that resource when Chef evaluates the code:
resource = Resource.new('NAME')
resource.parameter1 = value1
resource.parameter2 = value2
resource.run!

WARNING

Please note that the preceding code is not valid syntax and is only used for instructional purposes.
templatepackage, and service are just three of the many types of resources built into the Chef DSL. The following code demonstrates using the templatepackage, and service resources in Chef code:
template '/etc/resolv.conf' do 1
  source 'my_resolv.conf.erb'
  owner  'root'
  group  'root'
  mode   '0644'
end

package 'ntp' do 2
  action :upgrade
end

service 'apache2' do 3
  restart_command '/etc/init.d/apache2 restart'
end
1
This declares a template resource in Chef’s DSL. The template will be compiled from the local file my_resolv.conf.erb, be owned by root:root, have 0644 permissions, and be placed at /etc/resolv.conf on the target machine (where Chef evaluates the code).
2
This declares a package resource in Chef’s DSL. The “ntp” package will be upgraded.
3
This declares a service resource in Chef’s DSL. The “apache2” service will be accessible and manageable by Chef.
If you specify an invalid parameter (either one that does not exist or is misspelled), Chef will raise an exception:
NoMethodError
-------------

undefined method `not_a_real_parameter' for Chef::Resource
Chef uses a multiphase execution model, which lets you include logic switches or loops inside Chef code. For example, if you wanted to execute a resource on an array of objects, you could do so using the following code. The file resource is used to manage a file. content is a Chef DSL expression used to specify a string that is written to the file:
['bacon', 'eggs', 'sausage'].each do |type|
  file "/tmp/#{type}" do
    content "#{type} is delicious!"
  end
end
In the first phase of evaluation, Chef will dynamically expand that Ruby loop. After the first phase, internally the code becomes equivalent to:
file '/tmp/bacon' do
  content 'bacon is delicious!'
end

file '/tmp/eggs' do
  content 'eggs is delicious!'
end

file '/tmp/sausage' do
  content 'sausage is delicious!'
end
Even though the resources were dynamically created using Ruby interpolation and looping, they are still available as individual items in the resource list, because of Chef’s multiphase execution.
Similarly, top-level Ruby code is computed during the first phase of execution. When dynamically calculating a value (such as the total free memory on a target node), those values are cached and stored during the first phase of execution:
free_memory = node['memory']['total']

file '/tmp/free' do
  contents "#{free_memory} bytes free on #{Time.now}"
end
In the second phase of evaluation, the resource contained in the resource list will be:
file '/tmp/free' do
  contents "12904899202 bytes free on 2013-07-24 17:47:01 -0400"
end
So far, you have seen the file resource, template resource, service resource, and package resource. These are all resources packaged into the core of Chef. You can find a complete listing of all resources on the resources page. Here are some of the most commonly used Chef resources, followed by examples of their basic usage:
bash
Execute multi-line scripts written in the Bourne-again shell (bash) scripting language using the bash shell interpreter:
# Output 'hello' to the console
bash 'echo "hello"'
chef_gem
Install a gem inside of Chef, for use inside Chef; useful when a Chef code requires a gem to perform a function:
# Install the HTTParty gem to make RESTful requests
chef_gem 'httparty'
cron
Create or manage a cron entry that schedules commands to run periodically at specified intervals:
# Restart the computer every week
cron 'weekly_restart' do
  weekday '1'
  minute  '0'
  hour    '0'
  command 'sudo reboot'
end
deploy_revision
Control and manage a deployment of code from source control (such as a Rails application):
# Clone and sync an application from revision control
deploy_revision '/opt/my_app' do
  repo 'git://github.com/username/app.git'
end
directory
Manage a directory or directory tree, handling permissions and ownership:
# Recursively ensure a directory exists
directory '/opt/my/deep/directory' do
  owner     'root'
  group     'root'
  mode      '0644'
  recursive true
end
execute
Execute an arbitrary one-line command (as if it were entered on the command line):
# Write contents to a file
execute 'write status' do
  command 'echo "delicious" > /tmp/bacon'
end
file
Manage a file already present (but not already managed by Chef):
# Delete the /tmp/bacon file
file '/tmp/bacon' do
  action :delete
end
gem_package
Install a gem for use outside of Chef, such as an application or utility:
# Install bundler to manage dependencies
gem_package 'bundler'
group
Create or manage a local group definition with local user accounts as members:
# Create the bacon group
group 'bacon'
link
Create and manage symlinks and hard links:
# Link /tmp/bacon to /tmp/delicious
link '/tmp/bacon' do
  to '/tmp/delicious'
end
mount
Mount or unmount a file system:
# Mount /dev/sda8
mount '/dev/sda8'
package
Install a package using the operating system’s underlying package manager:
# Install the apache2 package (on Debian-based systems)
package 'apache2'
remote_file
Transfer a file from a remote location (such as a website):
# Download a remote file to /tmp/bacon
remote_file '/tmp/bacon' do
  source 'http://bacon.org/bits.tar.gz'
end
service
Start, stop, or restart a service:
# Restart the apache2 service
service 'apache2' do
  action :restart
end
template
Manage plain-text file contents parsed as an Embedded Ruby template:
# Write the /tmp/bacon template using the bits.erb source
template '/tmp/bacon' do
  source 'bits.erb'
end
user
Create or manage a local user account:
# Create the bacon user
user 'bacon'
These examples illustrate Chef’s DSL, as well as showcase some of the common resources used when working with Chef. Although our list is not comprehensive, it does include some of the most common Chef resources you will encounter. The full list of Chef’s built-in resources can be found in the online resource documentation.
The material presented in this chapter is all you need to know about Ruby to write Chef code. Only when you need to extend Chef beyond what is provided out of the box will you have to worry about delving more deeply into Ruby coding. When you need to level up your Ruby knowledge for this task, we highly recommendCustomizing Chef by Jon Cowie (O’Reilly).
Now that we have covered all the necessary fundamentals of Ruby, let’s get back to more Chef coding! In the next chapter, we will put your new Ruby knowledge to use by writing some Chef code.

2 comments:

  1. Nice post ! Thanks for sharing valuable information with us. Keep sharing..Ruby on Rails Online Training Hyderabad

    ReplyDelete

  2. Thanks for sharing such a Excellent Blog! We are linking too this particularly great post on our site. Keep up the great writing.
    ruby assignment help

    ReplyDelete