Ruby Guide
Ruby is a simple programming language:
- Chef uses Ruby as its reference language to define the patterns that are found in resources, recipes, and cookbooks
- Use these patterns to configure, deploy, and manage nodes across the network
Ruby is also a powerful and complete programming language:
- Use the Ruby programming language to make decisions about what should happen to specific resources and recipes
- Extend Chef in any manner that your organization requires
To learn more about Ruby, see:
Chef Infra Client 15 ships with Ruby 2.6 and Chef Infra Client 16 ships with Ruby 2.7.
Ruby Basics
This section covers the basics of Ruby.
Verify Syntax
Many people who are new to Ruby often find that it doesn’t take
long to get up to speed with the basics. For example, it’s useful to
know how to check the syntax of a Ruby file, such as the contents of a
cookbook named my_cookbook.rb
:
ruby -c my_cookbook_file.rb
to return:
Syntax OK
Comments
Use a comment to explain code that exists in a cookbook or recipe.
Anything after a #
is a comment.
# This is a comment.
Local Variables
Assign a local variable:
x = 1
Math
Do some basic arithmetic:
1 + 2 # => 3
2 * 7 # => 14
5 / 2 # => 2 (because both arguments are whole numbers)
5 / 2.0 # => 2.5 (because one of the numbers had a decimal place)
1 + (2 * 3) # => 7 (you can use parentheses to group expressions)
Strings
Work with strings:
'single quoted' # => "single quoted"
"double quoted" # => "double quoted"
'It\'s alive!' # => "It's alive!" (the \ is an escape character)
'1 + 2 = 5' # => "1 + 2 = 5" (numbers surrounded by quotes behave like strings)
Convert a string to uppercase or lowercase. For example, a hostname named “Foo”:
node['hostname'].downcase # => "foo"
node['hostname'].upcase # => "FOO"
Ruby in Strings
Embed Ruby in a string:
x = 'Bob'
"Hi, #{x}" # => "Hi, Bob"
'Hello, #{x}' # => "Hello, \#{x}" Notice that single quotes don't work with #{}
Escape Character
Use the backslash character (\
) as an escape character when quotes
must appear within strings. However, you don’t need to escape single
quotes inside double quotes. For example:
'It\'s alive!' # => "It's alive!"
"Won\'t you read Grant\'s book?" # => "won't you read Grant's book?"
Interpolation
When strings have quotes within quotes, use double quotes (" "
) on the
outer quotes, and then single quotes (' '
) for the inner quotes. For
example:
Chef::Log.info("Loaded from aws[#{aws['id']}]")
"node['mysql']['secretpath']"
"#{ENV['HOME']}/chef.txt"
antarctica_hint = hint?('antarctica')
if antarctica_hint['snow']
"There are #{antarctica_hint['penguins']} penguins here."
else
'There is no snow here, and penguins like snow.'
end
Truths
Work with basic truths:
true # => true
false # => false
nil # => nil
0 # => true ( the only false values in Ruby are false
# and nil; in other words: if it exists in Ruby,
# even if it exists as zero, then it's true.)
1 == 1 # => true ( == tests for equality )
1 == true # => false ( == tests for equality )
Untruths
Work with basic untruths (!
means not!):
!true # => false
!false # => true
!nil # => true
1 != 2 # => true (1 isn't equal to 2)
1 != 1 # => false (1 isn't equal to itself)
Convert Truths
Convert something to either true or false (!!
means not not!!):
!!true # => true
!!false # => false
!!nil # => false (when pressed, nil is false)
!!0 # => true (zero isn't false).
Arrays
Create lists using arrays:
x = ['a', 'b', 'c'] # => ["a", "b", "c"]
x[0] # => "a" (zero is the first index)
x.first # => "a" (see?)
x.last # => "c"
x + ['d'] # => ["a", "b", "c", "d"]
x # => ["a", "b", "c"] ( x is unchanged)
x = x + ['d'] # => ["a", "b", "c", "d"]
x # => ["a", "b", "c", "d"]
Whitespace Arrays
The %w
syntax is a Ruby shortcut for creating an array without
requiring quotes and commas around the elements.
For example:
if %w(debian ubuntu).include?(node['platform'])
# do debian/ubuntu things with the Ruby array %w() shortcut
end
When %w
syntax uses a variable, such as |foo|
, double quoted strings
should be used.
Right:
%w(openssl.cnf pkitool vars Rakefile).each do |foo|
template "/etc/openvpn/easy-rsa/#{foo}" do
source "#{foo}.erb"
...
end
end
Wrong:
%w(openssl.cnf pkitool vars Rakefile).each do |foo|
template '/etc/openvpn/easy-rsa/#{foo}' do
source '#{foo}.erb'
...
end
end
Example
WiX includes several tools – such as candle
(preprocesses and
compiles source files into object files), light
(links and binds
object files to an installer database), and heat
(harvests files from
various input formats). The following example uses a whitespace array
and the Chef InSpec file
audit resource to verify if these three tools
are present:
%w(
candle.exe
heat.exe
light.exe
).each do |utility|
describe file("C:/wix/#{utility}") do
it { should be_file }
end
end
Hash
A Hash is a list with keys and values. Sometimes hashes don’t have a set order:
h = {
'first_name' => 'Bob',
'last_name' => 'Jones',
}
And sometimes they do. For example, first name then last name:
h.keys # => ["first_name", "last_name"]
h['first_name'] # => "Bob"
h['last_name'] # => "Jones"
h['age'] = 23
h.keys # => ["first_name", "age", "last_name"]
h.values # => ["Jones", "Bob", 23]
Regular Expressions
Use Perl-style regular expressions:
'I believe' =~ /I/ # => 0 (matches at the first character)
'I believe' =~ /lie/ # => 4 (matches at the 5th character)
'I am human' =~ /bacon/ # => nil (no match - bacon comes from pigs)
'I am human' !~ /bacon/ # => true (correct, no bacon here)
/give me a ([0-9]+)/ =~ 'give me a 7' # => 0 (matched)
Statements
Use conditions! For example, an if
statement
if false
# this won't happen
elsif nil
# this won't either
else
# code here will run though
end
or a case
statement:
x = 'dog'
case x
when 'fish'
# this won't happen
when 'dog', 'cat', 'monkey'
# this will run
else
# the else is an optional catch-all
end
if
An if
statement can be used to specify part of a recipe to be used
when certain conditions are met. else
and elsif
statements can be
used to handle situations where either the initial condition isn’t met
or when there are other possible conditions that can be met. Since this
behavior is 100% Ruby, do this in a recipe the same way here as anywhere
else.
For example, using an if
statement with the platform
node attribute:
if node['platform'] == 'ubuntu'
# do ubuntu things
end
if modifier
if
can be used as a modifier that executes the left side of an expression
if the right side of the expression is true. The if
modifier expression must
be a single line, and else
and elsif
statements aren’t supported.
In the following example, the do_ubuntu_thing
function will execute if the platform on a node is Ubuntu.
do_ubuntu_thing if platform?('ubuntu')
case
A case
statement can be used to handle a situation where there are a
lot of conditions. Use the when
statement for each condition, as many
as are required.
For example, using a case
statement with the platform
node
attribute:
case node['platform']
when 'debian', 'ubuntu'
# do debian/ubuntu things
when 'redhat', 'centos', 'fedora'
# do redhat/centos/fedora things
end
For example, using a case
statement with the platform_family
node
attribute:
case node['platform_family']
when 'debian'
# do things on debian-ish platforms (debian, ubuntu, linuxmint)
when 'rhel'
# do things on RHEL platforms (redhat, centos, scientific, etc)
end
Call a Method
Call a method on something with .method_name()
:
x = 'My String'
x.split(' ') # => ["My", "String"]
x.split(' ').join(', ') # => "My, String"
Define a Method
Define a method (or a function, if you like):
def do_something_useless(first_argument, second_argument)
puts "You gave me #{first_argument} and #{second_argument}"
end
do_something_useless('apple', 'banana')
# => "You gave me apple and banana"
do_something_useless 1, 2
# => "You gave me 1 and 2"
# see how the parentheses are optional if there's no confusion about what to do
Ruby Class
Use the Ruby File
class in a recipe. Because Chef has the file
resource, use File
to use the Ruby File
class. For example:
execute 'apt-get-update' do
command 'apt-get update'
ignore_failure true
not_if { ::File.exist?('/var/lib/apt/periodic/update-success-stamp') }
end
Include a Class
Use :include
to include another Ruby class. For example:
::Chef::DSL::Recipe.include MyCookbook::Helpers
In non-Chef Ruby, the syntax is include
(without the :
prefix), but
without the :
prefix Chef Infra Client will try to find a provider
named include
. Using the :
prefix tells Chef Infra Client to look
for the specified class that follows.
Include a Parameter
The include?
method can be used to ensure that a specific parameter is
included before an action is taken. For example, using the include?
method to find a specific parameter:
if %w(debian ubuntu).include?(node['platform'])
# do debian/ubuntu things
end
or:
if %w(rhel).include?(node['platform_family'])
# do RHEL things
end
Patterns to Follow
This section covers best practices for cookbook and recipe authoring.
git Etiquette
Although not strictly a Chef style thing, please always ensure your
user.name
and user.email
are set properly in your .gitconfig
file.
user.name
should be your given name (for example, “Julian Dunn”)user.email
should be an actual, working e-mail address
This will prevent commit log entries similar to
"guestuser <login@Bobs-Macbook-Pro.local>"
, which are unhelpful.
Use of Hyphens
Cookbook and custom resource names should contain only alphanumeric characters. A hyphen (-
) is a valid character and may be used in
cookbook and custom resource names, but it’s discouraged. Chef Infra
Client will return an error if a hyphen isn’t converted to an
underscore (_
) when referencing from a recipe the name of a custom
resource in which a hyphen is located.Cookbook Naming
Use a short organizational prefix for application cookbooks that are
part of your organization. For example, if your organization is named
SecondMarket, use sm
as a prefix: sm_postgresql
or sm_httpd
.
Cookbook Versioning
- Use semantic versioning when numbering cookbooks.
- Only upload stable cookbooks from master.
- Only upload unstable cookbooks from the dev branch. Merge to master and bump the version when stable.
- Always update CHANGELOG.md with any changes, with the JIRA ticket and a brief description.
Naming
Name things uniformly for their system and component. For example:
- attributes:
node['foo']['bar']
- recipe:
foo::bar
- role:
foo-bar
- directories:
foo/bar
(if specific to component),foo
(if not). For example:/var/log/foo/bar
.
Name attributes after the recipe in which they’re primarily used. for example
node['postgresql']['server']
.
Parameter Order
Follow this order for information in each resource declaration:
- Source
- Cookbook
- Resource ownership
- Permissions
- Notifications
- Action
For example:
template '/tmp/foobar.txt' do
source 'foobar.txt.erb'
owner 'someuser'
group 'somegroup'
mode '0644'
variables(
foo: 'bar'
)
notifies :reload, 'service[whatever]'
action :create
end
File Modes
Always specify the file mode with a quoted 3-5 character string that defines the octal mode:
mode '755'
mode '0755'
Wrong:
mode 755
Specify Resource Action?
A resource declaration doesn’t require the action to be specified because Chef Infra Client will apply the default action for a resource automatically if it’s not specified within the resource block. For example:
package 'monit'
will install the monit
package because the :install
action is the
default action for the package resource.
However, if readability of code is desired, such as ensuring that a reader understands what the default action is for a custom resource or stating the action for a resource whose default may not be immediately obvious to the reader, specifying the default action is recommended:
ohai 'apache_modules' do
action :reload
end
String Quoting
Use single-quoted strings in all situations where the string doesn’t need interpolation.
Whitespace Arrays
When %w
syntax uses a variable, such as |foo|
, double quoted strings
should be used.
Right:
%w(openssl.cnf pkitool vars Rakefile).each do |foo|
template "/etc/openvpn/easy-rsa/#{foo}" do
source "#{foo}.erb"
...
end
end
Wrong:
%w(openssl.cnf pkitool vars Rakefile).each do |foo|
template '/etc/openvpn/easy-rsa/#{foo}' do
source '#{foo}.erb'
...
end
end
Recipes
A recipe should be clean and well-commented. For example:
###########
# variables
###########
connection_info = {
host: '127.0.0.1',
port: '3306',
username: 'root',
password: 'm3y3sqlr00t',
}
#################
# Mysql resources
#################
mysql_service 'default' do
port '3306'
initial_root_password 'm3y3sqlr00t'
action [:create, :start]
end
mysql_database 'wordpress_demo' do
connection connection_info
action :create
end
mysql_database_user 'wordpress_user' do
connection connection_info
database_name 'wordpress_demo'
password 'w0rdpr3ssdem0'
privileges [:create, :delete, :select, :update, :insert]
action :grant
end
##################
# Apache resources
##################
httpd_service 'default' do
listen_ports %w(80)
mpm 'prefork'
action [:create, :start]
end
httpd_module 'php' do
notifies :restart, 'httpd_service[default]'
action :create
end
###############
# Php resources
###############
package 'php-gd' do
action :install
end
package 'php-mysql' do
action :install
end
directory '/etc/php.d' do
action :create
end
template '/etc/php.d/mysql.ini' do
source 'mysql.ini.erb'
action :create
end
httpd_config 'php' do
source 'php.conf.erb'
notifies :restart, 'httpd_service[default]'
action :create
end
#####################
# wordpress resources
#####################
directory '/srv/wordpress_demo' do
user 'apache'
recursive true
action :create
end
tar_extract 'https://wordpress.org/wordpress-4.1.tar.gz' do
target_dir '/srv/wordpress_demo'
tar_flags ['--strip-components 1']
user 'apache'
creates '/srv/wordpress_demo/index.php'
action :extract
end
directory '/srv/wordpress_demo/wp-content' do
user 'apache'
action :create
end
httpd_config 'wordpress' do
source 'wordpress.conf.erb'
variables(
servername: 'wordpress',
server_aliases: %w(computers.biz www.computers.biz),
document_root: '/srv/wordpress_demo'
)
notifies :restart, 'httpd_service[default]'
action :create
end
template '/srv/wordpress_demo/wp-config.php' do
source 'wp-config.php.erb'
owner 'apache'
variables(
db_name: 'wordpress_demo',
db_user: 'wordpress_user',
db_password: 'w0rdpr3ssdem0',
db_host: '127.0.0.1',
db_prefix: 'wp_',
db_charset: 'utf8',
auth_key: 'You should probably use randomly',
secure_auth_key: 'generated strings. These can be hard',
logged_in_key: 'coded, pulled from encrypted databags,',
nonce_key: 'or a ruby function that accessed an',
auth_salt: 'arbitrary data source, such as a password',
secure_auth_salt: 'vault. Node attributes could work',
logged_in_salt: 'as well, but you take special care',
nonce_salt: 'so they're not saved to your chef-server.',
allow_multisite: 'false'
)
action :create
end
Cookstyle Linting
Chef Workstation includes Cookstyle for linting the Ruby-specific and Chef-specific portions of your cookbook code. All cookbooks should pass Cookstyle rules before being uploaded.
cookstyle your-cookbook
should return no offenses detected