Execute instructions in chef recipes only for Test Kitchen converge action - ruby

I need a way to run parts of chef recipes only in case of converge action in Test Kitchen.
I found a way to do it for ChefSpec:
unless defined?(ChefSpec)
cookbook_file "/home/#{node['user']}/script.sh" do
source 'install.sh'
owner node['user']
mode '0755'
action :create
end
end
How I can do it for Kitchen tool?

This requirement has been discussed in detail on a feature request on Github.
There are two ways to do this. One is to define an environment variable such as TEST_KITCHEN, then use it in recipe with if condition, only_if or not_if guards.
Below should work:
Set the environment variable:
export TEST_KITCHEN="1"
Run the resource conditionally:
if ENV['TEST_KITCHEN']
cookbook_file "/home/#{node['user']}/script.sh" do
source 'install.sh'
owner node['user']
mode '0755'
action :create
end
end
Other way is to use a node attribute, something like node['test_kitchen'] set to true and use it to run actions conditionally.
cookbook_file "/home/#{node['user']}/script.sh" do
source 'install.sh'
owner node['user']
mode '0755'
action :create
only_if { node['test_kitchen'] }
end

Related

chef guard only_if with '&&' not adhering to both statements

I have the below hash in a chef recipe, that creates a directory/s
node['fnb_base_directory']['directory_name'].map do |directory_name, dir|
next if directory_name.empty?
directory directory_name do
owner dir['owner']
group dir['group']
mode dir['mode']
recursive dir['recursive']
action dir['action']
only_if "getent passwd #{dir['owner']}" && "getent group #{dir['group']}"
end
end
I ONLY want chef to try create the directory based on this guard:
only_if "getent passwd #{dir['owner']}" && "getent group #{dir['group']}"
So that basically means that BOTH the user and the group must exist before trying to create the directory.
The problem appears to be that when chef interprets this, I see it is only adhering to ONE of the statements i.e. ONLY checks that group exists and then proceeds to attempt to create the directory, but will fail because the user does not exist yet.
See below interpretation:
directory("/opt/test_dir_creation") do
action [:create]
default_guard_interpreter :default
declared_type :directory
cookbook_name "fnb_base_wrapper"
recipe_name "fnb_base_directory"
recursive false
owner "nonexistent_user"
group "opc"
mode "0755"
only_if "getent group opc"
end
Failure:
directory[/opt/test_dir_creation] action create
* cannot determine user id for 'nonexistent_user', does the user exist on this system?
================================================================================
Error executing action `create` on resource 'directory[/opt/test_dir_creation]'
================================================================================
Chef::Exceptions::UserIDNotFound
--------------------------------
cannot determine user id for 'nonexistent_user', does the user exist on this system?
The reason the user does not exist yet, is because that user is created in another cookbook whose priority is not as high as our base cook (which creates directories), hence why the directory creation is done before the user is created.
The directory will then be created on the 2nd converge, where the user will then exist, and proceed to create the directory ONLY then.
FYI.
I am using getent because we use AD on our servers, so it may not always be a static user/group, but one that resides in AD.
I have also checked this question:
Using multiple conditions in Chef only_if guard
It does not help me.
Your help, guidance and advice will be greatly appreciated.
Try removing the quotes in the middle so the whole expression including the && condition runs in the shell.
only_if "getent passwd #{dir['owner']} && getent group #{dir['group']}"
Chef accepts a string shell command or a ruby block
only_if "some shell commands which are possibly in a pipeline return 0"
only_if {
return true if <condition1> && <condition2>
return true if <condition3> || <condition4>
return false
}

Chef use cookbook_file in ruby block

I have the following code to figure out where Java is located on the box. Java comes with our application and what Java version that is included with the application differs.
def app_java_home
if Dir.exist?("#{app_home}/jre-server/linux")
Dir.chdir("#{app_home}/jre-server/linux") do
Dir.glob('jdk*').select { |f| File.directory? f }[0]
end
end
end
Then, in my cookbook I have
aws_s3_file "#{app_download_path}/#{app_s3['archive_file']}" do
bucket app_s3['bucket']
remote_path app_s3['remote_path']
region aws_region
not_if { ::Dir.exists?(app_bin_dir) }
not_if { ::File.exists?("#{app_download_path}/#{app_s3['archive_file']}") }
end
execute 'extract' do
user 'root'
command "unzip #{app_download_path}/#{app_s3['archive_file']} > /dev/null"
not_if { ::Dir.exists?("#{app_home}/ourapp") }
only_if { ::File.exists?("#{app_download_path}/#{app_s3['archive_file']}") }
end
execute 'move' do
user 'root'
command "mv #{app_download_path}/ourapp/ #{app_install_path}"
not_if { ::Dir.exists?(app_home) }
end
cookbook_file "#{app_java_home}/jre/lib/security/local_policy.jar" do
source %W[#{app_release}/local_policy.jar default/local_policy.jar]
owner app_user_name
group app_group_name
mode 0755
end
cookbook_file "#{app_java_home}/jre/lib/security/US_export_policy.jar" do
source %W[#{app_release}/US_export_policy.jar default/US_export_policy.jar]
owner app_user_name
group app_group_name
mode 0755
end
However, the two cookbook_file resources fails because it can't find the directory:
No such file or directory # dir_chdir - /ourapp/jre-server/linux/
After a lot of googling, I've come to the conclusion that it's a .. "missmatch" (?) between compile time and run time of the recipes. Basically, if I understand it correctly, it tries to run the cookbook_file resource(s) first but fails. So never downloads, unpacks and installs the app artefact.
I've tried running app_java_home when the directory exists, and it does seem to work the way I want it..
I tried putting the cookbook_file resources in a ruby_block, but then I instead get:
undefined method `cookbook_file' for Chef::Resource::RubyBlock
The app_java_home .. function (?) used to look like this:
def app_java_home
"#{app_home}/jre-server/linux/#{jdk_version}"
end
Where jdk_version came from the databag. This worked fine, but we have a long standing bug/feature request in our system where it sometimes happens that "they" get the version they put in the databag wrong, causing all sorts of problems.. So they want a way to remove this dependency and instead "figure this out" dynamically.
Ruby and Chef isn't my forte, so I'm not sure what to try next. I have found references to Chef::Resources::CookbookFile (which, if I understand it, could/should be used inside ruby_blocks), but can't find any examples or documentation about it. The link on RubyDocs is broken.
Adding an answer here for a better explanation.
Any (Ruby) code that is not within any of the Chef resources, will run in Compile phase
All resource declarations will run in Convergence phase in the order they are defined
Thankfully, there is a way to make resources run in Compile phase if so required. Though IMHO it should be done sparingly and in exceptional cases.
As per your comment aws_s3_file and execute resources are the ones that unpack the app (and create the directory). In this case, it seems you want them to run in compile phase.
Prior to Chef client 16.0
Use the run_action option with the action that should be performed at the compile time. For example execute resource takes action :run:
# Note action ":nothing" and "run_action"
execute 'extract' do
user 'root'
command "unzip #{app_download_path}/#{app_s3['archive_file']} > /dev/null"
not_if { ::Dir.exists?("#{app_home}/ourapp") }
only_if { ::File.exists?("#{app_download_path}/#{app_s3['archive_file']}") }
action :nothing
end.run_action(:run)
Chef client 16.0 onwards
We can add a common property to the resources. Example with execute resource:
# Note the extra property "compile_time"
execute 'extract' do
user 'root'
command "unzip #{app_download_path}/#{app_s3['archive_file']} > /dev/null"
not_if { ::Dir.exists?("#{app_home}/ourapp") }
only_if { ::File.exists?("#{app_download_path}/#{app_s3['archive_file']}") }
compile_time true
end
And finally to answer the subject of the question:
Chef use cookbook_file in ruby block
This is not possible. Refer to the first point on the top. If we want Ruby code to run during converge (instead of compile), we put it within the ruby_block resource. So it can contain code like (for example):
ruby_block 'get directory' do
block do
def app_java_home
"#{app_home}/jre-server/linux/#{jdk_version}"
end
end
end
With the help of #seshadri_c, I finally managed to solve the problem! It took some doing, because I kept misunderstanding the suggestions etc.
So this is what I came up with (for posterity):
def jdk_version(required = true)
base_dir = "#{app_home}/jre-server/linux"
if Dir.exist?("#{base_dir}")
Dir.chdir("#{app_home}/jre-server/linux") do
Dir.glob("jdk*").each do |f|
if File.directory?(f)
return "#{f}"
end
end
end
end
end
def app_java_home
return "#{app_home}/jre-server/linux/#{jdk_version}"
end
Turns out I need to get just the version, individually, as well, so I rearranged the functions a bit. I'm sure it could be written much cleaner, but here the trick was to use return instead of puts/print! Well, I'm a programmer, but not a Ruby programmer so didn't know that was an option..
Then, in the cookbook, I added the .run_action() where needed. I didn't need them for the cookbook_file, which simplified things a bit:
aws_s3_file "#{app_download_path}/#{app_s3['archive_file']}" do
bucket app_s3['bucket']
remote_path app_s3['remote_path']
region aws_region
not_if { ::Dir.exists?(app_bin_dir) }
not_if { ::File.exists?("#{app_download_path}/#{app_s3['archive_file']}") }
end.run_action(:create)
execute 'extract' do
user 'root'
command "unzip #{app_download_path}/#{app_s3['archive_file']} > /dev/null"
not_if { ::Dir.exists?("#{app_home}/app") }
only_if { ::File.exists?("#{app_download_path}/#{app_s3['archive_file']}") }
end.run_action(:run)
execute 'move' do
user 'root'
command "mv #{app_download_path}/app/ #{app_install_path}"
not_if { ::Dir.exists?(app_home) }
end.run_action(:run)
# JCE Unlimited Strength Jurisdiction Policy Files
cookbook_file "#{app_java_home}/jre/lib/security/local_policy.jar" do
source %W[#{app_release}/local_policy.jar default/local_policy.jar]
owner app_user_name
group app_group_name
mode 0755
end
cookbook_file "#{app_java_home}/jre/lib/security/US_export_policy.jar" do
source %W[#{app_release}/US_export_policy.jar default/US_export_policy.jar]
owner app_user_name
group app_group_name
mode 0755
end
With all that, everything is running exactly when they're supposed to and everything seems to be working.

How to do an "unless" conditional when changing permissions in a ruby_block in chef?

In chef I Have a ruby_block where I am changing permissions and ownership of a directory. How can I do a check where the permissions are only changed if they have not already been changed by the " FileUtils.chown" statement? I need to do this within the ruby_block if possible because i am ganna have other code in the ruby block. What would my "unless" statement be? Here is my code:
ruby_block 'exe' do
block do
FileUtils.chmod 0755, '/make/news'
FileUtils.chown('root', 'root', '/make/news')
end
end
The correct way to do this is to use Chef's file resource:
file '/make/news' do
mode 0755
owner 'root'
group 'root'
end
You're going down the road of trying to re-write the file resource which is not a good idea.
Using the Chef Resource's not_if Guard
Chef resources share a number of common functions. The ruby_block resource supports the not_if property as a conditional guard. The general format is:
ruby_block 'custom chmod' do
block do
#
end
not_if { true }
end
So, you could program your logic this way, but it will eventually bite you badly. Chef often works better if you use a file or directory resource declaratively using a separate block to manage permissions, and then (if necessary) chain it with a notification from some other block that needs a given permission set. For example:
directory '/make/news' do
mode '0755'
owner 'root'
group 'root'
action :nothing
end
ruby_block 'do something with news' do
block do
#
end
only_if { true }
notifies :create, 'directory[/make/news]', :before
end
That said, the goal of configuration management is to continuously converge, so I'd strongly question whether creating this interdependency between resource blocks is truly necessary in the first place. If possible, just converge your directory permissions every time to enforce them. While this may create a sequencing dependency within your recipe, a more declarative approach often simplifies cookbook and recipe debugging in the long run. Your individual mileage may vary.

How to write ChefSpec Unit Test for ruby_block resource?

How to write the ChefSpec Unit tests for ruby_block? What if the local variables are declared in the recipe? How will it be handled?
Here is the code of a recipe:
package 'autofs' do
action :install
end
src = '/etc/ssh/sshd_config'
unless ::File.readlines(src).grep(/^PasswordAuthentication yes/).any?
Chef::Log.warn "Need to add/change PasswordAuthentication to yes in sshd config."
ruby_block 'change_sshd_config' do
block do
srcfile = Chef::Util::FileEdit.new(src)
srcfile.search_file_replace(/^PasswordAuthentication no/, "PasswordAuthentication yes")
srcfile.insert_line_if_no_match(/^PasswordAuthentication/, "PasswordAuthentication yes")
srcfile.write_file
end
end
end
unless ::File.readlines(src).grep('/^Banner /etc/issue.ssh/').any?
Chef::Log.warn "Need to change Banner setting in sshd config."
ruby_block 'change_sshd_banner_config' do
block do
srcfile = Chef::Util::FileEdit.new(src)
srcfile.search_file_replace(/^#Banner none/, "Banner /etc/issue.ssh")
srcfile.insert_line_if_no_match(/^Banner/, "Banner /etc/issue.ssh")
srcfile.write_file
end
end
end
As I am new to ChefSpec, I am able to write the code for the basic resources. I have written the Unit Test as below:
require 'chefspec'
describe 'package::install' do
let(:chef_run) { ChefSpec::SoloRunner.new(platform: 'ubuntu', version: '16.04').converge(described_recipe) }
it 'install a package autofs' do
expect(chef_run).to install_package('autofs')
end
it 'creates a ruby_block with an change_sshd_config' do
expect(chef_run).to run_ruby_block('change_sshd_config')
end
it 'creates a ruby_block with an change_sshd_banner_config' do
expect(chef_run).to run_ruby_block('change_sshd_banner_config')
end
end
Does the above implementation is corrct? I am not able to figure out how it can be written for complex resources like ruby block, etc. And how the local variables declared in recipe should be taken care.
Thanks in advance..
let(:conf) { double('conf') }
before do
allow(File).to receive(:readlines).with('/etc/ssh/sshd_config').and_return(["something"])
allow(File).to receive(:readlines).with('/etc/ssh/sshd_config').and_return(["something"])
end
it 'creates a ruby_block with an change_sshd_config' do
expect(chef_run).to run_ruby_block('change_sshd_config')
expect(Chef::Util::FileEdit).to receive(:new).with('/etc/ssh/sshd_config').and_return(conf)
confile = Chef::Util::FileEdit.new('/etc/ssh/sshd_config')
end
Do the same thing for other ruby block as well! Mostly it will work.
Or
You can test using inspec to test the state of your system. It seems like you may want to test the state of your system for the way sshd is configured after Chef has applied config. You could accomplish that with inspec with something like the following snippet:
describe file('/etc/ssh/sshd_config') do
its('content') { should match /whatever/ }
end
I hope this helps!
Unit tests should be testing that specific inputs produce the expected outputs. By expecting Chef to have run_ruby_block, you're essentially saying "Does Chef work the way I expect it to" -- not, "Does my ruby_block resource work the way I expect it to".
You should use ChefSpec to validate that the side effects of the ruby_block are what you expect them to be. That is, that the file(s) are modified. The ChefSpec documentation on RenderFileMatchers is probably what you're looking for.

How to define a function/action/... in chef that returns a value which can be used in e.g. not_if

I'm learning chef at the moment and I'm trying to write everything in a way that repeated provisioning doesn't break anything.
I have a server that is deployed on the machine and then there is some code loaded into it. The next time of provisioning I like to test first if the code has been loaded already. And I want to do it in a generic way because I use it in different recipes.
My idea would be to define a function/defintion/etc.. I can call the function which tests the condition and returns a value. My hopes would be that I can use this function/... in a not_if clause for other actions.
Is there a way to do this in chef with a defintion/action/provider/... or would I need to add some rubyish stuff somewhere?
Resources in Chef all have conditional execution.
The not_if and only_if statements can take a shell command as a string or a ruby block to determine if they should perform their action or not.
user "myuser" do
not_if "grep myuser /etc/password"
action :create
end
You might have a node attribute and use that as your conditional or call a ruby method that returns true or false.
template "/tmp/somefile" do
mode "0644"
source "somefile.erb"
not_if { node[:some_value] }
end
https://web.archive.org/web/20111120120013/http://wiki.opscode.com/display/chef/Resources#Resources-ConditionalExecution

Resources