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.
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]' # => trueshell_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_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"')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 }Declared under the Configuration module in libraries/configuration.rb.
Converts a Hash to INI-style configuration:
ini_config({
'this' => {
'is' => 'an',
'example' => 123
}
})Generates:
[this]
is=an
example=123
Converts a Hash to YAML:
yaml_config({
'this' => {
'is' => %w[ just a test ]
}
})Generates:
---
this:
is:
- just
- a
- test
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)"
}
}Converts a Hash to Java-style configuration:
java_config({
'this' => {
'is' => [ 'just', :a, 'FREEFORM' ],
10 => nil
}
})Generates:
this {
is = [ "just", a, "FREEFORM" ]
10 = nil
}
Converts a Hash to Java properties:
properties_config({
'foo' => 'bar'
})Generates:
foo=bar
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"
}
}
}
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=1234Materialization 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
endThat'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']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
endWe'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
endNow 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.
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 ]