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.
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
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:
It has the following gems and their dependencies installed:
rake
rspec
rspec_junit_formatter
serverspec
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
andRUBY_VERSION
accordingly. After installingrake
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:
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:
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
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.
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
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
)
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
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.
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
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
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.
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
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
.
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).
- "Ruby Version Manager" chapter of "Ruby on Windows Guides" book by Boško Ivanišević
- the original uru project on bitbucket
- skeleton Vagrantfile that installs and runs ruby, gem, serverspec after provision
- skeleton Vagrantfile for puppet provision
- sensu-plugins-serverspec
- automating serversped
- danger-junit Junit XML to HTML convertor
- loading spec
- enable checkboxes in the html-formatted report generated by rspec-core and rspec_junit_formatter rendered in Interner Explorer, make sure to confirm ActiveX popup. If this does not work, one may have to apply a patch explained in how IE generates the onchange event and run the
apply_filters()
ononclick
instead ofonchange
. - filtering RSpec log
- specialist
- vincentbernat/serverspec-example
- puppet/yamlfile
- cucumber-reporting
- cucumber-html
- cucumberjs-junitxml
- [cucumber-reports](https://github.com/mkolisnyk/cuc umber-reports)
- serverspec to inspec conversion example
- vagrant execute
- winrm CLI
- notes on using uru on Windows
- DSC Environment Analyzer overview, another introduction and source code
- uzyexe/serverspec Docker build env
- vagrant-serverspec Vagrant plugin
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