Even more Steroids for your Hiera
The Puppet
create_resources
function allows you to easy create a lot of objects. It is often used together with hiera lookups. But filling your YAML files, with resources, makes them big and hard to manage. In the previous blog post, we introduced Connect and showed you how Connect makes it easy to manage your hiera configuration. In this blog post we are going to show you even more powerful Connect stuff.
Objects
The Puppet create_resources
function allows you to easy create a lot of objects. Connect makes this even easier. In Connect you can name these resources and reference them. Let’s take a look at an example:
myhost('oradb.example.com') { ip: '10.10.10.20', host_aliases: ['oradb'] }
myhost('wls1.example.com') { ip: '10.10.10.30', host_aliases: ['wls1'] }
myhost('wls2.example.com') { ip: '10.10.10.31', host_aliases: ['wls2'] }
In here we define three myhosts
objects. Every myhost
has a name, an ip
address and a set of host_aliases
. These are all properties the host
type recognizes. In this example, myhost
is the type of the object. You can use any name you like.
Now we assign these myhosts
to a variable profile::base::hosts::list
. This becomes a big Hash with all hosts in it.
profile::base::hosts::list = {
myhost('oradb.example.com').to_resource('host'),
myhost('wls1.example.com').to_resource('host'),
myhost('wls2.example.com').to_resource('host'),
}
We can use this in Puppet:
profile::base::hosts( $list)
{
create_resources('host', $list)
...
}
Selecting parts of an object
After you have defined an object you can reuse it’s values in all sorts of ways. Let’s say we need the IP
address of the database server for the definition of a connection:
weblogic::datasource::database_ip = myhost('oradb.example.com').ip
Using the .
and [x]
syntax we can select part of an object. This selecting also works on all other data types. Let’s see a contrived example:
server = all_domains[0].databases[1].ip
This would look into the array all_domains
and get the first element. Of the first domain, it would then select the second database and return its ip address. The combination of file inclusion, objects and selectors, gives you great power. You can, for instance create a file containing all objects in your multinode configuration, and includ this in all config files for a specific node. To connect the values of the objects to multiple resource definitions, you can use selectors.
More about selectors
Selectors are passed to the underlying ruby system. So you can use any method ruby supports on the specified data type. Connect allows you to write selectors like this:
array = [1,2,3,4,5]
string = array.join(',') # "1,2,3,4,5"
hostname = 'DMACHINE1' # Development machine 1
type = hostname[0,1] # type is 'O'
host = hostname[1..-1] # host is MACHINE1
You can also use selectors when interpolating strings.
presidents = ['Clinton', 'Bush', 'Obama']
last_president = "The last President of the USA was #{presidents.last}"
Because interpolation only works on Connect variables, using selectors is limited to interpolating Connect variables. Thus,
last_president = "The last President of the USA was %{presidents.last}"
Doesn’t work. Even if the array presidents
is defined in Puppet.
Special selectors
The standard Array, Hash and String functions in ruby are already quite powerful. But sometimes you need some extra help. Connect defines the following special helper selectors.
extract
The extract
helper allows you to extract an array of values from an array of objects. An example clarifies this:
all_nodes = [
host('node1.domein.com'){
ip : '10.0.0.1'
},
host('node2.domein.com'){
ip : '10.0.0.2'
}
host('node3.domein.com'){
ip : '10.0.0.3'
}
]
ip_adresses = all_nodes.extract('ip') # will be ['10.0.0.1','10.0.0.2','10.0.0.3']
to_resource
Sometimes your Connect object contains values, the original puppet type doesn’t support. To filter out all nonsupported attributes, you can use the to_resource
selector on an object. The selector must be called with the type as a parameter.
my_raw_host = host('db.domain.com') {
ip: '10.0.0.100',
just_a_random_attribute: 10,
} # my_raw_host cannot be use for create_resource call's because if the invalid attribute
my_host = my_raw_host.to_resource('host') # can be used as a parameter for create_resource
to_resource
unfortunately only works on native types. So for defined types we need something else. For defined types, we have slice
. The slice selector must be called with the attributes as values.
my_raw_host = host('db.domain.com') {
ip: '10.0.0.100',
just_a_random_attribute: 10,
} # Contains more then an ip. I need just a hash with the name and an ip.
my_host = my_raw_host.slice('ip') # can be used just get the ip into the hash.
#
# It returns
# {'db.domain.com' => { 'ip' => '10.0.0.100'}}
#
slice
can be used on objects and hashes.
Multiple Objects with iterator
Sometimes you want to define a set of objects. Like, for example, a set of DNS servers. Besides some specific attributes, these object definitions are very similar. It would be a waste if we had to define them all. Connect has a solution for this. It’s called iterators.
With an iterator, you can define similar objects and replace specific values. That ‘s a little bit abstract. Let show an example:
dnsserver('dnsserver%d.mydomain.org') iterate ip from 1 to 10 do
ip: '10.0.0.%{ip}',
aliases: ['dnsserver%{ip}'],
end
This little snippet of Connect code, defines 10 dns servers: dnsserver1.mydomain.org
with ip address 10.0.0.1 and alias dnsserver1
up to dnsserver10.mydomain.org
with ip address 10.0.0.11 and alias dnsserver10
. This concept is extremely convenient when defining a set of resources.
You can use integers like in the example above, but you can also use strings:
users('user%{postfix}') iterate postfix from 'aa' to 'bb' do
username: 'user%{postfix}',
home: '/users/user%{postfix}'
end
You can use references in the definition of the iterator.
start = 'aa'
finish = 'bb'
users('user%{postfix}') iterate postfix from start to finish do
username: 'user%{postfix}',
home: '/users/user%{postfix}'
end
And last but not least, you can use multiple iterators:
route('%{ipaddress}')
iterate ipaddress from '10.0.0.1' to '10.0.0.9'
iterate adapter from 'eth0' to 'eth2'
do
ip: %{ipaddress}',
device: '%{adapter}'
end
This will create the following objects:
route('10.0.0.1') {ip: '10.0.0.1', device:'/eth0'}
route('10.0.0.2') {ip: '10.0.0.2', device:'/eth1'}
route('10.0.0.3') {ip: '10.0.0.3', device:'/eth2'}
route('10.0.0.4') {ip: '10.0.0.4', device:'/eth0'}
route('10.0.0.5') {ip: '10.0.0.5', device:'/eth1'}
route('10.0.0.6') {ip: '10.0.0.6', device:'/eth2'}
route('10.0.0.7') {ip: '10.0.0.7', device:'/eth0'}
route('10.0.0.8') {ip: '10.0.0.8', device:'/eth1'}
route('10.0.0.9') {ip: '10.0.0.9', device:'/eth2'}
You can stack as many iterators as you want. The largest iterator is leading for the number of objects that are generated. All other iterators will cycle their values.
External data
Puppet and Hiera are not the only systems in the world. There are a lot more possible sources of data for Puppet runs. For example:
Connect allows you to import data from any other data source. The generic syntax is:
import from datasource(param1, param2) into scope:: {
value1 = 'lookup 1'
value2 = 'lookup 2'
}
Check the list of available data sources to see if the data source you need, exists. Check how to make your own data source if you need to access other data.
import from puppetdb into datacenter:: {
ntp_servers = 'Class[Ntp::Server]' # Fetches all NTP nodes from puppetdb
# into the array datacenter::ntp_servers
dns_servers = 'Class[Dns::Server]' # Fetches all DNS nodes from puppetdb
# into the array datacenter::dns_servers
}
Check the puppetdb api for a specification of the supported query language.
Like other blocks, you can also use begin
and end
. If you do not specify a scope, the variables will go ito the default scope:
import from puppetdb begin
ntp_servers = 'Class[Ntp::Server]'
dns_servers = 'Class[Dns::Server]'
end
Alternatively, using the YAML
importer:
import from yaml('/aaa/a.yaml') do
variable1 = 'key1'
variable2 = 'yaml::key2'
end
WARNING Not all data sources are available yet. This is only to show the syntax.
Conclusion
Using objects and imports, makes Connect an even better solution to big configurations. The object syntax allows you to very concise describe a lot of objects. When you combine it with the powerful selectors, your options are limitless.
The import facilities allow you to make a great mix between Puppet code, hiera lookups and other external data sources like Consul or PuppetDB. At this point in time, I know of no other tools that allow you to do this.
Like we said on the previous blog post: We, especially when building large infrastructures, prefer Connect over YAML
any day.
In our reference implementation, you can see how we use it together with our Puppet Oracle modules and with our Puppet WebLogic modules, to install and configure Oracle and WebLogic infrastructure.
If you would like to have more information about the Connect language, checkout the Connect Language, in a Nutshell.