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

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
}

Related

Execute instructions in chef recipes only for Test Kitchen converge action

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

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.

Chef user group permissions on subfolders

I am working trying to give user group permissions on subfolders in a Linux environment. In Linux that would be:
chown -R user:group /var/lib/temp/*
How can I acheive the same in Chef with Ruby? I have tried this:
directory '/opt/jenkins/plugins' do
owner 'jenkins'
group 'jenkins'
mode '0755'
recursive true
action :create
end
By specifying recursive it does not help.
From the chef docs:
recursive
Ruby Types: TrueClass, FalseClass
Create or delete parent directories recursively. For the owner, group, and mode properties, the value of this attribute applies only to the leaf directory. Default value: false.
User and group permissions won't recurse, you'll have to set them on each sub-directory manually.
You could make this slightly easier by doing something like the following:
plugins = %w(plugin1 plugin2)
plugins.each do |plugin|
directory "/opt/jenkins/plugins/#{plugin}" do
owner 'jenkins'
group 'jenkins'
mode '0755'
action :create
end
end

How to coverage with chefspecs resources programatically added to run_context

I have following code in my recipe, which runs bash script that returns users (except root). 1 user per line.
ruby_block "Delete users" do
action :run
block do
users = Mixlib::ShellOut.new("whatever command with lines containing users").run_command.stdout
users.each_line do |user|
ex = Chef::Resource::Execute.new("Removing User: #{user}", run_context)
ex.command "remove user"
ex.run_action(:run)
end
end
end
Now I managed to mock shell out Here, but I can't figure out why and if it is possible to expect such resources. For instance if command returns
user1
user2
Then following will work
allow(shellout).to receive("whatever command with lines containing users").and_return("user1")
expect(chef_run).to run_ruby_block('Delete users')
But if I add following
expect(chef_run).to run_execute('Removing User: user1')
It will fail, while there isn't any resources in context that I haven't cover. (I am using Chef::Coverage so I know).
Thanks

Chef conditional resource argument

I'm creating a user via Chef. His properties are stored in data bag:
{
"id": "developer",
"home": "/home/developer",
"shell": "/bin/zsh",
"password": "s3cr3t"
}
The recipe is:
developer = data_bag_item('users', 'developer')
user developer['id'] do
action :create
supports :manage_home => true
home developer['home']
comment developer['comment']
shell developer['shell']
password developer['password']
end
The problem is that if zsh is not installed on node, I cannot login as developer. So, I want to conditionally apply argument for user resource, like:
user developer['id'] do
action :create
supports :manage_home => true
home developer['home']
comment developer['comment']
if installed?(developer['shell'])
shell developer['shell']
end
password developer['password']
end
How can I achieve this?
To complement #mudasobwa's answer the proper way to do it in chef and avoid missing the shell if it's installed by another recipe or a package resource in the same recipe you have to use lazy attribute evaluation.
Long version for thoose interested on the how and why:
This is a side effect on how chef works, there's a first time compiling the resources to build a collection, at this phase any ruby code in a recipe (outside of a ruby_block resource) if evaluated. Once that is done the resources collection is converged (the desired state is compared to the actual state and relevant actions are done).
The following recipe would do:
package "zsh" do
action :install
end
user "myuser" do
action :create
shell lazy { File.exists? "/bin/zsh" ? "/bin/zsh" : "/bin/bash" }
end
What hapens here is that the evaluation of the shell attribute value is delayed to the converge phase, we have to use a if-then-else construction (here with a ternary operator as I find it more readable) to fallback to a shell we're sure will be present (I used /bin/bash, but a failsafe value would be /bin/sh) or the shell attribute will be nil, which is not allowed.
With this delayed evaluation the test on the presence of "/bin/zsh" is done after the package has been installed and the file should be present. In case there was a problem within the package, the user resource will still create the user but with "/bin/bash"
The easiest way to achieve what you want is to check for the shell existence explicitly:
shell developer['shell'] if File.exist? developer['shell']

Resources