magic
A library cookbook meant to make writing cookbooks a bit easier. It exposes some helpful functions, which you can use directly in recipes and resources. This cookbook has no attributes, no recipes, and no dependencies. All Linux (and probably other Unix-like) platforms are supported.
Helpers
Declared under the Helpers
module in libraries/helpers.rb
.
file_cache_path
is a simpler way to use Chef::Config[:file_cache_path]
:
file_cache_path # => '/var/cache/chef'
file_cache_path 'cached.file' # => '/var/cache/chef/cached.file'
file_cache_path 'my', 'other.file' # => '/var/cache/chef/my/other.file'
resource?
can be used to ask whether or not a resource exists:
resource? 'this_thing[doesnt_exist]' # => false
resource? 'thing_thing[totally_exists]' # => true
shell_opts
can translate a Hash into a shell-friendly string of options:
shell_opts({ debug: true, simon: 'says' }) # => '--debug --simon says'
shell_opts({ debug: false, level: 2 }) # => '--level 2'
upstart_opts
works like shell_opts
for Upstart:
upstart_opts({ debug: true, simon: 'says' }) # => "--debug --simon 'says'"
upstart_opts({ debug: false, level: 2 }) # => "--level '2'"
Search
search_nodes
is a light abstraction over Chef search, which allows you to
make queries using a plain old Ruby Hash:
search_nodes chef_environment: 'example', role: 'test'
# => search(:node, 'chef_environment:"example" AND role:"test"')
search_nodes chef_environment: 'example', role: 'test', join_with: 'OR'
# => search(:node, 'chef_environment:"example" OR role:"test"')
Deep Merge
This library extends the Ruby Hash
class with deep merge capabilities from
Chef's own DeepMerge
mixin:
{ a: 1, b: { c: 2 } }.deep_merge b: { d: 4 }, c: 3
# => { a: 1, b: { c: 2, d: 4 }, c: 3 }
Configuration
Declared under the Configuration
module in libraries/configuration.rb
.
INI
Converts a Hash to INI-style configuration:
ini_config({
'this' => {
'is' => 'an',
'example' => 123
}
})
Generates:
[this]
is=an
example=123
YAML
Converts a Hash to YAML:
yaml_config({
'this' => {
'is' => %w[ just a test ]
}
})
Generates:
---
this:
is:
- just
- a
- test
JSON
Converts a Hash to JSON and returns a pretty representation:
json_config({
'this' => {
'is' => [ 'just', :a, 'FREEFORM' ],
10 => nil,
{} => [],
'deal' => /really/
}
})
Generates:
{
"this": {
"is": [ "just", "a", "FREEFORM" ],
"10": null,
"{}": [],
"deal": "(?-mix:really)"
}
}
Java
Converts a Hash to Java-style configuration:
java_config({
'this' => {
'is' => [ 'just', :a, 'FREEFORM' ],
10 => nil
}
})
Generates:
this {
is = [ "just", a, "FREEFORM" ]
10 = nil
}
Java Properties
Converts a Hash to Java properties:
properties_config({
'foo' => 'bar'
})
Generates:
foo=bar
Logstash
N.B. The name of this generator changed in v1.2
from logstash_config
to logstash_typed_config
to avoid a namespace collision (per Issue #5).
Converts a Hash to Logstash-style configuration:
logstash_typed_config({
'input' => {
'test' => {
'file' => {
'path' => '/var/log/test.log'
}
}
},
'filter' => {
'test' => {
'seq' => {}
}
},
'output' => {
'test' => {
'stdout' => {
'codec' => 'rubydebug'
}
}
}
})
Generates:
input {
file {
path => "/var/log/test.log"
type => "test"
}
}
filter {
if [type] == "test" {
seq {
}
}
}
output {
if [type] == "test" {
stdout {
codec => "rubydebug"
}
}
}
Exports
Converts a Hash to shell exports-style configuration:
exports_config({
'this' => nil,
'is' => 10,
'a' => :nother,
'test' => 1234
})
Generates:
export this=''
export is=10
export a=nother
export test=1234
Materialization
Materialization lets you pull a neat trick by phrasing string attributes in terms of other string attributes, so you can have attributes files that look like this:
# attributes/default.rb
default['example']['version'] = '1.2.3'
default['example']['url'] = 'http://example.com/%{version}.tar.gz'
At runtime, materialization will replace %{version}
with the node.example.version
attibute.
If you're familiar with string interpolation tricks in Ruby (aren't we all?), this should feel familiar:
$ irb
irb> puts '%{one} %{two} %{three}' % { one: 1, two: 2, three: 3 }
1 2 3
=> nil
The only innovation with materialization is that the interpolation is applied recursively:
# libraries/materialization.rb
module Materialization
def sym k ; k.respond_to?(:to_sym) ? k.to_sym : k end
def materialize obj, parent=nil
o = materialize_raw obj, parent
return ::Chef::Mash.new(o) if o.is_a? Hash
return o
end
def materialize_raw obj, parent=nil
obj = obj.to_hash if obj.respond_to? :to_hash
if obj.is_a? Hash
obj = obj.inject({}) { |memo, (k,v)| memo[sym(k)] = v ; memo }
obj.inject({}) { |memo, (k,v)| memo[sym(k)] = materialize_raw(v, obj) ; memo }
elsif obj.is_a? Array
obj.map { |o| materialize_raw(o, parent) }
elsif obj.is_a? String
obj % parent rescue obj
else
obj
end
end
end
That's an ugly chunk of code, but the results are intuitive enough:
materialize nil # => nil
materialize 'hello' # => 'hello'
materialize 'hello %{world}', world: 'bob' # => 'hello bob'
materialize %w[ %{one} %{two} %{three} ], one: 1, two: 2, three: 3
# => [ '1', '2', '3' ]
materialize one: [ { one: '%{two}', two: 2 } ], two: '%{three}', three: 4
# => { one: [ { one: '2', two: 2 } ], two: '4', three: 4 }
Now in a recipe, you'd materialize the relevant attribute namespace:
# recipes/default.rb
example = materialize node['example']
Reification
Consider this code from a hypothetical icinga2
cookbook:
# attributes/default.rb
default['icinga2']['repo']['name'] = 'icinga2'
default['icinga2']['repo']['uri'] = 'ppa:formorer/icinga'
default['icinga2']['repo']['distribution'] = node['lsb']['codename']
# recipes/default.rb
repo_spec = node['icinga2']['repo'].to_hash
repo_name = repo_spec.delete 'name'
apt_repository repo_name do
repo_spec.each do |k, v|
send k.to_sym, v
end
end
We're just adding an apt repository, configured according to our attributes.
In a broad sense, we've got a Chef resource, and we're converting attributes in a namespace to method calls on the resource. The name
attribute is required and passed along as the resource name.
We'll call this pattern reification:
# libraries/reification.rb
# Simplified, see "Notifications and Actions" below
module Reification
def reify resource, spec
spec = ::Mash.new(spec.to_hash)
name = spec.delete 'name'
send resource.to_sym, name do
spec.each do |k, v|
send k.to_sym, v
end
end
end
end
Now the cookbook is much tighter:
# attributes/default.rb
default['icinga2']['repo']['name'] = 'icinga2'
default['icinga2']['repo']['uri'] = 'ppa:formorer/icinga'
default['icinga2']['repo']['distribution'] = node['lsb']['codename']
# recipes/default.rb
reify :apt_repository, node['icinga2']['repo']
We might say that reify
instantiates a resource according to the provided attributes or spec.
Notifications and Actions
The implementation of reify
above is a bit simplified. The actual implementation
also supports resource notifications and actions. The function signature really
looks like reify resource, spec, notifications=[], actions=[]
. Use it like so:
reify :service, node['icinga2']['service'], [
[ :restart, 'icinga-server' ], # Delayed by default
[ :restart, 'icinga-sidecar', :immediately ]
], [ :enable, :start ]