The problem
In the process of modernizing our puppet infrastructure, I’ve been able to remove/delete many of the custom modules I had written many years ago and use community developed and supported modules from the puppet forge. Many of these modules accommodate a pattern of a single include in the manifest, and putting all the site-specific configuration data (if any) in hiera.
That said, some don’t allow this,
and resources must be explicitly configured. In the past, we’ve used
create_resources
in combination with hiera lookups.
freeradius.pp
profile:
create_resources('freeradius::module', hiera('freeradius::modules', {}))
radius.yaml
hiera data:
freeradius::modules:
ntlm_auth: {}
mschap:
instances:
- user
- ma
files: {}
ldap:
servers:
...
A solution
Puppet version >4 now have iteration functions that make
this pattern more flexible to write and easier to read and understand
than the create_resources
hack. Using ideas from a great blog
post, I would do something like:
lookup('freeradius::modules', {}).each | String $name, Hash $properties | {
freeradius::modules { $name: * => $properties }
}
Unfortunately, this particular module has a “generic” type
freeradius::module
as well as a handful of more-specific defined
types, such as freeradius::module::eap
. I’d like to have a single yaml
hash control which ones of these resources were created.
I had to ask on the puppet slack for a bit of help in the syntax of interpolating a resource type’s name, but quickly received a couple pointers to help me along my way to a solution:
include freeradius
lookup('freeradius::modules', {}).each | String $name, Hash $properties | {
# use generic module type if more-specific one does not exist
if defined(Resource["freeradius::module::${name}"]) {
Resource["freeradius::module::${name}"] { $name: * => $properties }
} else {
freeradius::module { $name: * => $properties }
}
}
A warning
Of course, I was admonished to be careful with defined()
as its
behavior is critically related to how the catalog is processed. Getting
your Puppet Ducks in a Row is a good explanation of these issues.
In this case, since the types are defined in the included freeradius
class module, I believe defined()
will work as expected.
Update
I’ve further developed this solution to be used for multiple resource types,
and for the mapping to be provided via hiera. I also fixed a bug with lookup()
defaults not behaving the way I expected.
class profile::freeradius (
$class_mapping = {}
) {
include ::freeradius
include profile::winbind
# instantiate classes from hiera data
$class_mapping.each | String $data, String $class | {
lookup({name => "freeradius::${data}", default_value => {} }).each | String $name, Hash $properties | {
# use more-specific class if it exists
if $name =~ /^[a-z_]+$/ and defined(Resource["freeradius::${class}::${name}"]) {
Resource["freeradius::${class}::${name}"] { $name: * => $properties }
} else {
Resource["freeradius::${class}"] { $name: * => $properties }
}
}
}
}
The above class is written once, and administrators need only edit the hiera data, even if they want to use a different, previously-unused resource.
profile::freeradius::class_mapping:
listens: listen
clients: client
modules: module
realms: realm
certs: cert
sites: site
dictionaries: dictionary
home_servers: home_server
home_server_pools: home_server_pool
[...]
freeradius::sites:
default:
listen:
- v4-auth:
ipaddr: '*'
type: auth
- v6-auth:
ipv6addr: '::'
type: auth
- v4-acct:
ipaddr: '*'
type: acct
- v6-acct:
ipv6addr: '::'
type: acct
authorize:
- filter_username
- preprocess
- suffix
[...]