Reuse template across different recipes - ruby

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.

Related

How to set dynamic variable once it is identified to all recipes in a cookbook

I need a help to set a global variable in chef recipes.
I have below series of recipes:
Discovers the tomcat from path variable/attibutes/default.rb:
default['tomcat_cookbook']['tomcathome']="['/home/tomcat','/home/ApacheTomcat']"
This recipe will identify the tomcat installation as either one of the directory will be available on server out of this two directories.
Lets say, if it sets the tomcathome to directory "/home/tomcat", I have some more subsequent recipes like start/stop/restart tomcat.
Currently for every recipe I am running discovery logic inside stop/start recipes while knowing that on a particular server, tomcathome is set to "/home/tomcat" .
Is there any way I can remove duplicate code for tomcat home discovery and make use of the identified tomcathome variable for remaining recipes.
Please suggest.
I think this would be a good use of libraries. I'll assume the cookbook name is tomcat_cookbook. In the libraries folder in a cookbook, create a file called path.rb.
Add the following code into the path.rb file. I prefer to namespace my libraries to organize my methods using CookbookName::ModuleName format.
libraries/path.rb:
module TomcatCookbook
module Path
def install_path
node['tomcat_cookbook']['tomcathome'].each do |path|
return path if ::Dir.exist?(path)
end
end
end
end
Within any recipe, you can include this module and use the methods in it:
# Use this include for use in the recipe
Chef::Recipe.send(:include, TomcatCookbook::Path)
# Use this include for using methods in the directory resource itself
Chef::Resource::Directory.send(:include, TomcatCookbook::Path)
Chef::Log.info("Install Path: #{install_path}")
directory "tomcat_install_path" do
path install_path
action :create
end
In certain situations, I have needed to create a common cookbook which includes only libraries which I can use across multiple cookbooks.

create directory with timestamp in chef

I want to create a directory from chef recipe to backup my existing artifacts. i want to create the backup directory with following format.
appname_bkp_17-10-11-125845
for example I need to create this directory and add the directory name into a variable which is something like;
bkp_dir_name = appname_bkp_17-10-11-125845
Please advice.
While Chef is a DSL, it is still first and foremost pure Ruby. You should try to learn a little about Ruby basics before committing to Chef entirely, because a lot of what you might want to do will be more efficient if you know the language.
time = Time.now.strftime("%F-%T").gsub(':','')
dir = "appname_bkp_#{time}"
path = ::File.join(node['default']['default_backup_path'], dir)
# Chef resource to create a directory with default properties
directory path

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

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

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.

Resources