Skip to content

Puppet module to deploy standalone ruby environment and run serverspec on the instance for a network access locked down cloud environment

Notifications You must be signed in to change notification settings

sergueik/uru_serverspec

Repository files navigation

Uru Serverspec

Introduction

There is a challenge to run serverspec on the instance managed by Puppet or Chef for a network access locked down cloud environment after the single sign-on (SSO) a.k.a. access management software has been provisioned making remote access impossible. For example review the Enterprise BokS for Unix ssh and various vendors-specific authentication schemes e.g. milti factor authentication (MFA) for Windows logon. By design such software disables ssh and winrm ssh key-based remote access. Remote access however is critical transport mechanism the vanilla serverspec / inspec relies on for code delivery.

With the help of Ruby Version Manager and specifically uru Ruby Installer one can bootstrap a standalone Ruby environment to run serverspec directly on the instance, on either Linux or Windows. The only prerequisite on a Linux system are openssl-libs, libcrypt and libyaml libraries, those are very likely already installed for openssl stack on a generic linux box.

Another interesting use case is when Puppet provision serves as a driver of a massive deloyment of a list of microservice application stack e.g. Java jars / wars to the cluster of nodes. In this scenario, there would be a Puppet profile solely responsibe for deploying the domain specific subset of such stack, typically via a massive Puppet create_resource function featuring a heavy hiera-centric configuration lookup to pick the release version, build, checksum and various application-specific parameters of the microservices:

create_resources('microservice_deployent', lookup('profile::api::microservices'), {
  path => $app_root_path,
  tags => $tags,
})

where all the details of home brewed microservice_deployent defined type would be serialized from hiera:

profile::api::microservices:
  account_service:
    v1:
      artifact_name: 'account_service.war'
      build_number: 123
      artifact_chesksum: 'c4f5c6a37486002f56f269b5e98200a2be84a41498f698bc890b7c255eedeb74'
      artifact_chesksum_type: 'sha256'

There will likely be more than one defined type like that in a real microservices hosting project.

Apparently when the serverspec is confgured to run from the development host, this would lead to duplication of version/configuration information elsewhere which would be highly undesired.

Ideally one would like to generate a serverspec test(s) for multile Puppet managed components via some template and same hiera data from the same profile through Puppet erb or epp templates:

require 'spec_helper'

context '<%= @name -%>' do
  # define all component configuration, version parameters
  name = '<%= @name -%>'
  catalina_home = '<%= @catalina_home -%>'
  microservices = {
    <% @microservices.each do |key,data| -%>
      '<%= @key -%>' =>
        {
          'artifact_name' => '<%= data["artifact_name"] -%>',
          'artifact_chesksum' => '<%= data["artifact_chesksum"] -%>',
        },
    <% end -%>
  }
  microservices.each do |key,data|
    describe file("#{catalina_home}/webapps/#{name}/#{key}/#{data['artifact_name']}.war") do
      it {should be_file}
      its(:sha256sum) { should eq "data['artifact_chesksum']" }
    end
    describe file("#{catalina_home}/webapps/#{name}/#{key}/#{data['artifact_name']}") do
      it {should be_directory}
      # TODO: microservice configuration detail XML lookup
    end
    describe command("curl http://localhost:8443/#{catalina_home}/webapps/#{name}/health") do
      its(:stdout) { should match 'UP' }
    end
  end
end
  • the server spec itself is elementary, it builds a valid Ruby hash which mimics the hieradata schema, and possibly other Puppet scope variables describing the application comtainer details and runs a file, directory, service health and optionally some advanced configuration checks for every deployed microservice. Its only complexity arises with integration with the cluster Puppet hieradata - it is not uncommon when hundreds of microservice artifacts are deployed. Every now and then when a new microservice expectaion is designed there is a moderate complexity task of coverting it into template. There is a little downside of template based serverspec generation - it is of course that the only environment everything is assembled fully is the instance itself.

On Unix, there certainly are alternatives, but on Windows, rvm-like tools are scarcely available. The only alternative found was pik, and it is not maintained since 2012. Also, installing a full Cygwin environment on a Windows instance just to enable one to run rvm feels like an overkill.

It is no longer necessary, though still possible to run serverspec at the end of provision. To run the same set of tests locally on the insance in uru environment and remotely on the developer host in Vagrant serverspec plugin - see the example below on how to update the Vagrantfile.

A possible alternative is to add the uru_serverspec module to control repository role / through a dedicated 'test' profile (stage), which will cause Puppet to verify the modules and everything declared in the 'production' profile (stage). This is possible thanks to a special modules mount point a Puppet server serves files from every module directory as if someone had copied the files directory from every module into one big directory, renaming each of them with the name of their module. Note since acording to official Puppet guidelines role class is supposed to declare profile classes with include, and do nothing else, one is discouraged from creating files resources in the role and would likely need to place serverspec files (which are in fact, role-specific) under profiles directory.

The module uru_serverspec can be configured to execute rake spec with the serverspec files during every provision run (the default) or only when changes are detected in the ruby file or hiera configuration.

This is different from the regular Puppet module behavior, therefore the full Puppet run will not be idempotent, but this reflects the module purpose.

When moving to production, this behavior can be suppressed through module parameters. Alternatively, the 'test' stage where the module is declared, can be disabled, or the class simply can be managed through hiera_include to not bepresent in production environment.

On the other hand, exactly because the module ability of being not idempotent, one can use uru_serverspec for the same tasks the Chef Inspec is used today.

To continue running serverspec through vagrant-serverspec plugin, one would have to update the path of the rspec files in the Vagrantfile pointing it to inside the module files e.g. since serverspec are strongly platform-specific, use the instance's Vagrant config.vm.box or the arch (defined elsewhere) to choose the correct spec file for the instance:

arch = config.vm.box || 'linux'
config.vm.provision :serverspec do |spec|
  if File.exists?("spec/#{arch}")
    spec.pattern = "spec/#{arch}/*_spec.rb"
  elseif File.exists?("files/serverspec/#{arch}")
    spec.pattern = "files/serverspec/#{arch}/*_spec.rb"
  end
end

The uru_serverspec module can collect serverspec resources from other modules's via Puppet's puppet:///modules URI and the Puppet file resource:

file {'spec/local':
  ensure              => directory,
  path                => "${tool_root}/spec/local",
  recurse             => true,
  source              => $::uru_serverspec::covered_modules.map |$name| {
    "puppet:///modules/${name}/serverspec/${::osfamily}"
  },
  source_permissions => ignore,
  sourceselect        => all,
}

Alternatively when using roles and profiles, the uru module can collect serverspec files from the profile: /site/profile/files which is also accessible via puppet:///modules URI.

file {'spec/local':
  ensure              => directory,
  path                => "${tool_root}/spec/local",
  recurse             => true,
  source              => $::uru_serverspec::server_roles.map |$server_role| {"puppet:///modules/profile//serverspec/roles/${server_role}" },
  source_permissions => ignore,
  sourceselect       => all,
}

This mechanism relies on Puppet file type and its 'sourceselect' attribute. Regrettably no similar URI for roles: puppet:///modules/roles/serverspec/${role} can be constructed, though logically the serverspec are more appropriate to define per-role, than per-profile.

One can combine the two globs in one attribute definition:

  if ($profile_serverspec =~ /\w+/) {
    $use_profile = true
  } else {
    $use_profile = false
  }
  ...
  #lint:ignore:selector_inside_resource
  source => $use_profile ? {
  true    => "puppet:///modules/profile/serverspec/roles/${profile_serverspec}",
   default => $::uru_serverspec::covered_modules.map |$item| { "puppet:///modules/${item}" },
  },
  #lint:endignore

and also one can combine the narrow platform-specific tests and tests common to different platform releases separately to reduce the redundancy like below:

$serverspec_directories =  unique(flatten([$::uru_serverspec::covered_modules.map |$module_name| { "${module_name}/serverspec/${osfamily_platform}" }, $::testing_framework::covered_modules.map |$module_name| { "${module_name}/serverspec/${::osfamily}" }]))

Then it does the same with types

  # Populate the type directory with custom types from all covered modules
  file { 'spec/type':
    ensure             => directory,
    path               => "${tool_root}/spec/type",
    recurse            => true,
    source             => $::uru_serverspec::covered_modules.map |$module_name| { "puppet:///modules/${module_name}/serverspec/type" },
    source_permissions => ignore,
    sourceselect       => all,
  }

No equivalent mechanism of scanning the cookbooks is implemented with Chef yet AFAIK.

Note that using Ruby require_relative one can make the serverspec file located within the Puppet recommended module directory structure be included into another serverspec file and executed from the developer host during the node provision through vagrant-serverspec plugin. The exact instruction varies with the location of the serverspec which is often a non-standard one.

Providing Versions via Template

For extracting the versions one can utilize the following parameter

 version_template => $::uru::version_template ? {
  /\w+/   => $::uru::sut_role ? {
    /\w+/   => $::uru::version_template,
    default => ''  
  },  
  default => ''
  }

Puppet resource

  # Write the versions from caller provided template - only works for roles
  if $version_template =~ /\w+/ {   
    file { 'spec/local/versions.rb':
      ensure             => file,
      path               => "${tool_root}/spec/local/versions.rb",
      content            => template($version_template),
      source_permissions => ignore,
      require            => [File['spec/local']],
      before             => [File['runner']],
    } 
    }

template

$sut_version = '<%= scope.lookupvar("sut_version") -%>'

and rspec conditional include:

if File.exists?( 'spec/local/versions.rb') 
  require_relative 'versions.rb'
  puts "defined #{$sut_version}"
end

Internals

One could provision uru_serverspec environment from a zip/tar archive, one can also construct a Puppet module for the same. This is a lightweight alternative to DracoBlue/puppet-rvm module, which is likely need to build Ruby from source anyway.

The $URU_HOME home directory with Ruby runtime plus a handful of gems has the following structure: uru folder uru folder

It has the following gems and their dependencies installed:

rake
rspec
rspec_junit_formatter
serverspec

Setup

For windows, uru.zip can be created by doing a fresh install of uru and binary install of Ruby performed on a node with internet access or on a developer host, and installing all dependency gems from a sandbox Ruby instance into the $URU_HOME folder:

uru_rt.exe admin add ruby\bin
uru_rx.exe gem install --no-rdoc --no-ri serverspec rspec rake json rspec_junit_formatter

and zip the directory.

NOTE: running uru in a free Vmware instances provided by Microsoft for IE/Edge testing, one may need to add the ffi.gem which in turn may require installing Ruby DevKit within uru environment:

cd c:\uru
uru ls
>> 218p440     : ruby 2.1.8p440 (2015-12-16 revision 53160) [i386-mingw32]
uru.bat 218p440
cd c:\devkit
devkitvars.bat
>> Adding the DevKit to PATH...
cd c:\uru
gem install %USERPROFILE%\Downloads\ffi-1.9.18.gem

On Linux, the tarball creation starts with compiling Ruby from source, configured with a prefix ${URU_HOME}/ruby:

export URU_HOME='/uru'
export RUBY_VERSION='2.5.1'
export RUBY_RELEASE='2.5'

cd $URU_HOME
wget https://cache.ruby-lang.org/pub/ruby/${RUBY_RELEASE}/ruby-${RUBY_VERSION}.tar.gz
tar xzvf ruby-${RUBY_VERSION}.tar.gz

followed by on Centos

yum groupinstall -y 'Developer Tools'
yum install -y zlib-devel openssl-devel libyaml-devel

and on Ubuntu

apt-get install -y zlib1g-dev libssl-dev libyaml-dev

followed by

pushd ruby-${RUBY_VERSION}
./configure --prefix=${URU_HOME}/ruby --disable-install-rdoc --disable-install-doc
make clean
make
rm -fr  ruby
sudo make install

Next one is to check the page https://bitbucket.org/jonforums/uru/downloads/ for the latest available version of uru_rt:

curl -L -k   https://bitbucket.org/jonforums/uru/downloads/uru.json  | grep '"version":'

and install binary distribution of uru

export URU_HOME='/uru'
export URU_VERSION='0.8.5'
pushd $URU_HOME
wget https://bitbucket.org/jonforums/uru/downloads/uru-${URU_VERSION}-linux-x86.tar.gz
tar xzvf uru-${URU_VERSION}-linux-x86.tar.gz

After Ruby and__uru__is installed one switches to the isolated environment and installs the required gem dependencies

./uru_rt admin add ruby/bin
---> Registered ruby at `/uru/ruby/bin` as `251p57`
./uru_rt ls
251p57      : ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
export URU_INVOKER=bash
./uru_rt 251p57
---> now using ruby 2.5.1-p57 tagged as `251p57`
./uru_rt gem list
./uru_rt gem install --no-ri --no-rdoc rspec serverspec rake rspec_junit_formatter yamllint rexml
cp -R ~/.gem .

Finally the $URU_HOME is converted to an archive, that can be provisioned on a clean system.

NOTE: with $GEM_HOME one can make sure gems are installed under .gems rather then the into a hidden $HOME/.gem directory. This may not work correctly with some releases of uru. To verify, run the command on a system uru is provisioned from the tarball:

./uru_rt gem list --local --verbose

If the list of gems is shorter than expected, e.g. only the following gems are listed,

bigdecimal (1.2.4)
io-console (0.4.3)
json (1.8.1)
minitest (4.7.5)
psych (2.0.5)
rake (13.0.6)
rdoc (4.1.0)
test-unit (2.1.10.0)

the ${URU_HOME}\.gem directory may need to get copied to ${HOME}

  • update the RAKE_VERSION, GEM_VERSION and RUBY_VERSION accordingly. After installing rake gem it may need to get copied
cp -R  ~/.gem/ruby/2.5.0/gems/rake-13.0.6 ruby/lib/ruby/gems/2.5.0/gems/

If the error

<internal:gem_prelude>:1:in `require': cannot load such file -- rubygems.rb (LoadError)

is observed, note that you have to unpackage the archive uru.tar.gz into the same $URU_HOME path which was configured when Ruby was compiled. Note: rvm is known to give the same error if the .rvm diredctory location was changed .

In the spec directory there is a trimmed down windows_spec_helper.rb and spec_helper.rb required for serverspec gem:

require 'serverspec'
set :backend, :cmd

and a vanilla Rakefile generated by serverspec init

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

task :spec    => 'spec:all'
task :default => :spec

namespace :spec do
  targets = []
  Dir.glob('./spec/*').each do |dir|
    next unless File.directory?(dir)
    target = File.basename(dir)
    target = "_#{target}" if target == 'default'
    targets << target
  end

  task :all     => targets
  task :default => :all

  targets.each do |target|
    original_target = target == '_default' ? target[1..-1] : target
    desc "Run serverspec tests to #{original_target}"
    RSpec::Core::RakeTask.new(target.to_sym) do |t|
      ENV['TARGET_HOST'] = original_target
      t.rspec_opts = "--format documentation --format html --out reports/report_#{$host}.html --format json --out reports/report_#{$host}.json"
      t.pattern = "spec/#{original_target}/*_spec.rb"
    end
  end
end

with a formatting option added:

t.rspec_opts = "--format documentation --format html --out reports/report_#{$host}.html --format json --out reports/report_#{$host}.json"

This would enforce verbose formatting of rspec result logging and let rspec generate standard HTML and json rspec reports. One can use to produce junit XML reports.

The spec/local directory can contain arbitrary number of domain-specific spec files, as explained above. The uru module contains a basic serverspec file uru_spec.rb that serves as a smoke test of the uru environment:

Linux:

require 'spec_helper'
context 'uru smoke test' do
  context 'basic os' do
    describe port(22) do
      it { should be_listening.with('tcp')  }
    end
  end
  context 'detect uru environment' do
    uru_home = '/uru'
    gem_version='2.1.0'
    user_home = '/root'
    describe command('echo $PATH') do
      its(:stdout) { should match Regexp.new("_U1_:#{user_home}/.gem/ruby/#{gem_version}/bin:#{uru_home}/ruby/bin:_U2_:") }
    end
  end
end

Windows:

require 'spec_helper'
context 'basic tests' do
  describe port(3389) do
    it do
     should be_listening.with('tcp')
     should be_listening.with('udp')
    end
  end

  describe file('c:/windows') do
    it { should be_directory }
  end
end
context 'detect uru environment through a custom PATH prefix' do
  describe command(<<-EOF
   pushd env:
   dir 'PATH' | format-list
   popd
    EOF
  ) do
    its(:stdout) { should match Regexp.new('_U1_;c:\\\\uru\\\\ruby\\\\bin;_U2_;', Regexp::IGNORECASE) }
  end
end

but any domain-specific serverspec files can be placed into the spec/local folder.

There should be no nested subdirectories in spec/local. If there are subdirectories, their contents will be silently ignored.

Finally in ${URU_HOME} there is a platform-specific bootstrap script:

runner.ps1 for Windows:

$URU_HOME = 'c:/uru'
$GEM_VERSION = '2.1.0'
$RAKE_VERSION = '10.1.0'
pushd $URU_HOME
uru_rt.exe admin add ruby\bin
$env:URU_INVOKER = 'powershell'
.\uru_rt.exe ls --verbose
$TAG = (invoke-expression -command 'uru_rt.exe ls') -replace '^\s+\b(\w+)\b.*$', '$1'
.\uru_rt.exe $TAG
.\uru_rt.exe ruby ruby\lib\ruby\gems\${GEM_VERSION}\gems\rake-${RAKE_VERSION}\bin\rake spec

runner.sh for Linux:

#!/bin/sh
export URU_HOME=/uru
export GEM_VERSION='2.1.0'
export RAKE_VERSION='10.1.0'

export URU_INVOKER=bash
pushd $URU_HOME
./uru_rt admin add ruby/bin
./uru_rt ls --verbose
export TAG=$(./uru_rt ls 2>& 1|awk -e '{print $1}')
./uru_rt $TAG
./uru_rt gem list
./uru_rt ruby ruby/lib/ruby/gems/${GEM_VERSION}/gems/rake-${RAKE_VERSION}/bin/rake spec

The results are nicely formatted in a standalone HTML report:

resultt

and as json:

{
    "version": "3.5.0.beta4",
    "examples": [{
        "description": "should be directory",
        "full_description": "File \"c:/windows\" should be directory",
        "status": "passed",
        "file_path": "./spec/local/windows_spec.rb",
        "line_number": 4,
        "run_time": 0.470411,
        "pending_message": null
    }, {
        "description": "should be file",
        "full_description": "File \"c:/test\" should be file",
        "status": "failed",
        "file_path": "./spec/local/windows_spec.rb",
        "line_number": 8,
        "run_time": 0.545683,
        "pending_message": null,
        "exception": {
            "class": "RSpec::Expectations::ExpectationNotMetError",
            ...
        }
    }],
    "summary": {
        "duration": 1.054691,
        "example_count": 2,
        "failure_count": 1,
        "pending_count": 0
    },
    "summary_line": "2 examples, 1 failure"
}

One can easily extract the stats by spec file, descriptions of the failed tests and the overall summary_line from the json to stdout to get it captured in the console log useful for CI:

report_json = File.read('results/report_.json')
report_obj = JSON.parse(report_json)

puts 'Failed tests':
report_obj['examples'].each do |example|
  if example['status'] !~ /passed|pending/i
    pp [example['status'],example['full_description']]
  end
end

stats = {}
result_obj[:examples].each do |example|
  file_path = example[:file_path]
  unless stats.has_key?(file_path)
    stats[file_path] = { :passed => 0, :failed => 0, :pending => 0 }
  end
  stats[file_path][example[:status].to_sym] = stats[file_path][example[:status].to_sym] + 1
end
puts 'Stats:'
stats.each do |file_path,val|
  puts file_path + ' ' + (val[:passed] / (val[:passed] + val[:pending] + val[:failed])).floor.to_s + ' %'
end

puts 'Summary:'
pp result_obj[:summary_line]

To execute these one has to involve uru_rt. Linux:

./uru_rt admin add ruby/bin/ ; ./uru_rt ruby processor.rb --no-warnings --maxcount 100

Windows:

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -executionpolicy remotesigned  ^
-Command "& {  \$env:URU_INVOKER = 'powershell'; invoke-expression -command 'uru_rt.exe admin add ruby/bin/' ; invoke-expression -command 'uru_rt.exe ruby processor.rb --no-warnings --maxcount 100'}"

Alternatively on Windows one can process the result.json in pure Powewrshell:

param(
  [Parameter(Mandatory = $false)]
  [string]$name = 'result.json',
  [Parameter(Mandatory = $false)]
  [string]$directory = 'results',
  [Parameter(Mandatory = $false)]
  [string]$serverspec = 'spec\local',
  [int]$maxcount = 100,
  [switch]$warnings
)

$statuses = @('passed')

if ( -not ([bool]$PSBoundParameters['warnings'].IsPresent )) {
  $statuses += 'pending'
}

$statuses_regexp = '(?:' + ( $statuses -join '|' ) +')'

$results_path = ("${directory}/${name}" -replace '/' , '\');
if (-not (Test-Path $results_path)) {
  write-output ('Results is unavailable: "{0}"' -f $results_path )
  exit 0
}
if ($host.Version.Major -gt 2) {
  $results_obj = Get-Content -Path $results_path | ConvertFrom-Json ;
  $count = 0
  foreach ($example in $results_obj.'examples') {
    if ( -not ( $example.'status' -match $statuses_regexp )) {
      # get few non-blank lines of the description
      # e.g. when the failed test is an inline command w/o a wrapping context
      $full_description = $example.'full_description'
      if ($full_description -match '\n|\\n' ){
        $short_Description = ( $full_description -split '\n|\\n' | where-object { $_ -notlike '\s*' } |select-object -first 2 ) -join ' '
      } else {
        $short_Description = $full_description
      }
      Write-Output ("Test : {0}`r`nStatus: {1}" -f $short_Description,($example.'status'))
      $count++;
      if (($maxcount -ne 0) -and ($maxcount -lt $count)) {
        break
      }
    }
  }
  # compute stats -
  # NOTE: there is no outer context information in the `result.json`
  $stats = @{}
  $props =  @{
    Passed = 0
    Failed = 0
    Pending = 0
  }
  foreach ($example in $results_obj.'examples') {
    $spec_path = $example.'file_path'
    if (-not $stats.ContainsKey($spec_path)) {
      $stats.Add($spec_path, (New-Object -TypeName PSObject -Property $props ))
    }
    # Unable to index into an object of type System.Management.Automation.PSObject
    $stats[$spec_path].$($example.'status') ++

  }

  write-output 'Stats:'
  $stats.Keys | ForEach-Object {
    $spec_path = $_
    # extract outermost context from spec:
    $context_line = select-string -pattern @"
context ['"].+['"] do
"@ -path $spec_path | select-object -first 1
    $context = $context_line -replace @"
^.+context\s+['"]([^"']+)['"]\s+do\s*$
"@, '$1'
    # NOTE: single quotes needed in the replacement
    $number_examples = $stats[$spec_path]
    # not counting pending examples
    # $total_number_examples = $number_examples.Passed + $number_examples.Pending + $number_examples.Failed
    $total_number_examples = $number_examples.Passed + $number_examples.Failed
    Write-Output ("{0}`t{1}%`t{2}" -f ( $spec_path -replace '^.+[\\/]','' ),([math]::round(100.00 * $number_examples.Passed / $total_number_examples,2)), $context)
  }
  write-output 'Summary:'
  Write-Output ($results_obj.'summary_line')
} else {
  Write-Output (((Get-Content -Path $results_path) -replace '.+\"summary_line\"' , 'serverspec result: ' ) -replace '}', '' )
}

For convenience the processor.ps1 and processor.rb, and processor.sh are provided. Finding and converting to a better structured HTML report layout with the help of additional gems is a work in progress.

The Puppet module is available in a sibling directory:

Specifying filename of the serverspec Report

As default, the report file names results are saved are results_.json and results.html. The argument allows overriding this: On Linux:

./runner.sh myresult.json
Results in results/myresult.json

then

./processor.sh myresult.json
No failed tests.
Summary: "3 examples, 0 failures"

or

./uru_rt ruby processor.rb --results_filename myresult.json
Reading: results/myresult.json
"3 examples, 0 failures"

On Windows:

. .\runner.ps1 myresult.json
DEBUG: results in results/myresult.json

and

. .\processor.ps1 -results_filename myresult.json

or, alternatively

 .\uru_rt.exe ruby .\processor.rb --results_filename myresult.json

Specifying user-sensitive tests

The most natural use case ofspecfying the file name of the serverpsec report is when there are user sensitive validations. The fragment below demonstrates this:

require 'spec_helper'

context 'user sensitive' do
  root_home = '/root'
  # condition at the 'describe' level
  context 'home directory' do
    describe command('echo $HOME'), :if => ENV.fetch('USER').eql?('root') do
      its(:stdout) { should match Regexp.new(root_home) }
    end
    describe command('echo $HOME'), :unless => ENV.fetch('USER').eql?('root') do
      its(:stdout) { should_not match Regexp.new(root_home) }
    end
  end
  # condition at the 'context' level
  context 'home directory', :if => ENV.fetch('USER').eql?('root') do
    describe command('echo $HOME') do
      its(:stdout) { should match Regexp.new(root_home) }
    end
  end
  context 'home directory', :unless => ENV.fetch('USER').eql?('root') do
    describe command('echo $HOME') do
      its(:stdout) { should_not match Regexp.new(root_home) }
    end
  end
  # include branch condition in the 'title' property
  context "home directory of #{ENV.fetch('USER')}" do
    describe command('echo $HOME') do
      its(:stdout) { should_not be_empty }
    end
  end
end

the equivalent code for Windows is awork in progress.

Migration

To migrate serverspec from a the vagrant-serverspec default directory, one may use require_relative. Also pay attention to use a conditional

if File.exists?( 'spec/windows_spec_helper.rb')
  require_relative '../windows_spec_helper'
end

in the serverspec in the Ruby sandbox if the same rspec test is about to be run from Vagrant and from the instance

Useful modifiers

To detect Vagrant run :

user_home = ENV.has_key?('VAGRANT_EXECUTABLE') ? 'c:/users/vagrant' : ( 'c:/users/' + ENV['USER'] )

This will assign a hard coded user name versus target instance environment value to Ruby variable. Note: ENV['HOME'] was not used - it is defined in both cygwin (C:\cygwin\home\vagrant) and Windows environments (C:\users\vagrant)

To detect__uru__runtime:

context 'URU_INVOKER environment variable', :if => ENV.has_key?('URU_INVOKER')  do
  describe command(<<-EOF
   pushd env:
   dir 'URU_INVOKER' | format-list
   popd
    EOF
  ) do
    its(:stdout) { should match /powershell|bash/i }
  end
end

As usual, one can provide custom types in the spec/type directory - that directory is excluded from the spec run. For example one can define the following class property_file.rb to inspect property files:

require 'serverspec'
require 'serverspec/type/base'
module Serverspec::Type
  class PropertyFile < Base

    def initialize(name)
      @name = name
      @runner = Specinfra::Runner
    end

    def has_property?(propertyName, propertyValue)
      properties = {}
      IO.foreach(@name) do |line|
        if (!line.start_with?('#'))
          properties[$1.strip] = $2 if line =~ /^([^=]*)=(?: *)(.*)/
        end
      end
      properties[propertyName] == propertyValue
    end
  end

  def property_file(name)
    PropertyFile.new(name)
  end
end

include Serverspec::Type

and create the test

require 'type/property_file'
context 'Custom Type' do
  property_file_path = "#{user_home}/sample.properties"
  describe property_file(property_file_path) do
    it { should have_property('package.class.property', 'value' ) }
  end
end

Parameters

To pass parameters to the serverspec use hieradata

---
uru::parameters:
  dummy1:
    key: 'key1'
    value: 'value1'
    comment: 'comment'
  dummy2:
    key: 'key2'
    value:
    - 'value2'
    - 'value3'
    - 'value4'
  dummy3:
    key: 'key3/key4'
    value: 'value5'

The processing of the hieradata is implemented in the standard way:

  $default_attributes = {
    target  => "${toolspath}/spec/config/parameters.yaml",
    require => File["${toolspath}/spec/multiple"],
  }

  $parameters = hiera_hash('uru::parameters')
  $parameters.each |$key, $values| {
    create_resources('yaml_setting',
      {
        $key => delete($values, ['comment'])
      },
      $default_attributes
    )
  }

This will produce the file /uru/spec/config/parameters.yaml on the instance with the following contents:

---
key1: 'value1'
key2:
- 'value2'
- 'value3'
- 'value4'
key3:
  key4: 'value5'

The unique dummy* keys from hieradata/common.yaml disappear - they exist for Puppet create_resources needs only. The optional comment key is ignored. Note usage of yamlfile Puppet module syntax for nested keys.

The following fragment demonstrates the use spec/config/parameters.yaml in serverspec:

if ENV.has_key?('URU_INVOKER')
  parameters = YAML.load_file(File.join(__dir__, '../config/parameters.yaml'))
  value1 = parameters['key1']
end

Note: the Rspec metadata-derived serverspec syntax

context 'Uru-specific context', :if => ENV.has_key?('URU_INVOKER') do
  # uru-specific code
end

does not block YAML.load_file execution outside of uru-specific context and not to be used for this case - a plain Ruby conditon will do.

Compiling from the source

To compile uru package download ruby source from https://www.ruby-lang.org/en/downloads/, build and install Ruby into /uru/ruby:

pushd /uru/ruby-2.3.6
./configure --disable-install-capi --disable-install-rdoc --disable-install-doc --without-tk  --prefix=/uru/ruby
make; make install

then register with uru package

./uru_rt admin add /uru/ruby/bin
---> Registered ruby at `/uru/ruby/bin` as `236p384`

and update the runner.sh

e.g. for Ruby 2.3.6 add

GEM_VERSION='2.3.0'
RAKE_VERSION='10.4.2'
RUBY_VERSION='2.3.6'
RUBY_VERSION_LONG='2.3.6p384'
RUBY_TAG_LABEL='236p384'

and install the gems:

 ./uru_rt gem install --no-rdoc --no-ri specinfra serverspec rake rspec rspec_junit_formatter json nokogiri

Finally package the directory, and verify it works on a vanila node:

cd /
tar czvf ~sergueik/Downloads/uru_ruby_236.tar.gz /uru
rm -rv -f uru/
which ruby
tar xzvf ~sergueik/Downloads/uru_ruby_236.tar.gz
pushd /uru/
./runner.sh
# will report test passed

Note

The RSpec format options provided in the Rakefile

rspec_opts = "--require spec_helper --format documentation --format html --out results/result_#{$host}.html --format json --out results/result_#{$host}.json"

are not compatible with Vagrant serverspc plugin, leading to the following error:

The
serverspec provisioner:
* The following settings shouldn't exist: rspec_opts

Inspec

It is possible to install the inspec.gem for Chef Inspec in the uru environment and repackage and use in the similar fashion, use case as with serverspec. Note for mixlib-shellout you will need to use Ruby 2.2.x To build dependency gems one will need to execute

sudo apt-get install build-essential

or

sudo yum install make automake gcc gcc-c++ kernel-devel

Note: serverspec and inspec appear to use very similar Rakefile and auxiliary Ruby files. Switch from one to the other was not fully tested yet.

Puppet Beaker Integration testing tool

Recently, Puppet switched to use Beaker to wrap Vagrant(Docker) and Serverspec to provision the instance(s), iterate across supported target platforms often performing mutiple consecutive puppet agent runs, and inspect the catalogs compilation and catalogs themselves using core Beaker DSL and various extentions to produce tests which are

  • geared to deal more with catalog than with the system
  • good for module developers by exploring methods like apply_manifests get_last_applied_resources and apparently somewhat heavily Rails metaprogramming-style expectations like:
require 'spec_helper_acceptance'
it 'should run without any errors' do
  base_pp = <<-EOF
    include stdlib
  EOF
  {
    1 => 2,
    2 => 0,
  }.eacho do |run, status|
    apply_manifest(base_pp,
    :modulepath => '/etc/puppetlabs/code/modules',
    :debug      => true,
    :catch_failures => true).exit_code).to eq status
  end
end
  • sampling valid, generic but really vague expectation, that conveys nothing about the error it might find and producing result that would only be legible to the developer of the module in question
  • somewhat formal and focused entirely on the Puppet catalog, prone of overlooking creation of damaged target systems

Non-root account

The initial version of uru_serverspec module the Ruby has been compiled and packaged by the root account. To switch uru_serverspec module to operate under a non-root user the simpest way is to

  • Copy the .gem folder into the target user home directory:
URU_USER='uru_user'
adduser $URU_USER
cp -R /root/.gem/ ~$URU_USER/
  • Adjust files and directories ownership:
chown -R $URU_USER:$URU_USER ~$URU_USER/.gem/
chown -R $URU_USER:$URU_USER $URU_HOME

Now the spec can be run by $URU_USER.

Alternatives and comparison

The well-known technology that existed already by the time the uru_serverspec module was designed is vagrant-serverspec Vagrant plugin.

The vagrant-serverspec Vagrant plugin is at the low level simply a ruby package behaving and designed like a regular Ruby gem, but housed under Vagrant application directory and installed / removed via designed vagrant plugin command.

The execution of the server spec is integrated in Vagrant workflow to take place after the provision:

config.vm.provision :serverspec do |spec|
  spec.pattern = '*_spec.rb'
  # configuration details omitted
end

This automates running serverspec from the Ruby runtime installed on the host machine remotely into the node. This only possible if the remoting (ssh or winrm) is still enabled after the node provision. To rerun a fixed spec one has to switch to command line and run

rake spec

directly, which is not much different then running the uru launcher shell script. Also, the vagrant-serverspec Vagrant plugin does not run tests after every failing provision, though this may be configurable matter. The second important difference is the spec files exercised through uru_serverspec module can be easier generated by the configuration management framework responsible for the node provision, usign the same inputs, therefore validation of the criteria like catalina jar/war artifact checksums can be integrated. The same effect may be achieved via retrofitting the vagrant-serverspec directory structure and making the outside-of module serverspec directory spec file some_spec.rb effectively a loader of the inside of module real_spec.rb :

require_relative '../../files/serverspec/rhel/real_spec'

A similar approach is often taken when refactoring similar tests into a smaller number of redundant files. Finally, it turns out quite often, the target node is the best way to develop the spec in question, especially when it aevaluates some application stack specific detail that only exists in its final form on the node, inside the application. This is especially true with configuration management tools that are prone to segregate the templates and variables (like Puppet or Chef).

See Also

NOTE: the operations does not (and actually cannot) directly follow a "Patch README document" which typically reads like below:

  • Shutdown the server if you have already
  • Copy patch files into their destinations (like $APP_HOME/repository/components/patches
  • Inject specific entries so configurations
  • Modify command line options to launchers
  • Do specific changes to systemd unit files
  • Restart the service with a provided command

This does not directly translate into the Puppet (Chef, Ansible, Powershell DSC, name your provision schema) workflow for many reasons

  • vendor specific API / DSL for connecting resourcesand for code / parameter segregation (members of your team are all Puppet certified, aren't they)
  • In-house best practices for structuring the configurations hierarchically
  • If the Patch101 was already puppetized, the Patch102 will most likely be a a copy paste of Patch101

Author

Serguei Kouzmine

About

Puppet module to deploy standalone ruby environment and run serverspec on the instance for a network access locked down cloud environment

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published