cookbook 'magic', '= 1.2.0'
magic
(22) Versions
1.2.0
-
Follow1
Cookbook helpers and other magical things
cookbook 'magic', '= 1.2.0', :supermarket
knife supermarket install magic
knife supermarket download magic
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 ]
Dependent cookbooks
This cookbook has no specified dependencies.