View on GitHub

logo

RSpec tests for your servers configured
by CFEngine, Puppet, Ansible, Itamae or anything else.

How to use SSH password login

Customize your spec_helper.rb like this.

require 'highline/import'

if ENV['ASK_LOGIN_PASSWORD']
  options[:password] = ask("\nEnter login password: ") { |q| q.echo = false }
else
  options[:password] = ENV['LOGIN_PASSWORD']
end

set :ssh_options, options

How to share Serverspec tests among hosts

serverspec-init generates template files like this.

|-- Rakefile
`-- spec
    |-- spec_helper.rb
    `-- www.example.jp
        `-- sample_spec.rb

This directory structure forces you to write tests per host.

But you can write tests per role like this.

|-- Rakefile
`-- spec
    |-- app
    |   `-- ruby_spec.rb
    |-- base
    |   `-- users_and_groups_spec.rb
    |-- db
    |   `-- mysql_spec.rb
    |-- proxy
    |   `-- nginx_spec.rb
    `-- spec_helper.rb

Rakefile should be like this.

require 'rake'
require 'rspec/core/rake_task'

hosts = %w(
  proxy001.example.com
  proxy002.example.com
  app001.example.com
  app002.example.com
  db001.example.com
  db002.example.com
)

task :spec => 'spec:all'

namespace :spec do
  task :all => hosts.map {|h| 'spec:' + h.split('.')[0] }
  hosts.each do |host|
    short_name = host.split('.')[0]
    role       = short_name.match(/[^0-9]+/)[0]

    desc "Run serverspec to #{host}"
    RSpec::Core::RakeTask.new(short_name) do |t|
      ENV['TARGET_HOST'] = host
      t.pattern = "spec/{base,#{role}}/*_spec.rb"
    end
  end
end

You can spec_helper.rb generated by serverspec-init as-is.


How to use Serverspec tests as shared behaviors

RSpec allows shared behaviors. Using this allows an alternate method of writing specs that can be run on multiple servers.

First, create a directory to hold the shared examples…typically ‘support’ or ‘shared’ In that directory, create a subdirectory to hold like behaviors. For example, if you are using Puppet, Ansible, Itamae, etc and you want to write shared behavior for your ‘database’ module/recipe/whatever.

mkdir -p serverspec/shared/database

then serverspec/shared/database/init.rb:

shared_examples 'database::init' do

  describe package('mysql-community-server') do
    it { should be_installed }
  end
    
end

Now to use that shared behavior on multiple hosts, create some specs for each server:

serverspec/specs/foo.bar.com_spec.rb:

require 'spec_helper'

describe 'foo.bar.com' do
  include_examples 'database::init'
end

and maybe bacon.bar.com also has apache:

require 'spec_helper'

describe 'bacon.bar.com' do
  include_examples 'database::init'
  include_examples 'apache::init'
end

Note that you can also pass parameters for greater reuse:

shared_examples 'deploy_user' do |deploy_user|
  describe user(deploy_user) do
    it { should exist }
    it { should have_home_directory("/home/#{deploy_user}") }
  end
end

# then later
include_examples 'deploy_user', 'the_user'

You can see examples in rubyisbeautiful/serverspec_examples.

RSpec shared examples docs will also help.


How to use host specific properties

Serverspec supports a simple mechanism to handle host specific properties.

This example assumes that host specific properties are in YAML file.

db001.example.jp:
  :roles:
    - base
    - db
  :server_id: 101
db002.example.jp:
  :roles:
    - base
    - db
  :server_id: 102

You can write Rakefile for role separated tests like this with the YAML file.

require 'rake'
require 'rspec/core/rake_task'
require 'yaml'
 
properties = YAML.load_file('properties.yml')
 
desc "Run serverspec to all hosts"
task :spec => 'serverspec:all'
 
namespace :serverspec do
  task :all => properties.keys.map {|key| 'serverspec:' + key.split('.')[0] }
  properties.keys.each do |key|
    desc "Run serverspec to #{key}"
    RSpec::Core::RakeTask.new(key.split('.')[0].to_sym) do |t|
      ENV['TARGET_HOST'] = key
      t.pattern = 'spec/{' + properties[key][:roles].join(',') + '}/*_spec.rb'
    end
  end
end

You can write spec_helper.rb to set host specific properties with set_property.

require 'serverspec'
require 'net/ssh'
require 'yaml'

properties = YAML.load_file('properties.yml')

host = ENV['TARGET_HOST']
set_property properties[host]
...

And you can use host specific properties with property like this.

require 'spec_helper'
 
describe file('/etc/my.cnf') do
  it { should contain "server-id = #{property[:server_id]}" }
end

PATH environment variable

You can add PATH environment variable in spec_helper.rb or other places like this.

set :path, '/sbin:/usr/local/sbin:$PATH'

Block scoped PATH environment variable

This feature is experimental.So this may be changed in the future.

You can also set PATH environment variable only in a block scope like this.

describe package('jekyll') do
  let(:path) { '/usr/local/rbenv/shims' }
  it { should be_installed.by('gem') }
end

Block scoped pre_command parameter

This feature is experimental.So this may be changed in the future.

You can set pre_command parameter like this.

describe service('httpd') do
  let(:pre_command) { 'source ~/.zshrc' }
  it { should be_running }
end

In this case, a command is expanded to source ~/.zshrc && service httpd status.


Parallel execution

If you have a lot of hosts, running tests on all of them can take some time. To speed up tests, they can be executed in parallel by passing -j 10 and -m flags to rake.


How to control sudo

You can disable sudo completely like this.

# In spec_helper.rb
set :disable_sudo, true

Or you can disable sudo temporarily like this.

# In spec_helper.rb
RSpec.configure do |c|
  c.around :each, sudo: false do |example|
    set :disable_sudo, true
    example.run
    set :disable_sudo, false
  end
  ...

# In xxxxx_spec.rb
describe command('whoami'), :sudo => false do
  its(:stdout) { should match 'vagrant' }
end

describe command('whoami') do
  its(:stdout) { should match 'root' }
end

Another way to disable sudo temporarily.

describe command('whoami') do
  let(:disable_sudo) { true }
  its(:stdout) { should match 'vagrant' }
end

describe command('whoami') do
  its(:stdout) { should match 'root' }
end

You can pass specific options to sudo.

For example, run command under appuser with simulate initial login.

# In spec_helper.rb
set :sudo_options, [ '-u', 'appuser', '-i' ]  # By Array
or
set :sudo_options, '-u appuser -i'  # By String

Or only in a block scope.

describe command('~/.rbenv/shims/gem list') do
  let(:sudo_options) { '-u appuser -i' }
  its(:stdout) { should contain('bundler') }
end

How to get OS information

You can get OS information of the target host by os helper method.

os[:family]  # redHat, ubuntu, debian and so on
os[:release] # OS release version
os[:arch]    # i386 or x86_64

You can use this feature like this.

if os[:family] == 'redhat'
  # RedHat bases OS related environment spec
  if os[:arch] == 'i386'
    # 32bit environment spec
  else
  end
elsif ['debian', 'ubuntu'].include?(os[:family])
  # debian related environment spec
end

Or like this.

describe file('/usr/lib64'), :if => os[:arch] == 'x86_64' do
  it { should be_directory }
end