
Creating virtual resources
The next technique that we are going to discuss helps you solve conflicts in your manifests and build some elegant solutions in special situations.
Remember the uniqueness constraint that was introduced in Chapter 1, Writing Your First Manifests; any resource must be declared at most once in a manifest. There cannot be two classes or defined type instances that declare the same file
, package
, or any other type of resource. Each resource must have a unique type/name combination. This applies to instances of defined types as well as native resources.
This can pose issues when multiple modules need a common resource, such as an installed package, or perhaps even independent settings in the same configuration file. A component class for such resources, as introduced in Chapter 4, Modularizing Manifests with Classes and Defined Types, will resolve basic conflicts of this kind. It can be included an arbitrary number of times in the same manifest.
This can be impractical when the number of shared resources is fairly large. Imagine that you find yourself in a situation where a large number of different Puppet nodes require software from a significant set of yum
repositories. Puppet will happily manage the repository configuration on the agents through its yumrepo
type. However, you don't actually want all these repositories configured on every last machine—they do incur maintenance overhead after all. It would, instead, be desirable for each node to automatically receive the configuration for all repositories it requires for its packages but not more.
When solving this using component classes, you would wrap each repository in a distinct class. The class names should closely resemble (and most likely contain) the name of the respective repositories:
class yumrepos::team_ninja_stable { yumrepo { 'team_ninja_stable': ensure => present, ... } }
Package resources that rely on one or more such repositories will need to be accompanied by appropriate include
statements:
include yumrepos::team_ninja_stable include yumrepos::team_wizard_experimental package { 'doombunnies': ensure => installed, require => Class[ 'yumrepos::team_ninja_stable', 'yumrepos::team_wizard_experimental' ],
This is possible, but it is less than ideal. Puppet does offer an alternative way to avoid duplicate resource declarations in the form of virtual resources. It allows you to add a resource declaration to your manifest without adding the resource to the actual catalog. The virtual resource must be realized or collected for this purpose. Just like class inclusion, this realization of virtual resources can happen arbitrarily in the same manifest.
Our previous example can, therefore, use a simpler structure with just one class to declare all the yum
repositories as virtual resources:
class yumrepos::all { @yumrepo { 'tem_ninja_stable': ensure => present, } @yumrepo { 'team_wizard_experimantel': ensure => present, } }
The @
prefix marks the yumrepo
resources as virtual. This class can be safely included by all nodes. It will not affect the catalog until the resources are realized:
realize(Yumrepo['team_ninja_stable']) realize(Yumrepo['team_wizard_experimental']) package { 'doombunnies': ensure => installed, require => [ Yumrepo['team_ninja_stable'], Yumrepo['team_wizard_experimental'] ], }
The realize
function converts the referenced virtual resources to real ones, which get added to the catalog. Granted, this is not much better than the previous code that relied on the component classes. The virtual resources do make the intent clearer, at least. Realizing them is less ambiguous than some include
statements—a class can contain many resources and even more include
statements.
To really improve the situation, you can introduce a defined type that can realize the repositories directly:
define curated::package($ensure,$repositories=[]) { if $repositories != [] and $ensure != 'absent' { realize(Yumrepo[$repositories]) } package { $name: ensure => $ensure } }
You can then just pass the names of yumrepo
resources for realization:
curated::package { 'doombunnies': ensure =>'installed', repositories => [ 'team_ninja_stable', 'team_wizard_experimental', ], }
Better yet, you can most likely prepare a hash, globally or in the scope of curated::package
, to create the most common resolutions:
$default_repos = { 'doombunnies' => [ 'team_ninja_stable', 'team_wizard_experimental', ], ... }
The curated::package
can then look packages up if no explicit repository names are passed. Use the has_key
function from the puppetlabs-stdlib
module to make the lookup safer:
if $repositories != [] { realize(Yumrepo[$repositories]) } elsif has_key($default_repos,$name) { $repolist = $default_repos[$name] realize(Yumrepo[$repolist]) }
Realizing resources more flexibly using collectors
Instead of invoking the realize
function, you can also rely on a different syntactic construct, which is the collector
:
Yumrepo<| title == 'team_ninja_stable' |>
This is more flexible than the function call at the cost of a slight performance penalty. It can be used as a reference to the realized resource(s) in certain contexts. For example, you can add ordering constraints with the chaining operator:
Yumrepo<| title == 'team_ninja_stable' |> -> Class['...']
It is even possible to change values of resource attributes during collection. There is a whole section dedicated to such overrides later in this chapter.
As the collector is based on an expression, you can conveniently realize a whole range of resources. This can be quite dynamic—sometimes, you will create virtual resources that are already being realized by a rather indiscriminate collector. Let's look at a common example:
User<| |>
With no expression, the collection encompasses all virtual resources of the given type. This allows you to collect them all, without worrying about their concrete titles or attributes. This might seem redundant, because then it makes no sense to declare the resources as virtual in the first place. However, keep in mind that the collector might appear in some select manifests only, while the virtual resources can be safely added to all your nodes.
To be a little more selective, it can be useful to group virtual resources based on their tags. We haven't discussed tags yet. Each resource is tagged with several identifiers. Each tag is just a simple string. You can tag a resource manually by defining the tag
metaparameter:
file { '/etc/sysctl.conf': tag => 'security' }
The named tag is then added to the resource. Puppet implicitly tags all resources with the name of the declaring class, the containing module, and a range of other useful meta information. For example, if your user module divides the user
resources in classes such as administrators
, developers
, qa
, and other roles, you can make certain nodes or classes select all users of a given role with a collection based on the class name tag:
User<| tag == 'developers' |>
Note that the tags actually form an array. The ==
comparison will look for the presence of the developers
element in the tag
array in this context. Have a look at another example to make this more clear:
@user { 'felix': ensure => present, groups => [ 'power', 'sys' ], } User<| groups == 'sys' |>
This way, you can collect all users who are members of the sys
group.
If you prefer function calls over the more cryptic collector syntax, you can keep using the realize
function alongside collectors. This works without issues. Remember that each resource can be realized multiple times, even in both ways, simultaneously.
If you are wondering, the manifest for a given agent can only realize virtual resources that are declared inside this same agent's manifest. Virtual resources do not leak into other manifests. Consequently, there can be no deliberate transfer of resources from one manifest to another, either. However, there is yet another concept that allows such an exchange; this is described in the next section.