Chef/Ruby: Iterating through a Hash Array - ruby

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 -%>

Related

Nested iteration in Puppet template [Ruby]

I started to learn Puppet this week and trying hard to implement user's keys to /etc/ssh/sudo_authorized_keys.
I have a dictionary of users with keys in sudo_users.yaml:
core::sudo_users_keys:
kate:
keys:
key1:
type: "ssh-ed25519"
key: "AAAAC3N..."
john:
keys:
key1:
type: "ssh-ed25519"
key: "AAAAC..."
marvin:
keys:
key1:
type: "ssh-ed25519"
key: "AAAAC3Nza..."
Then I create this in sudokeys.pp file:
class core::sudokeys {
file { "/etc/ssh/sudo_authorized_keys":
ensure => file,
mode => "0440",
content => template("core/sudo_authorized_keys.erb"),
}
As you can see, I want to implement template with iteration.
This is my current template:
<%- scope['core::sudo_users_keys'].each | user | -%>
{
<%- if user[1] -%>
<%- $user[1]['keys'].each do | key | -%>
<%= $key[1]['type'] $key[1]['key'] -%>
<%- end end -%>
}
<%- end -%>
I have the same dictionary with id_rsa keys to ssh and use interaction as below.
It works perfectly for ssh_authorized_key, but I can use it in this case by adding keys to /etc/ssh/sudo_authorized_keys. That's why I decided to use a template and only inject keys inside the sudo_authorized_keys file.
class core::sshkeys {
lookup("core::sudo_users_keys").each | $user | {
if $user[1] {
$user[1]["keys"].each | $key | {
ssh_authorized_key { "${user[0]}-${key[0]}":
ensure => present,
user => $user[0],
type => $key[1]["type"],
key => $key[1]["key"],
}
}
}
}
}
Puppet documentation doesn't include this kind of more complicated iterations and I feel like wandering in the fog.
Currently, I'm getting this error when deploying my template, but I feel like the way I prepare this will not work as I wanted.
Internal Server Error: org.jruby.exceptions.SyntaxError: (SyntaxError) /etc/puppetlabs/code/environments/test/modules/core/templates/sudo_authorized_keys.erb:2: syntax error, unexpected tSTRING_BEG
_erbout.<< " {\n".freeze
^
I will appreciate any suggestions about template construction. What should I change to make it work and extract only type and key values?
Your approach is workable, but there are multiple issues with your code, among them
the scope object in a template provides access to Puppet variables, not to Hiera data. Probably the easiest way to address that would be to
give your core::sudokeys class a class parameter to associate with the data ($userkeys, say),
change the Hiera key for the data so that it will be automatically bound to the new class parameter, and
in the template, access the data via the class parameter (which doesn't even require using the scope object in that case).
in the template, you seem to be assuming a different structure for your data than it actually has in Hiera. Your data is a hashes of hashes of hashes, but you are accessing parts of it as if there were arrays involved somewhere (user[1], key[1]). Also, if there were one or more arrays, then do be aware that Ruby array indexes start at 0, not at 1.
Scriptlet code in an ERB template is (only) Ruby. Your scriptlet code appears to mix Ruby and Puppet syntax. In particular, Ruby variable names are not prefixed with a $, and Ruby block parameters appear inside the block, not outside it.
<%= ... %> tags a for outputting the value of one Ruby expression each. You appear to be trying to emit multiple expressions with the same tag.
Also, it's worth noting that scriptlet code can span multiple lines. That can make it clearer. Additionally, you may need to pay attention to whitespace and newlines in your template to get the output you want. ERB has options to consume unwanted whitespace around scriptlet tags, if needed, but sometimes its at least as easy to just avoid putting in whitespace that you don't want. On the other hand, be sure not to suppress whitespace that you actually wanted.
So, here's one form that all of the above might take:
sudo_users.yaml
core::sudokeys::users_keys:
kate:
keys:
key1:
type: "ssh-ed25519"
key: "AAAAC3N..."
john:
keys:
key1:
type: "ssh-ed25519"
key: "AAAAC..."
marvin:
keys:
key1:
type: "ssh-ed25519"
key: "AAAAC3Nza..."
sudokeys.pp
class core::sudokeys($users_keys) {
file { "/etc/ssh/sudo_authorized_keys":
ensure => file,
mode => "0440",
content => template("core/sudo_authorized_keys.erb"),
}
}
sudo_authorized_keys.erb
<% #users_keys.each do | user |
if user.has_key?('keys') do
user['keys'].each do |key|
%><%= key['type'] %> <%= key['key'] %>
<% # don't consume preceding whitespace here
end
end
end
%>
I used John Bollinger answer, however template wasn't perfect in my case, because I was constantly getting errors like eg. syntax error, unexpected end-of-file _erbout.
The proper sudo_authorized_keys.erb file that worked for me is this one:
<%- #users_keys.each do | user, config | -%>
<%- if config.has_key?('keys') -%>
<%- config['keys'].each do | name, value | -%>
<%= value['type'] %> <%= value['key'] %> <%= user %>
<%- end -%>
<%- end -%>
<%- end -%>

Chef Custom Resources : mutable property or an alternative

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.

Chef template loop: can't convert Chef::Node::immutableMash into String

I've got a Vagrant setup in which I'm trying to use Chef-solo to generate an conf file which loops though defined variables to pass to the application. Everything is working except the loop and I'm not familiar enough with Ruby/Chef to spot the error.
I'm going to lay out the whole chain of events in case there is something along the way that is the problem, but the first portions of this process seem to work fine.
A config file is written in yaml and includes env variable definitions to be passed:
...
variables:
- DEBUG: 2
...
The config file is read in by the Vagrantfile into a ruby hash and used to create the Chef json nodes:
...
settings = YAML::load(File.read("config.yaml"))
# Provision The Virtual Machine Using Chef
config.vm.provision "chef_solo" do |chef|
chef.json = {
"mysql" => {"server_root_password" => "secret"},
"postgresql" => {"password" => {"postgres" => "secret"}},
"nginx" => {"pid" => "/run/nginx.pid"},
"php-fpm" => {"pid" => "/run/php5-fpm.pid"},
"databases" => settings["databases"] || [],
"sites" => settings["sites"] || [],
"variables" => settings["variables"] || []
}
...
A bunch of chef cookbooks are run (apt, php, nginx, mysql etc) and finally my custom cookbook which is whats giving me grief. The portion of the cookbook responsible for creating a the conf file is shown here:
# Configure All Of The Server Environment Variables
template "#{node['php-fpm']['pool_conf_dir']}/vars.conf" do
source "vars.erb"
owner "root"
group "root"
mode 0644
variables(
:vars => node['variables']
)
notifies :restart, "service[php-fpm]"
end
And the vars.erb is just a one-liner
<%= #vars.each {|key, value| puts "env[" + key + " = " + value } %>
So, when I run all this chef spits out an error about not being able to convert a hash to a string.
can't convert Chef::Node::immutableMash into String
So for some reason this is coming across as an immutableMash and the value of key ends up being the hash [{"DEBUG"=>2}] and value ends up a nil object, but I'm not sure why or how to correct it.
The hash is ending up as the value of key in your example because the YAML file declares DEBUG: 2 as a list member of variables. This translates to variables being an array with a single hash member.
Try changing the template code to this:
<%= #vars[0].each {|key, value| puts "env[" + key + " = " + value } %>
Or try changing the YAML to this and not changing the template code:
variables:
DEBUG: 2
Either change will get your template loop iterating over the hash that you are expecting.

Iterate though ENV and write the result to a file in Chef

I am trying to iterate through the EVN Hash and save the result into a file. Below is my attempt:
file "/srv/www/shared/test-create-file.txt" do
owner "root"
group "root"
mode "0755"
tempVar = ""
ENV.each_pair do |k, v|
tempVar = tempVar#{k}#{" = "}#{v}
end
content tempVar
action "create"
end
The file is created but with an empty content. Could I get some hints?
You don't really need to create a variable for that:
file "/srv/www/shared/test-create-file.txt" do
owner "root"
group "root"
mode "0755"
content ENV.map { |k,v| "#{k} = #{v}" }.join("\n")
action "create"
end
In case you just want to fix your iteration, try this instead:
ENV.each_pair do |k, v|
tempVar = "#{tempVar}#{k} = #{v}\n"
end
You don't need to create a temporary variable in this case. You can just pass the method directly into the content block as cassianoleal suggested.
However, I would strongly suggest moving to a template resource. You are performing data manipulation, which should really be left for a template:
template "/srv/www/shared/test-create-file.txt" do
source "my-template.erb"
owner "root"
group "root"
mode "0755"
action "create"
end
And then in the template:
<% ENV.each do |key, value| %>
<%= key %>=<%= value %>
<% end %>
You can read more about the ERB templating language and Chef templates on the Chef Docs.
The hash (#) symbol in ruby is for making comments. Every thing after the hash (#) symbol on line 7 is being considered a comment.
In the each_pair block you are continually reassigning tempVar to tempVar. As tempVar is initially an empty String it is setting tempVar to an emptyString in each iteration.
If you're expecting tempVar to accumulate the return value from the each_pair block, you should consider using inject:
tempVar = {key1: :value1, key2: :value2}.inject('') do |memo, member|
memo += "#{member[0]} = #{member[1]}\n"
end
# tempVar => "key1 = value1\nkey2 = value2\n"

Sorting a YAML output in an ERB template

In an ERB template used by Puppet, I'm trying to sort a YAML output of a hash to make sure it's always the same output.
Here is what I have so far (in mytest/templates/hash.erb):
<%=
class MyHash < Hash
def to_yaml( opts = {} )
YAML::quick_emit( self, opts ) do |out|
out.map( taguri, to_yaml_style ) do |map|
keys.sort.each do |k|
v = self[k]
map.add( k, v )
end
end
end
end
end
myScope = scope.to_hash.reject{|k,v| k.to_s =~ /(uptime|timestamp|free)/}
MyHash[myScope].to_yaml
-%>
which yields:
$ puppet apply -e 'include mytest' --modulepath .
Failed to parse template mytest/hash.erb:
Filepath: /usr/lib/ruby/1.8/yaml.rb
Line: 391
Detail: wrong argument type String (expected Data)
at /home/rpinson/bas/puppet/mytest/manifests/init.pp:3 on node foo.example.com
This is the content of mytest/manifests/init.pp:
class mytest {
notify { 'toto':
message => template('mytest/hash.erb'),
}
}
I can't seem to understand where this type Data comes from, and how to cast the parameters properly in order to get this to work…
It turns out, the source of problem is ZAML, YAML replacement which Puppet uses for performance purposes.
It can be proved by testing this erb-template without Puppet.
I'm pretty sure it will work.
I'm investigating to allow to work them together.
UPDATE:
ZAML.dump(MyHash[myScope]) # instead of MyHash[myScope].to_yaml
This may heal problem, at least it does my one that really similar.

Resources