I have a fairly basic puppet module for a webservice running tomcat. I want to setup logrotate on Tomcat's catalina.out file, and I want to start by writing a test that confirms logrotate is included in the module and setup with the correct settings.
Here's a stripped down version of my webservice.pp, for example:
class my_module::webservice (
...
){
include ::tomcat_server
...
logrotate::rule { 'tomcat':
path => '/var/log/tomcat/catalina.out',
rotate => 1,
rotate_every => 'day',
copytruncate => true,
missingok => true,
compress => true,
delaycompress => true,
}
}
and I have included the logrotate forge module in my .fixtures.yml like so:
fixtures:
forge_modules:
logrotate:
repo: 'puppet-logrotate'
ref: '3.2.1'
...
But I can only write a test that confirms that logrotate is included in the module like so:
require 'spec_helper'
describe 'my_module::webservice' do
on_supported_os.each do |os, os_facts|
context "on #{os}" do
let(:facts) { os_facts }
it { is_expected.to compile }
it { is_expected.to contain_class('logrotate') }
end
end
end
This doesn't work (if I remove the logrotate block from init.pp then the tests still pass):
it { is_expected.to contain_class('logrotate::conf') }
nor does asking for with:
it { is_expected.to contain_class('logrotate') \
.with('path' => '/var/log/tomcat/catalina.out',
'rotate' => 1,
'rotate_every' => 'day',
'copytruncate' => true,
'missingok' => true,
'compress' => true,
'delaycompress' => true,
)
}
and nor does a separate/nested describe block:
describe 'logrotate::rule' do
let(:title) { 'tomcat' }
let(:params) do
{
'path' => '/var/log/tomcat/catalina.out',
'rotate' => 1,
'rotate_every' => 'day',
'copytruncate' => true,
'missingok' => true,
'compress' => true,
'delaycompress' => true,
}
end
end
I can't find anything in the rspec docs that mention anything other than testing the class is defined. Is it even possible to do what I am trying to do?
Here is my directory layout:
puppet
`- modules
`- my_module
|- data
|- manifests
| |- init.pp
| `- webservice.pp
|- spec
| |- classes
| | `- webservice_spec.rb
| `- spec_helper.rb
|- .fixtures.yml
|- Gemfile
|- hiera.yaml
|- metadata.json
`- Rakefile
I have a fairly basic puppet module for a webservice running tomcat. I want to setup logrotate on Tomcat's catalina.out file, and I want to start by writing a test that confirms logrotate is included in the module and setup with the correct settings.
That sounds very reasonable. However, this ...
Here's a stripped down version of my init.pp, for example:
class my_module::webservice (
...
){
... is at best poor practice. If it exists at all then the init.pp manifest of module my_module should define only class my_module. A class named my_module::webservice should instead be defined in a manifest named webservice.pp in module my_module. The expectations for module layout are documented in the Puppet online documentation. Although you might be able to get away with certain discrepancies from those specifications, there is only downside to doing so.
At this point I observe that "inner class" is not idiomatic Puppet terminology, and it suggests a misunderstanding of what you're working with. Specifically, this ...
logrotate::rule { 'tomcat':
[...]
... does not declare a class at all, but rather declares a resource of type logrotate::rule, which is apparently a defined type provided by the puppet/logrotate module. In general, declaring a resource does not imply anything about classes from the module (if any) that provides the resource's type.
Furthermore, although it is entirely possible that declaring a logrotate::rule resource does cause class logrotate to be included in the catalog too, that would be an implementation detail of logrotate::rule, and as such, your spec tests should not be testing for it. Only if my_module::webservice is expected to itself declare class logrotate should its tests be checking for that.
You go on to say:
This doesn't work (if I remove the logrotate block from init.pp then
the tests still pass):
it { is_expected.to contain_class('logrotate::conf') }
You haven't presented enough code for us to determine why the tests pass when that is included in them, but something is very strange if ever that expectation is satisfied. logrotate::conf is also a defined (resource) type, not a class, so that expectation should never succeed. And following a theme I introduced above, if class my_module::webservice does not declare any logrotate::conf resource directly then its tests should not be checking for one.
nor does asking for with:
it { is_expected.to contain_class('logrotate') \
.with('path' => '/var/log/tomcat/catalina.out',
'rotate' => 1,
'rotate_every' => 'day',
'copytruncate' => true,
'missingok' => true,
'compress' => true,
'delaycompress' => true,
)
}
Of course that doesn't succeed. It expresses an expectation of a declaration of class logrotate, but what you've actually declared is a resource of type logrotate::rule. Even if logrotate::rule did declare logrotate, one would not expect it to pass on its own parameter list.
and nor does a separate/nested describe block:
describe 'logrotate::rule' do
[...]
Again, that's not surprising. Such a describe block tells RSpec that logrotate::rule is the class under test. Not only is it not the class under test (that is of course my_module::webservice), but, again, logrotate::rule is not a class at all. RSpec can certainly test defined types, too, but that's not what you're after here.
To test whether a resource is declared by the class under test, one uses a predicate of the form contain_type(title), where any namespace separators (::) in the type name are replaced by double underscores. For example:
it do
is_expected.to contain_logrotate__rule('tomcat')
end
It is permitted, but optional, to include one or more with clauses to specify expectations of the declared parameters of the designated resource. Following what you appear to have been trying to do, then, maybe this would more fully express what you're looking for:
require 'spec_helper'
describe 'my_module::webservice' do
on_supported_os.each do |os, os_facts|
context "on #{os}" do
let(:facts) { os_facts }
it do
is_expected.to compile
is_expected.to contain_logrotate__rule('tomcat')
.with(
path: '/var/log/tomcat/catalina.out',
rotate: 1,
rotate_every: 'day',
copytruncate: true,
missingok: true,
compress: true,
delaycompress: true
)
end
end
end
end
Do note, by the way, that when you want to test multiple predicates against the same example, it is substantially more efficient to group them together in the same it block, as demonstrated above, than to put each in its own it block. As in, you will probably notice the difference in test running time even from combining just two it blocks into one.
Additionally, my example above demonstrates coding style close to that required to avoid warnings from pdk validate, which brings us to an additional point: it is always useful to verify that pdk validate completes without errors or warnings before trying the unit tests. You will probably find that it is excessively picky about both Puppet and Ruby code style, but it will also pick up some issues that lead to mysterious test failures. Also, it runs much faster than the tests do, and it will pick up substantially all syntax errors in both Puppet and Ruby code. It's frustrating to have your tests take a long time to fail on account of a minor syntax error.
Related
I am writing unit tests for our puppet code (puppet 3.8). I have a variable set by data in hiera. For example, I have this code in puppet:
# globals value coming from hiera
$status = $globals['yum']['status']
if $status =~ /on/ {
service { 'yum-cron':
ensure => 'running',
enable => true,
hasrestart => true,
require => [ Package['yum-cron'], File['/var/lock/subsys/'] ]
}
} else {
service { 'yum-cron':
ensure => 'stopped',
enable => false,
hasrestart => true,
require => Package['yum-cron'],
}
file {'/var/lock/subsys/yum-cron':
ensure => 'absent',
require => Package['yum-cron'],
}
}
In my rspec test file, I have the following code to test both the parts of the if/else:
context 'If the globals yum status = on' do
it 'The service resource yum-cron should exist' do
is_expected.to contain_service('yum-cron').with(
ensure: 'running',
enable: true,
hasrestart: true,
require: ['Package[yum-cron]', 'File[/var/lock/subsys/]' ]
)
end
end
context 'If the globals yum status = off' do
let(:status) {'off'}
it 'The service resource yum-cron should NOT exist' do
is_expected.to contain_service('yum-cron').with(
ensure: 'stopped',
enable: false,
hasrestart: true,
require: 'Package[yum-cron]'
)
end
end
No matter what I do in my xxx_setup.rb file to test both parts of the if/else statement, only the part that matches the value coming from hiera tests successfully. Because the value from hiera sets the value of $status to "on", that section evaluates successfully in the rspec test code. But the section where I try to test for the value of $status to be "off" fails no matter how I try to set the value of the status variable in rspec. When the puppet catalog is generated, it seems to generate only the section that matches what is in hiera and not what I set the $status variable to in rspec.
What am I missing?
let(:status) in your rspec code is just setting a local variable status, it is not setting the global $status. What's more, your puppet code is setting that $status global at the top of the file, so even if you could set it in your rspec code, it would be overwritten.
You say $globals is getting it's values from hiera. I've never used that before, but if you're using rspec-puppet gem, it looks like you can define the path to your hiera yaml file. So you could then possibly overwrite the value after that, or have separate hiera yaml files for each test.
Many thanks to #supremebeing7 for setting me on the right path. I created a second hiera yaml file which contained alternate values for which I wanted to test in my rspec code. I added the following code to the section in my rspec file where I needed to test with the alternate value:
context 'If the globals yum status = off' do
let(:hiera_config) {
'spec/fixtures/alt_hiera/hiera.yaml' }
hiera = Hiera.new({ :config =>
'spec/fixtures/alt_hiera/hiera.yaml' })
globals = hiera.lookup('globals', nil, nil)
This alternate hiera file had the value of "off" set for $globals['yum']['status'] and my test passed.
This may be a simple matter of mocking a resource, but...
class myclass (
String stringParam,
Integer intParam,
File fileParam
) {
# do some things
$path = fileParam['title']
$mode = fileParam['mode']
# do some more things
}
Now I want to write an rspec-puppet test for this class. How do I either create or mock a File resource and get it into the catalog that rspec-puppet uses, so that I can reference it?
The answers to this and this got me partway there, but everything I've tried has led to myClass complaining that it's being passed a string instead of a file reference.
...
let(:params) {{
:stringParam => 'Here is my string',
:intParam => 238,
:fileParam => *??????,*
}}
There isn't really much support in rspec-puppet for this, as a class test parameters list is generated from the :params assuming only strings (or nested hashes/arrays etc. containing strings) or a couple of permitted symbol values used almost literally, :undef and :default. It doesn't have a way of passing in resource references.
A workaround exists that lets you put literal content into a manifest though, by passing an object that responds to the inspect method. For example, in your spec_helper.rb:
class ResourceReference
def initialize(ref)
#ref = ref
end
def inspect
#ref
end
end
And then in your spec:
let(:params) {{
:stringParam => 'Here is my string',
:intParam => 238,
:fileParam => ResourceReference.new("File[/etc/foo]"),
}}
rspec-puppet will call the inspect method on the ResourceReference object which returns the string you've passed in. This should be placed in the manifest unchanged.
(This was originally used as a workaround for undef, which can now be passed as :undef.)
As an aside, you can set let(:pre_condition) { "..." } to add literal content to the test manifest before the generated class { ... }, but I don't think there's a way to use that here.
I'd strongly recommend filing a feature request against rspec-puppet.
I have written a foodcritic rule to catch any attempt to write to a blacklist of directories/files under the /etc directory.
When blacklisted paths are passed to resource declarations as strings in a recipe, the rule triggers, however when they are passed as attributes, the rule does not trigger:
#resources = [
'file',
'template',
'remote_file',
'remote_directory',
'directory'
]
#blacklist = [
'/etc/ssh/',
'/etc/init',
...
]
rule 'RULE001', 'do not manipulate /etc other than init/,init.d/ & default/' do
tags %w(security)
recipe do |ast|
violations = []
#resources.each do |resource_type|
violations << find_resources(ast, type: resource_type).select do |resource|
res_str = (resource_attribute(resource, 'path' || resource_name(resource)).to_s
#blacklist.any? { |cmd| res_str.include? cmd }
end
end
violations.flatten
end
end
Testing this using the below, the literal strings are caught, however when passed as attributes they are passed. Can anyone see what I'm missing?
attributes/default.rb:
default['testbook']['etc-test'] = '/etc/ssh/test.conf'
default['testbook']['etc-dir-test'] = 'etc/ssh/somedir/'
recipes/default.rb:
#template '/etc/ssh/test.conf' do <-- caught
template node['testbook']['etc-test'] do #<-- not caught
source 'test.conf'
owner 'nobody'
group 'nobody'
mode '0644'
action :create
end
#directory '/etc/ssh/somedir' do <-- caught
directory node['testbook']['etc-dir-test'] do <-- not caught
action :create
end
Yes, this isn't something you can fully handle via static analysis. Foodcritic and tools like it can only handle things that are static in the code, anything that could vary at runtime won't be known.
I have a Chef recipe for a multi-node web service, each node of which needs to get the hostname and IP of the other nodes, to put it into its own local configuration.
The code is shown below. The problem is that when the node.set[][] assignments are made in the ruby_block as shown, the values are empty when the template that relies upon them is created. If I want to create that template, I have to move all of the ruby_block code outside, and have it "loose" in the recipe. Which makes it harder to do unit-testing with Chefspec and the like.
Can any Chef guru set me straight? Is it just impossible to do node.set[] like this inside of a ruby_block? And if so, why doesn't it say so in the docs?
$cm = { :name => "web", :hostname => "" , :ip_addr => "" }
$ca = { :name => "data", :hostname => "" , :ip_addr => "" }
$cg = { :name => "gateway", :hostname => "" , :ip_addr => "" }
$component_list = [$cm, $ca, $cg]
ruby_block "get host addresses" do
block do
for cmpnt in $component_list
# do REST calls to external service to get cmpnt.hostname, ip_addr
# .......
node.set[cmpnt.name]['name'] = cmpnt.name
node.set[cmpnt.name]['host'] = cmpnt.hostname
node.set[cmpnt.name]['ip'] = cmpnt.ip_addr
end
end
end
template "/etc/app/configuration/config.xml" do
source "config.xml.erb"
variables( :dataHost => node['data']['host'],
:webHost => node['web']['host'],
:gatewayHost => node['gateway']['host'] )
action :create
end
I also added
subscribes :create, "ruby_block[get host addresses]", :immediately
to the template definition to ensure that the ruby_block ran before the template was created. This didn't make a difference.
I realize this is an old post, however for future reference, I just ran across this gist which gives a nice example of node variable assignments in the Compile vs. Converge phases. To adapt the gist to your example, you'll need to add code like the following to your ruby_block:
template_r = run_context.resource_collection.find(:template => "/etc/app/configuration/config.xml")
template_r.content node['data']['host']
template_r.content node['web']['host']
template_r.content node['gateway']['host']
For Chef 11, also see Lazy Attribute Evaluation.
The problem seems to be that attribute values inside your template resource definition get evaluated before actually invoking any resources.
I.e. the file is first executed as simple Ruby, compiling the resources, and only the the resource actions gets invoked. By that time, it is too late already.
I ran into the same problem when trying to encapsulate certain attribute manipulations into a resource. It simply does not work. Should anyone know a solution to this problem, I would appreciate it very much.
EDIT:
b = ruby_block...
...
end
b.run_action(:create)
Could possibly do the trick. It invokes the resource immediately.
The simplest answer to this is to not use chef attributes and not use ruby_block to do the work of talking to the REST API. The code can also be moved to a custom resource for better reuse:
unified_mode true
provides :my_resource
action :run do
cm = { :name => "web", :hostname => "" , :ip_addr => "" }
ca = { :name => "data", :hostname => "" , :ip_addr => "" }
cg = { :name => "gateway", :hostname => "" , :ip_addr => "" }
component_list = [cm, ca, cg]
hash = {}
for cmpnt in component_list
# do REST calls to external service to get cmpnt.hostname, ip_addr
# .......
hash[cmpnt.name] = {}
hash[cmpnt.name]['name'] = cmpnt.name
hash[cmpnt.name]['host'] = cmpnt.hostname
hash[cmpnt.name]['ip'] = cmpnt.ip_addr
end
template "/etc/app/configuration/config.xml" do
source "config.xml.erb"
variables( :dataHost => hash['data']['host'],
:webHost => hash['web']['host'],
:gatewayHost => hash['gateway']['host'] )
action :create
end
end
By using unified_mode and moving into a custom resource, it also makes it easier to use a node attribute without requiring the use of lazy {} or ruby_blocks. It also still allows chef configuration (like setting up resolv.conf or other network requirements before doing the REST calls) prior to calling this code while not having to think about compile/converge two pass issues in recipe context.
There is also no reason to use a resource like ruby_block to do pure ruby processing which does not change the system under management. In this case the ruby_block is hitting a REST service purely to collect data. That does not need to be placed into a Chef resource. It isn't clear from the question if that was being done because the questioner though it was a "best practice" (in this case it is not), or if it was being done to move execution to compile time in order to allow other chef resources that aren't part of the question to fire first (in which case using a custom resource is a much better solution than using a ruby_block).
It's been a while since this question, but in case someone is still looking for it, lazy evaluate is your friend:
template '/tmp/sql_file.sql' do
source "sql_file.sql.erb"
mode 0700
variables lazy {
# Create a new instance of MySQL library
mysql_lib = Acx::MySQL.new(
'127.0.0.1', 'root', node['mysql']['service']['pass']
)
password = node['mysql']['service']['support_admin']['ct_password']
# It returns the encrypted password after evaluate it, to
# be used in template variables
{ admin_password: mysql_lib.encrypted_password(password) }
}
end
https://docs.chef.io/resource_common.html#lazy-evaluation
Is there a way to group tests conditionally with rspec? By which I mean, is there a way to say "if this variable is some value, run this set of tests. If this variable is some other variable, run this other set of tests"?
Basic Example of where it would be needed (doesn't actually work, obviously, but should show you what I want). Assume the user to be tested is defined elsewhere and the current user being tested is #user. Although you may have better alternatives to that, that's fine.
before do
login_as_user(#user) #This logs them in and brings them to the homepage to be tested
page.visit("/homepage")
end
describe "Check the user homepage"
subject {page}
it {should have_content("Welcome, #{#user.name}!")}
if(#user.role=="admin")
it {should have_link("List Users"}
end
end
Keep in mind I have no control over the user being tested - I cannot create users on the fly, for example, and no given user is guaranteed to exist, and I don't know offhand what combination of roles a given user will have. So I do need some way to say "run this test only if these conditions are met", rather than a way to create situations where every test can be run.
You can use a let (or possibly let!) to define who the user being logged-in should be. (Obviously replace #regular_user & #admin_user with the appropriate factory/fixture/etc.)
before do
login_as_user(user)
page.visit "/homepage"
end
let(:user) { #regular_user }
describe "Check the user homepage" do
subject { page }
it { should have_content "Welcome, #{#user.name}!" }
context "when an administrator" do
let(:user) { #admin_user }
it { should have_link "List Users" }
end
end
Okay, apparently the issue was as simple as this: #user is an instance variable, which only exists when the tests are being executed. Dynamic generation of tests does not work with instance variables for that reason.
However, by declaring it as a local variable somewhere outside any test-style blocks (before, after, it or specify), you can have access to it for the conditional logic.
The solution was as simple as taking the # sign off the front of the user.
in the same kind of idea you can use static variables
In order to easily switch between environment, I even use the system global variables
in my spec_helper.rb, I've set the default settings
default_env = {
'SITE_ROOT' => "http://localhost:3000",
'ACCOUNT_ID' => 'user',
'ACCOUNT_PASSWORD' => 'password',
'SUBMIT_REAL_MONEY' => 'false'
}
# setting default env variables if no ENV ones exists
default_env.each do |const_name, value|
val = ENV[const_name].nil? ? value : ENV[const_name]
eval_str = "#{const_name}='#{val}'"
puts eval_str
eval eval_str
end
Then I can specify my settings in the command line calling my specs:
SITE_ROOT='https://my.production.site/' SUBMIT_REAL_MONEY=true rspec
And here is a spec behind a simple condition:
if SUBMIT_REAL_MONEY == 'true'
it { should respond_with 200 }
end