Chef - Read a file from git repo at runtime and use parse value in recipe - ruby

I would like to read a file from a checkout git repository to parse a config file and use this data to perform few resources commands.
git "/var/repository" do
action :sync
end
config = JSON.parse(File.read("/var/repository/config.json" ))
config.each do |job, flags|
#do some resources stuff here
end
This will not work because the file doesn't exist at compile time:
================================================================================
Recipe Compile Error in /var/chef/cache/cookbooks/...
================================================================================
Errno::ENOENT
No such file or directory - /var/repository/config.json
I where trying to load the file in ruby_block and perform the Chef resource actions there, but this didn't worked. Also setting the parsed config to a variable and use it outside of the ruby_block didn't work.
ruby_block "load config" do
block do
config = JSON.parse(File.read("/var/repository/config.json"))
#node["config"] = config doesn't work - node["config"] will not be set
config.each do |job, flags|
#do some stuff - will not work because Chef context is missing
end
end
end
Any idea how I could read the file at runtime and used the parsed values in my recipe?

You may also find it helpful to use lazy evaluation in scenarios like this.
In some cases, the value for an attribute cannot be known until the execution phase of a chef-client run. In this situation, using lazy evaluation of attribute values can be helpful. Instead of an attribute being assigned a value, it may instead be assigned a code block.

Related

Reuse template across different recipes

I have a Chef cookbook with many recipes that have the same code, beside other particular things.
template 'stack_file' do
local true
source File.join(base_dir, 'stack_templates/admin.yml.erb')
path File.join(base_dir, 'stacks/admin.yml')
variables(context)
end
template 'settings_file' do
sensitive true
local true
source File.join(base_dir, 'config_templates/settings_admin.yml.erb')
path File.join(base_dir, 'configs/settings_admin.yml')
variables(context)
end
Is it possible to somehow put this code in a method that I would call with my source_file, destination_file and variables?
I guess you can write a module and include it in your recipes as you do in plain Ruby.
module StackFile
...the code you want to share...
end
Then you can use:
inlclude StackFile
or
Chef::Recipe.send(:include, StackFile)
or when using *_if conditions
Chef::Resource.send(:include, StackFile)
Do this:
create new cookbook.
don't create recipes in it, but instead create a resource
you can define any parameters (inputs like source file, dest file etc like you mentioned)
add your templates to the new cookbook.
in your other cookbooks, create dependency to the previously created cookbook. This will enable you to call the resource you created there (remember that when you call resource from another cookbook and it is creating templates, it will try to take the template file from the current cookbook and not the one where the resource is defined. That is why you need to specify cookbook name when creating template (in the shared cookbook's resource) - see cookbook attribute of https://docs.chef.io/resource_template.html)
Repeat for any number of cookbooks.

Chef - run install block based on variable condition

Background: our systems are setup in a way that I will only be able to see the local chef log and will have no access to the Chef server console or any other sysadmin privileges. Hence I have a need to log locally if I want to see if or why something failed.
I can hear you asking " If you don't trust the pkg or Chef to install it correctly, then..." My answer is that while you are correct, I still want to be covered by the occasional anomaly.
My goal is to install a pkg, check to see that it installed correctly than go on to the next pkg.
On to the question:
I would like to set a variable that checks for the existence of a directory that was created by the first package using the following code:
mycond = ::File.directory?('/opt/MyPkg/conf')
Chef::Log.fatal("MyPkg package not installed ? conf dir is missing") unless mycond
the next stage in the recipee is to run the next install block checking to see if the variable has been set.
yum_package 'OtherPkg' do
action :install
only_if { mycond }
end
My question is since the only_if is failing, I was wondering if there was something wrong with the way I am setting the mycond variable ? perhapes {} braces are needed somewhere in the code ?
Total Chef newbie so please be specific with your answer.
Thanks !
Full code below:
yum_package 'MyPkg' do
flush_cache [ :before ]
action :install
end
mycond = ::File.directory?('/opt/MyPkg/conf')
Chef::Log.fatal("MyPkg package not installed ? conf dir is missing") unless mycond
yum_package 'OtherPkg' do
action :install
only_if { mycond }
end
The problem is Chef's two-pass model. See https://coderanger.net/two-pass/ for the full explanation for for this you just need to move the condition check in to the only_if block itself since that is delayed until converge time: only_if { ::File.directory?('/opt/MyPkg/conf') }.
Using the fatal log level is also probably not a good idea as this isn't actually a fatal error as written.
Chef has an order of precidance that controls the flow of execution.
Code inside resource blocks (e.g. 'yum_package') will execute AFTER any loose code in your recipe.
The following lines are being executed FIRST, before your 'yum_package' blocks:
mycond = ::File.directory?('/opt/MyPkg/conf')
Chef::Log.fatal("MyPkg package not installed ? conf dir is missing") unless mycond
I believe you can nest resource blocks. You cold be able to combind all this code in a 'ruby_block' and it should execute in order as you'd expect.

How does Chef include files generated on runtime as a template source

Using Chef recipe, I am first generating a .erb file dynamically based on inputs from a CSV file and then I want to use that .erb file as a template source. But unfortunately the changes made (in .erb file) are not considered while the recipe is converging the resources. I also tried to use lazy evaluation but not able to figure out how to use it for the template source.
Quoting the template documentation:
source Ruby Types: String, Array
The location of a template file. By default, the chef-client looks for
a template file in the /templates directory of a cookbook. When the
local property is set to true, use to specify the path to a template
on the local node. This property may also be used to distribute
specific files to specific platforms. See “File Specificity” below for
more information. Default value: the name of the resource block. See
“Syntax” section above for more information.)
And
local
Ruby Types: TrueClass, FalseClass
Load a template from a local path. By default, the chef-client loads
templates from a cookbook’s /templates directory. When this property
is set to true, use the source property to specify the path to a
template on the local node. Default value: false.
so what you can do is:
# generate the local .erb file let's say source.erb
template "/path/to/file" do
source "/path/to/source.erb"
local true
end
Your question sounds like and XY problem, reading a csv file to make a template sounds counter-productive and could probably be done with attributes and taking advantage of the variable attribute of template resource.
Assuming you know how to capture the values from the CSV file as a local variable in the recipe.
Examples:
csv_hostname
csv_fqdn
Here is what you do to create a template with lazy loading attributes. The following example creates a config file.
example.erb file
# Dynamically generated by awesome Chef so don't alter by hand.
HOSTNAME=<% #host_name %>
FQDN=<% #fqdn %>
recipe.rb file
template 'path\to\example.config' do
source 'example.erb'
variables(
lazy {
:host_name => csv_hostname,
:fqdn => csv_fqdn
})
end
If you need it to run at compile time, add the action to the block.
template 'xxx' do
# blah blah
end.run_action(:create)

Using File::read in a provider's default.rb in Chef

I am trying to create an LWRP that will call the resource that is defined within itself. My cookbook's structure is as follows:
In the machine cookbook's provider, I have a code snippet as follows:
require 'chef/provisioning' # driver for creating machines
require '::File'
def get_environment_json
##environment_template = JSON.parse(File::read(new_resource.template_path + "environment.json"))
return ##environment_template
end
The code is only trying to read a json file and I am using File::read for it.
I keep getting an error as follows:
LoadError
cannot load such file -- ::File
Does anyone know how I can use File::read inside my LWRP's provider?
OK, so the prior two answers are both half right. You have two problems.
First, you can't require ::File as it's already part of Ruby. This is the cause of your error.
Second, if you call File.read you will grab Chef's File not ruby's. You need to do a ::File.read to use Ruby's File class.
require '::File'
Is incorrect and is causing the LoadError. Delete this line. You don't need it. File is part of the Ruby core and doesn't need to be required.
To further explain, the string argument to require represents the file name of the library you want to load. So, it should look like require "file", or require "rack/utils".
It happens becuase Chef already has a file resource. We have to use the Ruby File class in a recipe.We use ::File to use the Ruby File class to fix this issue. For example:
execute 'apt-get-update' do
command 'apt-get update'
ignore_failure true
only_if { apt_installed? }
not_if { ::File.exist?('/var/lib/apt/periodic/update-success-stamp') }
end
Source: https://docs.chef.io/ruby.html#ruby-class

Test file initialization based off template using ChefSpec

I've got the following template file creation in my cookbook:
template "my_file" do
path "my_path"
source "my_file.erb"
owner "root"
group "root"
mode "0644"
variables(#template_variables)
notifies :restart, resources(service: "my_service")
end
and the following assertions in my ChefSpec tests:
chef_run.should create_file "my_file"
chef_run.file("my_file").should be_owned_by('root', 'root')
Which results in the following failure:
No file resource named 'my_file' with action :create found.
This is due to the fact that I am not using afile resource but a template resource. Question: How can I test for file creation off a template resource using ChefSpec?
There are two ways to solve your problem.
First, you can use the create_template matcher. This will match only "template" resources in the run context:
expect(chef_run).to create_template('my_file')
This matcher is also chainable, so you can assert attributes:
expect(chef_run).to create_template('my_file')
.with_path('my_path')
.with_owner('root')
However, this matcher won't actually render the template. So you can't check if you've setup file-specificity correctly.
There's also a top-level matcher for any kind of "file" (file, cookbook_file, and template) that actually renders the contents in memory:
expect(chef_run).to render_file('my_file').with_content(/^match me$/)
You can find more information about render_file in the README.
According to the docs (https://github.com/acrmp/chefspec) you should be able to use:
expect(chef_run).to create_file 'my_file'
I think something changed very recently (possibly the version of chefspec on rubygems), however, because tests I had passing earlier today (using the same syntax you are using) are now suddenly failing.

Resources