Through the last months, I’ve written a lot about my work on the translator module for Puppet, which allows us to control mgmt with manifest code.
One gimmick we had imagined early was the ability to support arbitrary
manifests in mgmt
, by invoking puppet resource
for vertices that mgmt
itself cannot handle. This works now, in principle, and it nicely demonstrates
the amount of work that still needs doing.
Shaving the yak
Adding the generic pseudo-translator code to the module was relatively
straight-forward. You can still see it on GitHub.
It’s basically a copy of the exec
translator, but the cmd
value is constructed from the original resource’s type, title, and all
attributes.
The first thing I tried was a very simple cron
resource.
puppet mgmtgraph print --code \
'cron { "/bin/true": ensure => present, hour => 0, minute => 12 }'
The intention was to generate the following command within an exec
vertex
for mgmt
:
puppet resource cron /bin/true ensure=present hour=0 minute=12
However, this was the actual result:
puppet resource cron '/bin/true' \
provider='crontab' ensure='present' \
minute='["12"]' hour='["0"]' \
target='ffrank' loglevel='notice'
Now, the additional parameters provider
, target
and loglevel
make sense and are not
harmful. The values for hour
and minute
, on the other hand, look sensible from a
Ruby point of view, but are not suitable for the puppet resource
command at all.
Should the generator code perform more processing to get rid of the array braces? No, because there is a very good reason for them to appear here. So instead of looking for a workaround for this specific example, I got thinking about how the following resource should be translated:
cron {
"/bin/true":
ensure => present,
hour => [ 0, 12 ], # <- array!
minute => 12,
}
Quite a few of Puppet’s core resource types accept array values for some properties,
but puppet resource
will not actually have any of that. After all, it wasn’t built
for the kind of heavy lifting that we have in mind here.
This essentially means that we cannot use puppet resource
in this fashion after all.
An alternative is needed.
It would be possible to fall back to puppet apply
and create one-shot manifests
for each resource, instead of the puppet resource
invocation. However, I feel that
this would be even more painful than the original plan. Not only would Puppet need
to launch for every resource, it would also need to parse a manifest, build a catalog
and create a complete transaction context for every single vertex. So what’s the
middle ground here?
I ended up creating yet another Puppet module
that adds the puppet yamlresource
subcommand. It’s essentially a clone of
puppet resource
, but as the name implies, it accepts YAML input:
puppet mgmtgraph print --code \
'cron { "/bin/true": ensure => present, hour => 0, minute => 12 }'
# ...
resources:
exec:
- name: Cron:/bin/true
cmd: |-
puppet yamlresource cron '/bin/true' '{"provider": "crontab",
"ensure": "present", "minute": ["12"], "hour": ["0"],
"target": "ffrank", "loglevel": "notice"}'
# ...
The translator actually feeds it with JSON, but that is still valid YAML (as Adrien once pointed out to me), and pleasantly compact on the command line.
Now what
All right, we now have rules to accept not only all class and resource
relationships, there is even a way
to usher all the resources over to mgmt
, if only to have them handed right
back to Puppet in the end. Does that mean that we can go ahead and throw
any manifest at the translator and let mgmt
run along? Not quite.
After all, there are still important details that get lost in translation
(bonus question: pun or fair use). Simple example, mgmt
does not yet
manage file permissions. Consider a file
resource in Puppet that
specifies a value for the mode
property:
puppet mgmtgraph print --code 'file { "/etc/passwd": mode => "644" }'
# ...
file:
- name: "/etc/passwd"
path: "/etc/passwd"
content:
The result is a plain resource that does not manage anything at all.
There are about 25 other parameters that can be passed to file
resources
in Puppet that lack a counterpart in mgmt
. The other supported resource
types have similar issues, if not quite so many different parameters.
This is a major problem, because a large manifest can have an almost perfect translation, but a single property that gets ignored from an early resource can throw all dependent resources off. That’s why it’s important that this effect is at least very obvious to the user. To that end, the translator module now emits warnings about any parameter it gets from Puppet that has no translator handling.
The first tests of this feature went quite a bit noisier than expected. All resources would lead to output such as this:
Warning: cannot translate: Package[cowsay] { loglevel => :notice } (attribute is ignored)
A simple file
resource would produce 18 such warnings! These were spurious
for the most part. It is not really important what log level the user chose
for a given resource. The catalog will still work as intended.
False positives like these are a pet peeve of mine. Even in moderate numbers, they can render a monitoring or logging system practically unusable. Avoid if at all possible. In the case of resource translators, this called for a means to declare a white-list of attributes that can just pass without comment. It takes the following form in translator DSL:
ignore :validate_replacement, :provider, :sourceselect
The above example is from the file
translator. The ignored parameters are
of no interest because
- The
validate_replacement
parameter only has effect in tandem withvalidate_cmd
(which will cause a warning). - The
file
type has only one provider (plus one for Windows). - The
sourceselect
parameter is only effective on files that specify more than onesource
, which is not translatable in the first place.
Other parameters require a little more sophisticated handling. Consider
the purge
parameter of the file resource. It makes Puppet remove
unmanaged files from directory trees that are under management.
When left at its default of false
, it has no effect whatsoever.
That’s why it doesn’t make sense to emit a warning in this case.
However, if a file
uses a purge => true
parameter, the warning is
very important, because mgmt
cannot replicate Puppet’s behavior
appropriately. We can express this in translator DSL as follows:
ignore :purge do |value|
if value
Puppet.warning "#{@resource.ref} uses the purge attribute, which cannot be translated. Unmanaged content will be ignored."
end
end
All currently available translator instances have received such handlers.
Puppet’s parameters tend to have default values, so without the ignore
statements, translating the resources would become quite noisy.
Putting it to the test
To see how well the whole translation engine worked by now, I let it run against one of my favourite manifests. It goes like this:
puppet apply -e 'include puppetdb'
Doing this on a fresh system takes a while, but you end up with a functional PuppetDB stack, so that’s pretty neat.
I did not actually intend to run it (not really feeling the need to
get PuppetDB onto my hacking laptop right now), but it’s a cool
showcase for the translator. Sure enough, the mgmt
graph came out
just fine (I think). It’s comprised of 223 vertices and 465 edges
between them. 130 of the vertices are of type noop
; in other words,
we are looking at 65 classes and defines. The 93 bona fide resources
are mostly exec
(for unsupported resource types), but also around
40 translatables like file
and service
.
The number of translator warnings was quite alarming at first, more than 570. However, upon closer examination, it became clear that the newly introduced default translator did in fact complain about almost each and every attribute it encountered, despite the fact that it actually accounted for all of them. By fixing this, I more than halved the number of warnings to about 200.
These warnings from a real-live catalog are really valuable, because they allow us to estimate a few things:
- How many false positives are still raised to the user?
- What attributes are frequently reported and should be added to the translator?
- Which ones are not (yet) supported by
mgmt
?
All quite useful to guide future development.
What’s still missing
The translator itself is in a nice shape. In fact, I feel that this is a time
at which it makes sense for advanced Puppet users to go ahead and start playing
with the translator, mgmt
itself, and the glue code. Feedback and patches
most welcome. (The Forge is a
good place to get started with the translator.)
There is still the question about exported resources that I’d like to tackle eventually (but here, also, patches are welcome if you feel so inclined).
A big item is performance. The default translation needs to invoke Puppet at least once for each vertex that is visited. Launching the Ruby runtime and loading the necessary Puppet code is very expensive. I guess this will be something I will address before getting up to other work again.
Thanks for reading, and stay tuned for more updates.