Mocking out file contents in ChefSpec - chefspec

I have a chef cookbook which copies the ubutunu user's (default user of the amazon AMI am using) authorized keys file to a newly created user.
ubuntu_public_key_file = "/home/ubuntu/.ssh/authorized_keys"
file "#{new_user_homedir}/.ssh/authorized_keys" do
owner new_user
group new_user_group
mode "0600"
content IO.read(ubuntu_public_key_file)
end
Am trying out chefspec and I want to test this out. I want to mock the existence of ubuntu_public_key_file and its contents. Any feedback on this is appreciated!

You use RSpec stubs:
describe 'cookbook::recipe' do
let(:content) do
"CUSTOM SSH KEY CONTENT HERE"
end
before do
IO.stub(:read).with('/home/ubuntu/.ssh/authorized_keys').and_return(content)
end
end

Related

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.

Keeping files updated with a Chef recipe

The challenge prompt is above, and my latest attempt is below. The directories and files are created as expected, and the read-out after executing chef-apply multipleCopies.rb tells me the files are linked, but when I update any one of the files, the others do not follow suit. Any ideas? Here is my code:
for x in 1..3
directory "multipleCopy#{x}" do
mode '0755'
action :create
end
end
file "multipleCopy1/secret.txt" do
mode '0755'
action :create
end
for x in 2..3
link "multipleCopy#{x}/secret.txt" do
to "multipleCopy1/secret.txt"
link_type :hard
subscribes :reload, "multipleCopy1/secret.txt", :immediately
end
end
Note: For less headache, I am testing the recipe locally before uploading to the ubuntu server referenced in the prompt, which is why my file paths are different and why I have not yet included the ownership properties.
So a file hard link doesn't seem to be what the question is going for (though I would say your solution is maybe better since this is really not what Chef is for, more on that later). Instead they seem to want you to have three actually different files, but sync the contents.
So first the easy parts, creating the directories and the empty initial files. It's rare to see those for loops used in Ruby code, though it is syntactically valid:
3.times do |n|
directory "/var/save/multipleCopy#{n+1}" do
owner "ubuntu"
group "root"
mode "755"
end
file "/var/save/multipleCopy#{n+1}/secret.txt" do
owner "root
group "root"
mode "755"
end
end
But that doesn't implement the hard part of sync'ing the files. For that we need to first analyze the mtimes on the files and use the most recent as the file content to set.
latest_file = 3.times.sort_by { |n| ::File.mtime("/var/save/multipleCopy#{n+1}/secret.txt") rescue 0 }
latest_content = ::File.read("/var/save/multipleCopy#{latest_file+1}/secret.txt") rescue nil
and then in the file resource:
file "/var/save/multipleCopy#{n+1}/secret.txt" do
owner "root
group "root"
mode "755"
content latest_content
end
As for this not being a good use of Chef: Chef is about writing code which asserts the desired state of the machine. In the case of files like this, rather than doing this kind of funky stuff to check if a file has been edited, you would just say that Chef owns the file content for all three and if you want to update it, you do it via your cookbook (and then usually use a template or cookbook_file resource).

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 cookbook - copy file to local user with '~' ruby variable

Well, let's start by saying I'm a chef noob and I am trying to hash this code out.
I am in a full mac shop. I am using Chef to automate system wide changes. As I'm new, I'm rolling it out onto our Mac AV systems.
Basically, there is a folder on a file server that has MAC SCREEN SAVERS directory. I copy the server directory locally to the MAC OS X /User/user_name/Pictures directory.
So, this is what I got in chef:
local_folder_modified = File.mtime("~/Pictures/SCREEN SAVER NEW MACS")
server_folder_modified = File.mtime("/Volumes/SERVER/SCREEN\ SAVER\ NEW\ MACS/")
if server_folder_modified != local_folder_modified
# file has changed
then
require 'fileutils'
FileUtils.cd('server_folder_modified') do
FileUtils.rm('local_folder_modified/*')
FileUtils.cp_r './*', 'local_folder_modified'
Else
end
end
Anyways, I can't figure how to set the '~' to be the running user of this recipe. So, if Comp_A has user Jim_Beam and Comp_B has user Jack_Daniels, I don't want to set the code to be:
ENV[HOME] = /user/jimbeam
As it won't work on Jack_Daniels. Right?
I've read that file.expand will work, or ENV, but I am really unsure what will be the best code to say
"hey, I want the current user that will need this screen saver - so set the environment as a variable so it works across different nodes".
Anyways, thanks for your help. I hope I am making sense!
Yes, use File.expand. It will expand the tilde ~ to be the the home directory of the user running this cookbook. Alternatively, you could do:
"#{ENV['HOME']}/Pictures/SCREEN SAVER NEW MACS"
Like the previous comment, this is not chef DSL or ruby code. What is the source of this code or is it just pseudo-code to ask the question?
Also, chef-client is not frequently run as multiple users in a chef server deployment. It's usually run in a sudo context. So maybe you are referring to a --local-mode or chef-zero application?
You may want to use file stat of /dev/console to get the current user. Depending how you are running the chef-client Env[‘Home’] might not give you want you want. Try this:
console_user = Etc.getpwuid(::File.stat("/dev/console").uid).name
home_dir = ::File.join(‘Users’, console_user)
You can see that the chef launchd provider uses this method to determine the console user
Also there is a much simpler way to do what you are trying to accomplish with the remote_file resource. Try this:
console_user = Etc.getpwuid(::File.stat("/dev/console").uid).name
home_dir = ::File.join(‘Users’, console_user)
pics = ::File.join("#{home_dir}/Pictures/")
server_base_url = "https://PLACE_WHERE_STORE/Wallpapers")
[
‘Pic1’,
‘Pic2’,
].each do |pic|
remote_file ::File.join(pics, pic) do
source “#{server_base_url}/#{pic}”
owner console_user
group console_user
mode '0755'
action :create
end
end
For added security you should also include checksum

Removing boilerplate attributes from resources

In chef, each resource is defined like this:
directory "/home/akihiro/folder" do
owner "akihiro"
group "akihiro"
mode 0755
end
If this is the only task under akihiro's home directory, that's fine.
Unfortunately, I have to create directories, copy files, and apply templates, all under the same home directory as the owner. Therefore owner "akihiro"; group "akihiro" must be set on every resource, which is very redundant.
If the resource could be written like this,
directory "/home/akihiro/folder" do
as_akihiro
mode 0755
end
where as_akihiro is defined somewhere outside the resource, the recipe would get much clearer.
Is it possible to remove the boilerplate attributes by defining a new method?
You have a few options here.
Rubyish
You can create a Ruby module that defines this method:
module Impersonator
def as(person, perms = '0755')
send(:owner, person)
send(:group, person)
send(:mode, perms)
end
end
And then include this module in the resource:
Chef::Resource.send(:include, Impersonator)
And then use it:
directory '/foo/bar' do
as 'akihiro' # or as 'akihiro', '0644'
end
Chefish
If I understand your use case correct, the preferred way to handle this is with an LWRP (or HWRP). You indicated this process occurs multiple times and wraps core Chef resources. This is a great use case for an LWRP. Essentially you wrap and parameterize all of these resources into a single "wrapper".
# providers/default.rb
action :run do
user new_resource.username do
# ...
end
directory "/home/#{new_resource.username}" do
owner new_resource.username
group new_resource.group
mode new_resource.mode
end
# Other resources, using the `new_resource` object
end
And then in a Chef recipe, you would use this resource (assuming it is named "company_user"):
company_user 'akihiro'
That is possible. What works for me is to open the class that implements the "directory" resource, Chef::Resource::Directory and add a method as_akihiro. To do so, add a library to *your_cookbook*/libraries/as_user_helper.rb
class Chef::Resource::Directory
def as_akihiro()
owner "akihiro"
group "akihiro"
end
and you're done.

Resources