Puppet - create NESTED custom fact - ruby

I have successfully created a .rb custom fact that parses a built-in fact to create a new value, however I am now trying to use it as nested custom fact for Puppet.
The hierarchy I want to create is similar to built-in facts, eg running Facter (or Facter -p) would show:
custom_parent => {
custom_fact_1 => whatever
custom_fact_2 => whatever2
}
and usage in a puppet manifest would be:
$custom_parent.custom_fact_1
So far I have tried leading syntax such as:
Facter.add (:custom_parent)=>(custom_fact_1) do
Facter.add (:custom_parent)(:custom_fact_1) do
Facter.add (:custom_parent.custom_fact_1) do
Facter.add (:custom_parent:custom_fact_1) do
Facter.add (custom_parent:custom_fact_1) do
...and many other variations however cannot get a nested custom fact array to create. I've Googled for a while and if anyone knows if it is possible I would be very grateful.
I did find that nested facts can be created using an array in a .yaml file in the /etc/puppetlabs/facter/facts.d/ directory as below, however this sets FIXED values and does not process logic which I require in my custom fact.
{
"custom_parent":
{
"custom_fact_1": "whatever",
"custom_fact_2": "whatever2",
}
}
Thanks in advance.

The hierarchy I want to create is similar to built-in facts, eg
running Facter (or Facter -p) would show:
custom_parent => {
custom_fact_1 => whatever
custom_fact_2 => whatever2
}
There are no "nested" facts. There are "structured" facts, however, and these may have hashes as their values. To the extent that Facter presents output that you characterize as "nested", that's surely what you're looking at.
Since the elements of a structured fact's value are not facts in their own right, they need to be specified in the resolution of the fact itself:
Facter.add (:custom_parent) do
{
:custom_fact_1 => 'whatever',
:custom_fact_2 => 'whatever2',
}
end
The whatever and whatever2 do not need to be literal strings; they can be more or less arbitrary Ruby expressions. Of course, you can also set the members separately from creating the hash (but in the same fact resolution):
Facter.add (:custom_parent) do
value = new Hash()
value[:custom_fact_1] = 'whatever'
value[:custom_fact_2] = 'whatever2'
value
end

Thank you #John Bollinger. Your example was very close however I found that I needed to use type => aggregate and chunk to get it to work. I also combined it with a defined function, with the end result being based on the code below.
If you have any other suggestions to improve code conformity on this please feel free to point it out. Cheers
# define the function to process the input fact
def dhcp_octets(level)
dhcp_split = Facter.value(:networking)['dhcp'].split('.')
if dhcp_split[-level..-1]
result = dhcp_split[-level..-1].join('.')
result
end
end
# create the parent fact
Facter.add(:network_dhcp_octets, :type => :aggregate) do
chunk(:dhcp_ip) do
value = {}
# return a child => subchild array
value['child1'] = {'child2' => dhcp_octets(2)}
# return child facts based on array depth (right to left)
value['1_octets'] = dhcp_octets(1)
value['2_octets'] = dhcp_octets(2)
value['3_octets'] = dhcp_octets(3)
value['4_octets'] = dhcp_octets(4)
# this one should return an empty fact
value['5_octets'] = dhcp_octets(5)
value
end
end

Related

Searching a Hash

I'm trying to complete this Codewars Challenge and I'm confused as to where I'm going wrong. Could someone please give me a hand?
The question provides a "database" of translations for Welcome, and the instructions say:
Think of a way to store the languages as a database (eg an object). The languages are listed below so you can copy and paste!
Write a 'welcome' function that takes a parameter 'language' (always a string), and returns a greeting - if you have it in your database. It should default to English if the language is not in the database, or in the event of an invalid input.
My attempt:
def greet(language)
greeting = { 'english'=>'Welcome',
'czech'=>'Vitejte',
'danish'=>'Velkomst',
'dutch'=>'Welkom',
'estonian'=>'Tere tulemast',
'finnish'=>'Tervetuloa',
'flemish'=>'Welgekomen',
'french'=>'Bienvenue',
'german'=>'Willkommen',
'irish'=>'Failte',
'italian'=>'Benvenuto',
'latvian'=>'Gaidits',
'lithuanian'=>'Laukiamas',
'polish'=>'Witamy',
'spanish'=>'Bienvenido',
'swedish'=>'Valkommen',
'welsh'=>'Croeso'
}
greeting.key?(language) ? greeting.each { |k, v| return v if language == k } : 'IP_ADDRESS_INVALID'
end
To my eyes when I run my code through the IDE it seems to be working as per request but I guess I must be wrong somehow.
It's telling me it :
Expected: "Laukiamas", instead got: "Welcome"
But when I type:
p greet("lithuanian")
I get Laukiamas.
You can provide you greeting hash with a default value. It is as simple as
greeting.default = "Welcome"
This enhanced hash does all the work for you. Just look up the key; when it is not there you'll get "Welcome".
Preface
First of all, please don't post links to exercises or homework questions. Quote them in your original question to avoid link rot or additional create work for people trying to help you out.
Understanding the Problem Defined by the Linked Question
Secondly, you're misunderstanding the core question. The requirement is basically to return the Hash value for a given language key if the key exists in the Hash. If it doesn't, then return the value of the 'english' key instead. Implicit in the exercise is to understand the various types of improper inputs that would fail to find a matching key; the solution below addresses most of them, and will work even if your Ruby has frozen strings enabled.
A Working Solution
There are lots of ways to do this, but here's a simple example that will handle invalid keys, nil as a language argument, and abstract away capitalization as a potential issue.
DEFAULT_LANG = 'english'
TRANSLATIONS = {
'english' => 'Welcome',
'czech' => 'Vitejte',
'danish' => 'Velkomst',
'dutch' => 'Welkom',
'estonian' => 'Tere tulemast',
'finnish' => 'Tervetuloa',
'flemish' => 'Welgekomen',
'french' => 'Bienvenue',
'german' => 'Willkommen',
'irish' => 'Failte',
'italian' => 'Benvenuto',
'latvian' => 'Gaidits',
'lithuanian' => 'Laukiamas',
'polish' => 'Witamy',
'spanish' => 'Bienvenido',
'swedish' => 'Valkommen',
'welsh' => 'Croeso'
}
# Return a translation of "Welcome" into the language
# passed as an argument.
#
# #param language [String, #to_s] any object that can
# be coerced into a String, and therefore to
# String#downcase
# #return [String] a translation of "Welcome" or the
# string-literal +Welcome+ if no translation found
def greet language
language = language.to_s.downcase
TRANSLATIONS.fetch language, TRANSLATIONS[DEFAULT_LANG]
end
# Everything in the following Array of examples except
# +Spanish+ should return the Hash value for +english+.
['Spanish', 'EspaƱol', 123, nil].map { greet(_1) }
This will correctly return:
#=> ["Bienvenido", "Welcome", "Welcome", "Welcome"]
because only Spanish (when lower-cased) will match any of the keys currently defined in the TRANSLATIONS Hash. All the rest will use the default value defined for the exercise.
Test Results
Since there are some RSpec tests included with the linked question:
describe "Welcome! Translation" do
it "should translate input" do
Test.assert_equals(greet('english'), 'Welcome', "It didn't work out this time, keep trying!");
Test.assert_equals(greet('dutch'), 'Welkom', "It didn't work out this time, keep trying!");
Test.assert_equals(greet('IP_ADDRESS_INVALID'), 'Welcome', "It didn't work out this time, keep trying!")
end
end
The code provided not only passes the provided tests, but it also passes a number of other edge cases not defined in the unit tests. When run against the defined tests, the code above passes cleanly:
If this is homework, then you might want to create additional tests to cover all the various edge cases. You might also choose to refactor to less idiomatic code if you want more explanatory variables, more explicit intermediate conversions, or more explicit key handling. The point of good code is to be readable, so be as explicit in your code and as thorough in your tests as you need to be in order to make debugging easier.

How to parse two elements from a list to make a new one

I have this input repeated in 1850 files:
[
{
"id"=>66939,
"login"=>"XXX",
"url"=>"https://website.com/XX/users/XXX"
},
...
{}
]
And I wanted to make a list in a way that by looking for the login I can retrieve the ID using a syntax like:
users_list[XXX]
This is my desired output:
{"XXX"=>"66570", "XXX"=>"66570", "XXX"=>"66570", "XXX"=>"66570", ... }
My code is:
i2 = 1
while i2 != users_list_raw.parsed.count
temp_user = users_list_raw.parsed[i2]
temp_user_login = temp_user['login']
temp_user_id = temp_user['id']
user = {
temp_user_login => temp_user_id
}
users_list << user
i2 += 1
end
My output is:
[{"XXX":66570},{"XXX":66569},{"XXX":66568},{"XXX":66567},{"XXX":66566}, ... {}]
but this is not what I want.
What's wrong with my code?
hash[key] = value to add an entry in a hash. So I guess in your case users_list[temp_user_login] = temp_user_id
But I'm unsure why you'd want to do that. I think you could look up the id of a user by having the login with a statement like:
login = XXX
user = users_list.select {|user| user["login"] == login}.first
id = user["id"]
and maybe put that in a function get_id(login) which takes the login as its parameter?
Also, you might want to look into databases if you're going to manipulate large amounts of data like this. ORMs (Object Relational Mappers) are available in Ruby such as Data Mapper and Active Record (which comes bundled with Rails), they allow you to "model" the data and create Ruby objects from data stored in a database, without writing SQL queries manually.
If your goal is to lookup users_list[XXX] then a Hash would work well. We can construct that quite simply:
users_list = users_list_raw.parsed.each.with_object({}) do |user, list|
list[user['login']] = user['id']
end
Any time you find yourself writing a while loop in Ruby, there might be a more idiomatic solution.
If you want to keep track of a mapping from keys to values, the best data structure is a hash. Be aware that assignment via the array operator will replace existing values in the hash.
login_to_id = {}
Dir.glob("*.txt") { |filename| # Use Dir.glob to find all files that you want to process
data = eval(File.read(filename)) # Your data seems to be Ruby encoded hash/arrays. Eval is unsafe, I hope you know what you are doing.
data.each { |hash|
login_to_id[hash["login"]] = hash["id"]
}
}
puts login_to_id["XXX"] # => 66939

chef attributes value not getting parsed in another attribute

I am setting attributes in default.rb as
default[:my_app] = {
:vol => "data02",
:commitlog => "/foo/bar/node[:vol]/commitlog",
}
But :vol value is not getting parsed in commitlog attribute and I am getting following error.
mError executing action `create` on resource 'directory[/foo/bar/node[:vol]/comitlog]'[0m
You're missing the String interpolation syntax, e.g. y = "The value of X is #{X}." You probably want:
default[:my_app] = {
:vol => "data02",
:commitlog => "/foo/bar/#{node[:vol]}/commitlog",
}
Also, keep in mind that if you make one attribute depend on the value of another, you might override node[:my_app][:vol] later and expect the value of node[:my_app][:commitlog] to change with it, and it may not. The attributes will be parsed together, potentially before your override affects the first one.
Even after I am using the interpolation syntax, and when I am using node[:my_app][:commitlog] in recipe it shows /foo/bar//commitlog

Setting a string containing a variable to be checked when string is used - Ruby 1.9.3

Apologies in advance for what I think might be a stupid question. I promise I have searched, but I'm not sure if I've searched for the correct things. I'm a very new programmer, Ruby is my first serious language, and I taught myself it over the past few months. I've also never asked a question on StackOverflow, so hopefully this is acceptably clear and meets the other requirements for question-asking. Here is the question:
I have a branching method that calls various different APIs and various different URLs within them, based upon the values passed to the method. As is, it looks like:
if api == x
url = "http://url.x.com/api/#{#variable}"
elsif api == y
url = "http://url.y.com/api/public/#{#var1}_#{#var2}/#{#variable}"
etc.
The url's being called are different for each operation, as are the necessary variables. The variables used in the requests are being created as instance variables at the beginning of the method. The possible values of the instance variables are stored in a large hash or are being passed into the method by the call itself. The hash is structured like:
$globalhash =
{
"api_1" =>
{
"call_type_1" => {"info_1" => "", "info_2" => ""},
"call_type_2" => {"info_1" => "", "info_2" => ""}
},
"api_2" =>
{
"call_type_1" => {"info_1" => "", "info_2" => ""},
"call_type_2" => {"info_1" => "", "info_2" => ""}
}
}
The problem I have is that this branching section goes on for a long time in the code--partially because I've done it suboptimally, I'm sure. I'm also sure that my code would be much more efficient if the branching section didn't exist. Ideally, instead of the branching section, I'd like to make this happen:
url = $globalhash[#api][#call_type]["url"]
The value that pulls would be a URL specific to the call type and the api--the address, formatting, and various other differences included. So some values would be:
http://api.x.com/#var1_#var2/#variable
http://api.y.com/public/#variable
and so on. So the structures vary, and I need to access the instance variables stored within the method call, but I need to do so dynamically. The issue I've had is that every way I've tried to implement this results in the values of the instance variables in the strings for "url" being set when $globalhash is read as the program begins to run, with them all being nil. I need it to check the variable when the request is being made, and not before, basically. I have a similar (same?) issue with setting the post_data for the request--if I could find a way to set it in the hash, I'd have cleaner code that runs faster. The post_data is as:
post_data = {'value' => #var1, 'value2' => #var2, 'value3' => #var3}
and so on. The post_data is different for each API and for each call_type; the values are different as are the requested variables for them. I'd like to implement a set of key-value pairs in the hash that look vaguely like:
$globalhash = {"api_1" => {"call_type_1" => {"url" => "http://api.x.com/#{#variable}", "post_data" => "{'value' => #var1, 'value2' => #var2, etc.}"}}}
Except, of course, it would need to work--the variables it needs to reference are nil when $globalhash is being read. I'm not sure how best to solve this.
The program works as-is, but I have a very derpy-looking ~80 lines of branching code that figures out the structure of the URL and the post_data, and then requests it.
The only solution I've considered is creating an instance hash within the method to replace the branching code, that only gets created after declaring the instance variables. However, I'm afraid that would create a similar problem to the current one, of it being inefficient to create a huge new hash every time.
Thanks for reading, and I appreciate your help. I hope the pseudo-code is acceptable, I found it the best way to explain my question.
EDIT: Found a solution. Code:
url = eval "\"#{$globalhash["api_1"]["call_type_1"]["url"]}\""
["url"] references 'http://api.x.com/#{#variable}', single quotes prevent interpolation within the hash, eval function puts it in double quotes and string interpolation collects the instance variable at the time it is called. It's an ugly methodology BUT it does let me greatly shorten my code. Source: https://www.ruby-forum.com/topic/3237101
Instead of creating the hash using the instance variables...
post_data = {'value' => #var1, 'value2' => #var2, 'value3' => #var3}
...you could initialize the hash using strings representing the instance variables.
post_data = {'value' => '#var1', 'value2' => '#var2', 'value3' => '#var3'}
Then when you reference the hash variable, you would wrap the call in an eval.
post_data = {'value' => '#var1', 'value2' => '#var2', 'value3' => '#var3'}
#var2 = 'this is test data'
eval(post_data['value2'])
=> "this is test data"
Similarly I would use placeholders in the url...
url = 'http://api.x.com/[#variable]'
#variable = "cotton_tree"
my_url = url.dup
my_url.sub!(/\[.*?\]/, eval(my_url.match(/\[(.*?)\]/)[1])) while my_url.match(/\[(.*?)\]/)
p my_url
=> "http://api.x.com/cotton_tree"

Is 'buggy_logger' a reference to the 'status' string in this Ruby example?

In this example, do the nukes get launched because any changes that you make to buggy_logger get applied to the 'status' string - just like using a copy of a reference to an object -> when you make a change to the copy of the reference, the change gets applied to the underlying object -> that change is, in turn, reflected in any other references to the object. So, in other words, buggy_logger is an alias to the 'status' object without specifically using the alias keyword? Is that correct? So, in ruby, you just say
b = a
and then any changes you make to b afterwards are also reflected in a. Or is this only true because we're talking about Strings, which are mutable in Ruby?
# example-4.rb
status = "peace"
buggy_logger = status
print "Status: "
print buggy_logger << "\n" # <- This insertion is the bug.
def launch_nukes?(status)
unless status == 'peace'
return true
else
return false
end
end
print "Nukes Launched: #{launch_nukes?(status)}\n"
# => Status: peace
# => Nukes Launched: true
Yes, it is because strings are objects. Try
buggy_logger = status.dup
If you want a distinct object with the same initial value.
As for your question about alias I suspect you aren't correctly understanding how alias is used in ruby; it's used on methods, not objects, and isn't related to mutability.
Note also that the same semantics would have applied with any class; if status had been an array, a file, or anything else (provided it had mutable state suitable for use as a logger), you would have gotten analogous results.
One warning about dup though. If your object refers to other objects, the copy will also refer to the same objects. It's fine once you start thinking about it the right way, but tricky till then.

Resources