Chef Custom Resources : mutable property or an alternative - ruby

I read that as of 2016 OpsCode recommends against LWRP and the alternative HWRP. They rather recommend to use custom resources.
While this makes sense it leaves a lot to be desired in terms of leveraging ruby (unless I've misunderstood?)
I would like to be able to modify an array based on some boolean properties. I can then use that array to pass to the template, as in:
property :get_iptables_info, [TrueClass, FalseClass], default: false
property :get_pkglist, [TrueClass, FalseClass], default: false
property :cw_logs, Array, default: [], required: false
action :create do
ruby_block 'cw_iptables' do
block do
new_resource.cw_logs.push({ "#{new_resource.custom_dir}/iptables/iptables.txt" => { "log_group_name" => new_resource.log_group_name+"/iptables"}})
end
action :run
only_if {new_resource.get_iptables_info}
end
template "my_template" do
variables ({:logstreams => cw_logs})
end
end
Then in my template:
<% #logstreams.each_pair do |path, _object| %>
["#{path}"]
log_group_name = _object["log_group_name"]
<% end %>
The problem is that properties are immutable. So I get the error:
RuntimeError
------------
ruby_block[cw_iptables] (/tmp/kitchen/cache/cookbooks/custom_cw/resources/logs.rb line 43) had an error: RuntimeError: can't modify frozen Array
What's the correct way to do this? What's the correct way to write ruby code within the resource so that it's more modular and uses methods/functions?

Mutating properties inside a custom resource is generally a bad idea (there are exceptions but this isn't one of them). Better would be to use a local variable. Related, you don't need to use a ruby_block like that when you're already in a custom resource's action:
action :create do
cw_logs = new_resource.cw_logs
if new_resource.get_iptables_info
cw_logs += [whatever extra stuff you want]
end
template "my_template" do
variables logstreams: cw_logs
end
end
Notably that's using += instead of push which doesn't mutate the original. If you used push you would want cw_logs = new_resource.cw_logs.dup or similar.

Related

Creating nested Puppet fact (Ruby) by iterating over gem query output

I have working Ruby code to query DNS details and create Puppet custom facts (puppet 5, Facter 3.11.6) however I am trying to modify it to create nested facts from the key/value pairs that the query obtains.
Code that works to set individual facts with the key name is:
require 'resolv'
Resolv::DNS::Config.default_config_hash.each do | key, value |
if !value.nil?
Facter.add("dns_#{key}") do
if value.is_a?(Array)
setcode { value.join(',') }
else
setcode { value }
end
end
end
end
which creates individual facts thus:
dns_nameserver => 192.168.1.1,192.168.1.2
dns_ndots => 1
dns_search => test.domain
My failed attempt so far to create a nested fact under the parent fact of 'DNS' is:
require 'resolv'
Facter.add("dns") do
value ={}
Resolv::DNS::Config.default_config_hash.each do | key, result |
if !result.nil?
if result.is_a?(Array)
setcode { value['#{key}'] = result.join(',') }
else
setcode { value['#{key}'] = result }
end
end
end
end
which gives a limited result of just:
dns => 1
Other code I have tried seems to put an array output into the string and multiple IPs are quoted inside square brackets over 2 lines instead of being output as per the first code block at top of page.
The fact structure I am TRYING to achieve (by modifying the top of page code) is:
dns => {
nameserver => 192.168.1.1,192.168.1.2,
ndots => 1,
search => test.domain,
}
Thanks in advance for any assistance.
I finally got this with the assistance from a poster who put some great code leads here, but unfortunately removed it soon afterward. Here is the code that works:
require 'resolv'
Facter.add(:networking_dns) do
setcode do
Resolv::DNS::Config.default_config_hash.each_with_object({}) do | (key, value), sub|
if !value.nil?
sub[key] = value
sub
end
end
end
end
Now for some explanatory notes (please feel free to correct me or offer any optimisations to this):
# the resolv gem is required
require 'resolv'
# create the parent fact (has no value of its own)
Facter.add(:networking_dns) do
# start building instructions in the fact
setcode do
# use the resolv gem to lookup values in /etc/resolv.conf and add .each to process all key/value pairs returned
# also add _with_object({}) and sub in the variables to set a blank value for sub. Saves doing it separately. Sub can be any name but denotes the declaration for the nested facts
Resolv::DNS::Config.default_config_hash.each_with_object({}) do | (key, value), sub|
# create facts only when the value is not nil
if !value.nil?
sub[key] = value
sub
# using a closing blank entry for a nested fact is critical or they won't create! Place this outside of the case statement to prevent blank values
end
end
end
end
# use the appropriate number of ends and indent for readability
Thanks to the person who posted their guidance here before removing it. I would like to upvote you if you post again.
Any tips on optimisation to the able solution are welcome, as I'm still grasping Ruby (spent hours on this!)

Chef/Ruby: Iterating through a Hash Array

I'm trying to iterate through a hash defined in my Chef attributes file and write it to a config template:
default['disk'] = node['block_device'].select { |i,j| j['state'] == 'running' && i != 'cdrom' }.select { |r| puts "Disk #{r}"}
In my Chef template, I am calling the variable with <%= #disk %>, so all the work is being done in the attributes file variable.
The above attribute will show me the result I want when it is compiling the cookbook, but using the puts method will not write to the config template, and I come up with empty strings written instead (see below).
Compiling Cookbooks...
Disk sda
Disk sdb
Converging 7 resources
....
+ Disk "{}"
If I remove the puts method (should not need it to write to config template), then I get the entire ['block_device'] structure (instead of just the device name) as a Disk value written to config template instead.
I've also tried playing around with using the puts method within the config template as well, but got no where. How can I write a new line in my template per key value in the array during a chef-client run? I would like to get it written to the config template instead of STDOUT during compile??
Chef templates use Erb formatting do you would want to actually use that:
# recipe
template '/asdf' do
# ...
variables disks: node['block_device'].select { |i,j| j['state'] == 'running' && i != 'cdrom' }
end
# template
<%- #disk.each do |i, j| -%>
<%= i %>
<%- end -%>

Unable to set node attribute in runtime and reference it in chef template

I am getting template error due to backend.key=<%= node['key']%> used in source key.properties.erb doesn’t have a value while running shellout.
Error : Chef::Mixin::Template::TemplateError - undefined method `[]' for nil:NilClass
I have a ruby block to get the output of the file cat /tmp/key.txt and assigning as a node value.
Ruby block :
ruby_block "Get_key" do
block do
#tricky way to load this Chef::Mixin::ShellOut utilities
Chef::Resource::RubyBlock.send(:include, Chef::Mixin::ShellOut)
command = 'cat /tmp/key.txt'
command_out = shell_out(command)
node.set['key'] = command_out.stdout
end
action :create
end
Erb :
backend.key=<%= node['key']%>
There is no need to shell_out to read the contents of a file. Try this instead:
ruby_block "Get_key" do
only_if { node['key'] == "" }
block do
node.set['key'] = File.read('/tmp/key.txt')
end
end
But I think your actual problem is somewhere else. The error message indicates that node is nil within your template, which is pretty unusual.
So either I am blind and you really have some typo in the posted template line, or you simplified your code example in such a way that it hides your error. I assume your real template looks more like
backend.key=<%= node['foo']['key']%>
and foo not being an array. Check that.
Please don't use this pattern. It's slow and puts extra data in your node object which takes up space and RAM and makes you search index sad. What you want is this:
template "whatever" do
# Other stuff ...
variables my_file: lazy { IO.read('/tmp/key.txt') }
end
That will delay the read until converge time.

How do you create pretty json in CHEF (ruby)

How would you make an erb template that has human readable json?
The following code works, but it makes a flat json file
default.rb
default['foo']['bar'] = { :herp => 'true', :derp => 42 }
recipe.rb
template "foo.json" do
source 'foo.json.erb'
variables :settings => node['foo']['bar'].to_json
action :create
end
foo.json.erb
<%= #settings %>
Similar SO questions
Chef and ruby templates - how to loop though key value pairs?
How can I "pretty" format my JSON output in Ruby on Rails?
As pointed out by this SO Answer .erb templates are great for HTML, and XML, but is not good for json.
Luckily, CHEF uses its own json library which has support for this using .to_json_pretty
#coderanger in IRC, pointed out that you can use this library right inside the recipe. This article shows more extensively how to use chef helpers in recipes.
default.rb
# if ['foo']['bar'] is null, to_json_pretty() will error
default['foo']['bar'] = {}
recipe/foo.rb
pretty_settings = Chef::JSONCompat.to_json_pretty(node['foo']['bar'])
template "foo.json" do
source 'foo.json.erb'
variables :settings => pretty_settings
action :create
end
Or more concise as pointed out by YMMV
default.rb
# if ['foo']['bar'] is null, to_json_pretty() will error
default['foo']['bar'] = {}
recipe/foo.rb
template "foo.json" do
source 'foo.json.erb'
variables :settings => node['foo']['bar']
action :create
end
templates/foo.json.erb
<%= Chef::JSONCompat.to_json_pretty(#settings) %>
Something like this would also work:
file "/var/my-file.json" do
content Chef::JSONCompat.to_json_pretty(node['foo']['bar'].to_hash)
end
<%= Chef::JSONCompat.to_json_pretty(#settings) %> Works like Charm !!

Chef Recipes - Setting node attributes in ruby_block

I have a Chef recipe for a multi-node web service, each node of which needs to get the hostname and IP of the other nodes, to put it into its own local configuration.
The code is shown below. The problem is that when the node.set[][] assignments are made in the ruby_block as shown, the values are empty when the template that relies upon them is created. If I want to create that template, I have to move all of the ruby_block code outside, and have it "loose" in the recipe. Which makes it harder to do unit-testing with Chefspec and the like.
Can any Chef guru set me straight? Is it just impossible to do node.set[] like this inside of a ruby_block? And if so, why doesn't it say so in the docs?
$cm = { :name => "web", :hostname => "" , :ip_addr => "" }
$ca = { :name => "data", :hostname => "" , :ip_addr => "" }
$cg = { :name => "gateway", :hostname => "" , :ip_addr => "" }
$component_list = [$cm, $ca, $cg]
ruby_block "get host addresses" do
block do
for cmpnt in $component_list
# do REST calls to external service to get cmpnt.hostname, ip_addr
# .......
node.set[cmpnt.name]['name'] = cmpnt.name
node.set[cmpnt.name]['host'] = cmpnt.hostname
node.set[cmpnt.name]['ip'] = cmpnt.ip_addr
end
end
end
template "/etc/app/configuration/config.xml" do
source "config.xml.erb"
variables( :dataHost => node['data']['host'],
:webHost => node['web']['host'],
:gatewayHost => node['gateway']['host'] )
action :create
end
I also added
subscribes :create, "ruby_block[get host addresses]", :immediately
to the template definition to ensure that the ruby_block ran before the template was created. This didn't make a difference.
I realize this is an old post, however for future reference, I just ran across this gist which gives a nice example of node variable assignments in the Compile vs. Converge phases. To adapt the gist to your example, you'll need to add code like the following to your ruby_block:
template_r = run_context.resource_collection.find(:template => "/etc/app/configuration/config.xml")
template_r.content node['data']['host']
template_r.content node['web']['host']
template_r.content node['gateway']['host']
For Chef 11, also see Lazy Attribute Evaluation.
The problem seems to be that attribute values inside your template resource definition get evaluated before actually invoking any resources.
I.e. the file is first executed as simple Ruby, compiling the resources, and only the the resource actions gets invoked. By that time, it is too late already.
I ran into the same problem when trying to encapsulate certain attribute manipulations into a resource. It simply does not work. Should anyone know a solution to this problem, I would appreciate it very much.
EDIT:
b = ruby_block...
...
end
b.run_action(:create)
Could possibly do the trick. It invokes the resource immediately.
The simplest answer to this is to not use chef attributes and not use ruby_block to do the work of talking to the REST API. The code can also be moved to a custom resource for better reuse:
unified_mode true
provides :my_resource
action :run do
cm = { :name => "web", :hostname => "" , :ip_addr => "" }
ca = { :name => "data", :hostname => "" , :ip_addr => "" }
cg = { :name => "gateway", :hostname => "" , :ip_addr => "" }
component_list = [cm, ca, cg]
hash = {}
for cmpnt in component_list
# do REST calls to external service to get cmpnt.hostname, ip_addr
# .......
hash[cmpnt.name] = {}
hash[cmpnt.name]['name'] = cmpnt.name
hash[cmpnt.name]['host'] = cmpnt.hostname
hash[cmpnt.name]['ip'] = cmpnt.ip_addr
end
template "/etc/app/configuration/config.xml" do
source "config.xml.erb"
variables( :dataHost => hash['data']['host'],
:webHost => hash['web']['host'],
:gatewayHost => hash['gateway']['host'] )
action :create
end
end
By using unified_mode and moving into a custom resource, it also makes it easier to use a node attribute without requiring the use of lazy {} or ruby_blocks. It also still allows chef configuration (like setting up resolv.conf or other network requirements before doing the REST calls) prior to calling this code while not having to think about compile/converge two pass issues in recipe context.
There is also no reason to use a resource like ruby_block to do pure ruby processing which does not change the system under management. In this case the ruby_block is hitting a REST service purely to collect data. That does not need to be placed into a Chef resource. It isn't clear from the question if that was being done because the questioner though it was a "best practice" (in this case it is not), or if it was being done to move execution to compile time in order to allow other chef resources that aren't part of the question to fire first (in which case using a custom resource is a much better solution than using a ruby_block).
It's been a while since this question, but in case someone is still looking for it, lazy evaluate is your friend:
template '/tmp/sql_file.sql' do
source "sql_file.sql.erb"
mode 0700
variables lazy {
# Create a new instance of MySQL library
mysql_lib = Acx::MySQL.new(
'127.0.0.1', 'root', node['mysql']['service']['pass']
)
password = node['mysql']['service']['support_admin']['ct_password']
# It returns the encrypted password after evaluate it, to
# be used in template variables
{ admin_password: mysql_lib.encrypted_password(password) }
}
end
https://docs.chef.io/resource_common.html#lazy-evaluation

Resources