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

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!)

Related

How do I tack a string onto a variable and evaluated the entire thing as a variable in Ruby?

I have the following Ruby code:
module BigTime
FOO1_MONEY_PIT = 500
FOO2_MONEY_PIT = 501
class LoseMoney
##SiteName = 'FOO1'
#site_num = ##SiteName_MONEY_PIT
def other_unimportant_stuff
whatever
end
end
end
So, what I'm trying to do here is set the SiteName and then use SiteName and combine it with the string _MONEY_PIT so I can access FOO1_MONEY_PIT and store its contents (500 in this case) in #site_num. Of course, the above code doesn't work, but there must be a way I can do this?
Thanks!!
If you want to dynamically get the value of a constant, you can use Module#const_get:
module BigTime
FOO1_MONEY_PIT = 500
FOO2_MONEY_PIT = 501
class LoseMoney
##SiteName = 'FOO1'
#site_num = BigTime.const_get(:"#{##SiteName}_MONEY_PIT")
end
end
Do not, under any circumstance, use Kernel#eval for this. Kernel#eval is extremely dangerous in any context where there is even the slightest possibility that an attacker may be able to control parts of the argument.
For example, if a user can choose the name of the site, and they name their site require 'fileutils'; FileUtils.rm_rf('/'), then Ruby will happily evaluate that code, just like you told it to!
Kernel#eval is very dangerous and you should not get into the habit of just throwing an eval at a problem. It is a very specialized tool that should only be employed when there is no other option (spoiler alert: there almost always is another option), and only after a thorough security review.
Please note that dynamically constructing variable names is already a code smell by itself, regardless of whether you use eval or not. It pretty much always points to a design flaw somewhere. In general, you can almost guaranteed replace the multiple variables with a data structure. E.g. in this case something like this:
module BigTime
MONEY_PITS = {
'FOO1' => 500,
'FOO2' => 501,
}.freeze
class LoseMoney
##SiteName = 'FOO1'
#site_num = MONEY_PITS[##SiteName]
end
end
You can refactor this as to use a Hash for your name lookups, and a getter method to retrieve it for easy testing/validation. For example:
module BigTime
MONEY_PITS = { FOO1: 500, FOO2: 501 }
MONEY_PIT_SUFFIX = '_MONEY_PIT'
class LoseMoney
##site = :FOO1
def initialize
site_name
end
def site_name
#site_name ||= '%d%s' % [MONEY_PITS[##site], MONEY_PIT_SUFFIX]
end
end
end
BigTime::LoseMoney.new.site_name
#=> "500_MONEY_PIT"

Dump :node object in chef

I am a dev tasked with making some changes to the cookbooks owned by the devops team. We don't have access to knife or the chef server.
I tried to add some debugging to an existing chef recipe
members = search(
:node,
search_str,
filter_result: {
'name' => ['name'],
}
#
# print statements NOT working as expected
#
p ":node=" + :node.to_s # <--- prints ":node=node"
pp :node.to_s # <--- prints "node"
pp :node # <--- prints :node
Is it possible to dump the entire json structure of the :node object? Any workarounds. I want to see the structure of the :node object to debug the search() call as the search isn't working as expected.
Thank you.
:node in ruby is a symbol, not a variable. Symbols always start with colon (:). For starters you can think of them as of immutable strings. That is why you get such output. You just print a lot of strings.
What you actually need is just node:
pp "node:", node
But beware, node is a huge object with a lot of attributes.

Why negative value becomes positive after dumping into yaml file?

I have a simple sinatra app uses yaml files to handle data. One of the feature is an User can vote or veto for a Question. The vote feature works fine, but I met some weird things when implementing the veto feature.
Briefly saying:
when a question's current votes_count is positive (>= 1), the number will decrease correctly
but when a question's current votes_count is zero or negative, the number will successfully decrease in the data hash, but after dump data hash into the yaml file, negative becomes positive.
This is the yaml file for Question:
'1': !ruby/hash:Sinatra::IndifferentHash
title: " Best way to require all files from a directory in ruby?"
description: What's the best way to require all files from a directory in ruby ?
user_id: '3'
votes_count: 0
# other user information
This is the route handler relates to veto feature:
post "/questions/:id/veto" do
check_vote_validity_for_question(params[:id])
#question = Question.find_by(:id, params[:id])
#question.votes_count = (#question.votes_count.to_i - 1)
Question.update(params[:id], votes_count: #question.votes_count )
# omit user related code
end
This is the update method:
def self.update(id, attrs)
data = load_data_of(data_name)
# binding.pry
obj_info = data[id]
attrs.each do |k, v|
v = v.to_s if v.is_a?(Array)
obj_info[k] = v
end
# binding.pry
File.open(File.join(data_path, "#{data_name.to_s}.yaml"), "w+") do |f|
f.write(Psych.dump(data).delete("---"))
end
end
If I pause the program inside update method before and after update the data hash, it shows the value of votes_count was set correctly.
Before:
[1] pry(Question)> data
=> {"1"=>
{"title"=>" Best way to require all files from a directory in ruby?",
"description"=>"What's the best way to require all files from a directory in ruby ?",
"user_id"=>"3",
"votes_count"=>0},
After:
[1] pry(Question)> data
=> {"1"=>
{"title"=>" Best way to require all files from a directory in ruby?",
"description"=>"What's the best way to require all files from a directory in ruby ?",
"user_id"=>"3",
"votes_count"=>-1},
The value of the key"votes_count" in data hash is -1 after updating, but after I dumping the data hash into yaml file, the value of "votes_count" of the user in the yaml file became 1. And if the value in hash is -2, it will become 2 in the yaml file.
I tried making a hash which has a negative value in irb, then dump it into a yaml file, things work ok. I have no idea what happened. Could anybody help me?
It looks the problem in the line
f.write(Psych.dump(data).delete("---"))
You delete -.
For example
"-1".delete("---") #=> "1"

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 to read data from a different file without using YAML or JSON

I'm experimenting with a Ruby script that will add data to a Neo4j database using REST API. (Here's the tutorial with all the code if interested.)
The script works if I include the hash data structure in the initialize method but I would like to move the data into a different file so I can make changes to it separately using a different script.
I'm relatively new to Ruby. If I copy the following data structure into a separate file, is there a simple way to read it from my existing script when I call #data? I've heard one could do something with YAML or JSON (not familiar with how either work). What's the easiest way to read a file and how could I go about coding that?
#I want to copy this data into a different file and read it with my script when I call #data.
{
nodes:[
{:label=>"Person", :title=>"title_here", :name=>"name_here"}
]
}
And here is part of my code, it should be enough for the purposes of this question.
class RGraph
def initialize
#url = 'http://localhost:7474/db/data/cypher'
#If I put this hash structure into a different file, how do I make #data read that file?
#data = {
nodes:[
{:label=>"Person", :title=>"title_here", :name=>"name_here"}
]
}
end
#more code here... not relevant to question
def create_nodes
# Scan file, find each node and create it in Neo4j
#data.each do |key,value|
if key == :nodes
#data[key].each do |node| # Cycle through each node
next unless node.has_key?(:label) # Make sure this node has a label
#WE have sufficient data to create a node
label = node[:label]
attr = Hash.new
node.each do |k,v| # Hunt for additional attributes
next if k == :label # Don't create an attribute for "label"
attr[k] = v
end
create_node(label,attr)
end
end
end
end
rGraph = RGraph.new
rGraph.create_nodes
end
Given that OP said in comments "I'm not against using either of those", let's do it in YAML (which preserves the Ruby object structure best). Save it:
#data = {
nodes:[
{:label=>"Person", :title=>"title_here", :name=>"name_here"}
]
}
require 'yaml'
File.write('config.yaml', YAML.dump(#data))
This will create config.yaml:
---
:nodes:
- :label: Person
:title: title_here
:name: name_here
If you read it in, you get exactly what you saved:
require 'yaml'
#data = YAML.load(File.read('config.yaml'))
puts #data.inspect
# => {:nodes=>[{:label=>"Person", :title=>"title_here", :name=>"name_here"}]}

Resources