| space, → | next slide |
| ← | previous slide |
| d | debug mode |
| ## <ret> | go to slide # |
| c | table of contents (vi) |
| f | toggle footer |
| r | reload slides |
| z | toggle help (this) |
[main]
confdir = /home/rcrowley/work/extending-puppet
logdir = /var/log/puppet
rundir = /var/run/puppet
ssldir = $vardir/ssl
vardir = /var/lib/puppet
pluginsync = true
# Master
puppet master --confdir=$HOME/work/extending-puppet
# Agent
puppet agent --confdir=$HOME/work/extending-puppetPuppet::Type.newtype :foo do
# TODO Properties and parameters.
endnewproperty :foo do
desc "Foo."
newvalue :bar
newvalue :baz
end
newproperty :quux do
desc "Quux."
endprovider.foo called to read.provider.foo= called to write.ensure property.ensure propertyexists?createdestroyensure property newvalue(:present) do
if @resource.provider and
@resource.provider.respond_to?(:create)
@resource.provider.create
else
@resource.create
end
nil # return nil so the event is autogenerated
end
newvalue(:absent) do
if @resource.provider and
@resource.provider.respond_to?(:destroy)
@resource.provider.destroy
else
@resource.destroy
end
nil # return nil so the event is autogenerated
endnewparam :foo do
desc "Foo."
newvalue :bar
newvalue :baz
end
newparam :quux do
desc "Quux."
endpackage is a type.apt is a provider of packages.lib/puppet/type/github.rbrequire 'puppet/type'
Puppet::Type.newtype :github do
@doc = "Send a public key to GitHub."
newparam :path, :namevar => true do
desc "Private key pathname."
end
newparam :username do
desc "GitHub username."
end
newparam :token do
desc "GitHub API token."
end
ensurable do
defaultvalues
defaultto :present
end
endgithub { "/root/.ssh/github":
ensure => present,
token => "0123456789abcdef0123456789abcdef",
username => "rcrowley",
}lib/puppet/provider/github/https.rbrequire 'base64'
require 'json'
require 'net/http'
require 'net/https'
require 'openssl'
Puppet::Type.type(:github).provide :https do
@doc = "Send a public key to GitHub."
defaultfor :operatingsystem => :ubuntu
# TODO exists?, create, destroy
# TODO private methods
endlib/puppet/provider/github/https.rb def exists?
return false unless File.exists?("#{@resource[:path]}")
return false unless File.exists?("#{@resource[:path]}.pub")
!github_id.nil?
end
def create
system "ssh-keygen -q -f '#{@resource[:path]
}' -b 2048 -N '' -C ''"
POST("/api/v2/json/user/key/add",
:title => File.basename("#{@resource[:path]}"),
:key => File.read("#{@resource[:path]}.pub"))
end
def destroy
if id = github_id
POST("/api/v2/json/user/key/remove", :id => id)
end
File.unlink "#{@resource[:path]}"
File.unlink "#{@resource[:path]}.pub"
endlib/puppet/provider/github/https.rbprivate
def github_id
public_key = File.read("#{@resource[:path]}.pub").strip
JSON.parse(
GET("/api/v2/json/user/keys").body,
:symbolize_names => true
)[:public_keys].find { |gh| gh[:key] == public_key }[:id]
rescue NoMethodError
nil
endlib/puppet/provider/github/https.rbprivate
def GET(path)
request = Net::HTTP::Get.new(path)
authorize(request)
connection.request(request)
end
def POST(path, options={})
request = Net::HTTP::Post.new(path)
authorize(request)
request.set_form_data(options)
connection.request(request)
endlib/puppet/provider/github/https.rbprivate
def connection
return @connection if defined?(@connection)
@connection = Net::HTTP.new("github.com", 443)
@connection.use_ssl = true
@connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
@connection
end
def authorize(request)
request["Authorization"] = "Basic #{Base64.encode64(
"#{@resource[:username]}/token:#{@resource[:token]}"
).gsub("\n", "")}"
endpuppet-pippackage provider for Python’spip package management tool.puppet-pip on 2.6gem install puppet-pip
export \
RUBYLIB=$GEM_HOME/puppet-pip-1.0.0/liblib/puppet/provider/package/pip.rbrequire 'puppet/provider/package'
require 'xmlrpc/client'
Puppet::Type.type(:package).provide :pip,
:parent => ::Puppet::Provider::Package do
desc "Python packages via `pip`."
has_feature :installable, :uninstallable,
:upgradeable, :versionable
# TODO self.parse, self.instances,
# TODO query, latest, install, uninstall, update,
# TODO lazy_pip
endlib/puppet/provider/package/pip.rb def self.parse(line)
if line.chomp =~ /^([^=]+)==([^=]+)$/
{:ensure => $2, :name => $1, :provider => name}
else
nil
end
end
def self.instances
packages = []
pip_cmd = which('pip') or return []
execpipe "#{pip_cmd} freeze" do |process|
process.collect do |line|
next unless options = parse(line)
packages << new(options)
end
end
packages
endlib/puppet/provider/package/pip.rb def query
self.class.instances.each do |provider_pip|
if @resource[:name] == provider_pip.name
return provider_pip.properties
end
end
return nil
end
def latest
client = XMLRPC::Client.new2("http://pypi.python.org/pypi")
client.http_header_extra = {"Content-Type" => "text/xml"}
result = client.call("package_releases", @resource[:name])
result.first
endlib/puppet/provider/package/pip.rb def install
args = %w{install -q}
if @resource[:source]
args << "-e"
if String === @resource[:ensure]
args << "#{@resource[:source]}@#{@resource[:ensure]}" +
"#egg=#{@resource[:name]}"
else
args << "#{@resource[:source]}#egg=#{@resource[:name]}"
end
else
case @resource[:ensure]
when String
args << "#{@resource[:name]}==#{@resource[:ensure]}"
when :latest
args << "--upgrade" << @resource[:name]
else
args << @resource[:name]
end
end
lazy_pip *args
endlib/puppet/provider/package/pip.rb def uninstall
lazy_pip "uninstall", "-y", "-q", @resource[:name]
endlib/puppet/provider/package/pip.rb def update
install
endlib/puppet/provider/package/pip.rb private
def lazy_pip(*args)
pip *args
rescue NoMethodError => e
if pathname = which('pip')
self.class.commands :pip => pathname
pip *args
else
raise e
end
endpackage { "django":
ensure => "1.3",
provider => pip,
}file { "/root/.ssh/github":
content => github(
"my sweet public key",
"rcrowley",
"0123456789abcdef0123456789abcdef"
),
ensure => file,
group => "root",
mode => 0600,
owner => "root",
}lib/puppet/parser/ functions/github.rbrequire 'base64'
require 'net/https'
require 'sshkey'
Puppet::Parser::Functions.newfunction :github,
:type => :rvalue do |args|
# TODO Check GitHub for an existing public key.
key = SSHKey.generate
request = Net::HTTP::Post.new("/api/v2/json/user/key/add")
request["Authorization"] = "Basic #{Base64.encode64(
"#{args[1]}/token:#{args[2]}").gsub("\n", "")}"
request.set_form_data(
:title => args[0], :key => key.ssh_public_key)
connection = Net::HTTP.new("github.com", 443)
connection.use_ssl = true
connection.request(request)
key.rsa_private_key
end$ puppet --genconfig | grep node_terminus
# node_terminus = plain
$
lib/puppet/ indirector/node/plain.rbrequire 'puppet/node'
require 'puppet/indirector/plain'
class Puppet::Node::Plain < Puppet::Indirector::Plain
# ...
# Just return an empty node.
def find(request)
node = super
node.fact_merge
node
end
endlib/puppet/indirector/plain.rbrequire 'puppet/indirector/terminus'
# An empty terminus type, meant to just return empty objects.
class Puppet::Indirector::Plain < Puppet::Indirector::Terminus
# Just return nothing.
def find(request)
indirection.model.new(request.key)
end
end def find(request)
puts caller
indirection.model.new(request.key)
endlib/puppet/indirector/node/plain.rb:15:in `find' lib/puppet/indirector/indirection.rb:193:in `find' lib/puppet/indirector/catalog/compiler.rb:91:in `find_node' lib/puppet/indirector/catalog/compiler.rb:119:in `node_from_request' lib/puppet/indirector/catalog/compiler.rb:33:in `find' lib/puppet/indirector/indirection.rb:193:in `find' lib/puppet/network/http/handler.rb:106:in `do_find' lib/puppet/network/http/handler.rb:68:in `send' lib/puppet/network/http/handler.rb:68:in `process' lib/puppet/network/http/webrick/rest.rb:24:in `service' # ...
Puppet::Indirector:: Indirection#findlib/puppet/indirector/node/plain.rb:15:in `find' lib/puppet/indirector/indirection.rb:193:in `find' lib/puppet/indirector/catalog/compiler.rb:91:in `find_node' lib/puppet/indirector/catalog/compiler.rb:119:in `node_from_request' lib/puppet/indirector/catalog/compiler.rb:33:in `find' lib/puppet/indirector/indirection.rb:193:in `find' lib/puppet/network/http/handler.rb:106:in `do_find' lib/puppet/network/http/handler.rb:68:in `send' lib/puppet/network/http/handler.rb:68:in `process' lib/puppet/network/http/webrick/rest.rb:24:in `service' # ...
def find(request)
extract_facts_from_request(request)
node = node_from_request(request)
if catalog = compile(node)
return catalog
else
# This shouldn't actually happen; we should either return
# a config or raise an exception.
return nil
end
end def find(request)
indirection.model.new(request.key)
end
indirection.model returns Puppet::Node.Puppet::Node has @parameters and @classes.@parameters and @classes.lib/puppet/parser/compiler.rb def compile
# Set the client's parameters into the top scope.
set_node_parameters
create_settings_scope
evaluate_main
evaluate_ast_node
evaluate_node_classes
evaluate_generators
finish
fail_on_unevaluated
@catalog
endset_node_parameters and evaluate_node_classes handle@parameters and @classesevaluate_ast_node handlesnode definitions from Puppet code.@parameters and @classespuppet.confexternal_nodes = /usr/local/bin/classifier
node_terminus = exec
/usr/local/bin/classifier#!/bin/sh
cat <<EOF
---
classes: []
parameters:
foo: bar
EOFlib/puppet/ indirector/node/exec.rb def find(request)
output = super or return nil
# Translate the output to ruby.
result = translate(request.key, output)
create_node(request.key, result)
endlib/puppet/ indirector/exec.rb def find(request)
# Run the command.
unless output = query(request.key)
return nil
end
# Translate the output to ruby.
output
end def find(request)
puts caller
# Run the command.
unless output = query(request.key)
return nil
end
# Translate the output to ruby.
output
endlib/puppet/indirector/node/exec.rb:17:in `find' lib/puppet/indirector/indirection.rb:193:in `find' lib/puppet/indirector/catalog/compiler.rb:91:in `find_node' lib/puppet/indirector/catalog/compiler.rb:119:in `node_from_request' lib/puppet/indirector/catalog/compiler.rb:33:in `find' lib/puppet/indirector/indirection.rb:193:in `find' lib/puppet/network/http/handler.rb:106:in `do_find' lib/puppet/network/http/handler.rb:68:in `send' lib/puppet/network/http/handler.rb:68:in `process' lib/puppet/network/http/webrick/rest.rb:24:in `service' # ...
As expected, everything’s the same except where the indirector looked for the node.
Where’s that foo parameter injected by the external node classifier?
lib/puppet/indirector/node/exec.rb:17:in `find' lib/puppet/indirector/indirection.rb:193:in `find' lib/puppet/indirector/catalog/compiler.rb:91:in `find_node' lib/puppet/indirector/catalog/compiler.rb:119:in `node_from_request' lib/puppet/indirector/catalog/compiler.rb:33:in `find' lib/puppet/indirector/indirection.rb:193:in `find' lib/puppet/network/http/handler.rb:106:in `do_find' lib/puppet/network/http/handler.rb:68:in `send' lib/puppet/network/http/handler.rb:68:in `process' lib/puppet/network/http/webrick/rest.rb:24:in `service' # ...
lib/puppet/ indirector/catalog/compiler.rb def find_node(name)
begin
return nil unless node =
Puppet::Node.indirection.find(name)
rescue => detail
puts detail.backtrace if Puppet[:trace]
raise Puppet::Error,
"Failed when searching for node #{name}: #{detail}"
end
# Add any external data to the node.
add_node_data(node)
node
endnode? def find_node(name)
begin
return nil unless node =
Puppet::Node.indirection.find(name)
rescue => detail
puts detail.backtrace if Puppet[:trace]
raise Puppet::Error,
"Failed when searching for node #{name}: #{detail}"
end
# Add any external data to the node.
add_node_data(node)
puts node.inspect
node
endnode?
#<Puppet::Node:0xb70fa674 @name="devstructure.hsd1.ca.comcast.net.",
@classes={}, @expiration=Sat Jan 08 22:47:33 +0000 2011,
@parameters={"network_eth1"=>"33.33.33.0", "netmask"=>"255.255.255.0",
"kernel"=>"Linux", "processorcount"=>"1", "swapfree"=>"998.67 MB",
"physicalprocessorcount"=>"0", "uniqueid"=>"007f0101",
"lsbmajdistrelease"=>"10", "fqdn"=>"devstructure.hsd1.ca.comcast.net.",
"operatingsystemrelease"=>"10.10", "virtual"=>"physical",
"ipaddress"=>"10.0.2.15", "memorysize"=>"346.15 MB",
"is_virtual"=>"false", :_timestamp=>Sat Jan 08 22:17:32 +0000 2011,
"clientversion"=>"2.6.4", "hardwaremodel"=>"i686",
"kernelrelease"=>"2.6.35-22-generic-pae",
"rubysitedir"=>"/usr/local/lib/site_ruby/1.8", "ps"=>"ps -ef",
"macaddress_eth0"=>"08:00:27:8e:9e:65", "domain"=>"hsd1.ca.comcast.net.",
"netmask_eth0"=>"255.255.255.0", "serverip"=>"10.0.2.15",
"servername"=>"devstructure.hsd1.ca.comcast.net.", "timezone"=>"UTC",
"uptime_days"=>"9", "macaddress_eth1"=>"08:00:27:c3:78:5a", "id"=>"root",
"netmask_eth1"=>"255.255.255.0", "processor0"=>"Intel(R) Core(TM)2 Duo CPU
T8300 @ 2.40GHz", "hardwareisa"=>"unknown", "lsbdistrelease"=>"10.10",
"selinux"=>"false", "manufacturer"=>"innotek GmbH", "interfaces"=>"eth0,eth1",
"memoryfree"=>"225.10 MB", "uptime_hours"=>"216", "foo"=>"bar",
"lsbdistdescription"=>"Ubuntu 10.10", "lsbdistcodename"=>"maverick", "kernelversion"=>"2.6.35", "path"=>"/home/vagrant/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/var/lib/gems/1.8/bin", "hostname"=>"devstructure", "uptime"=>"9 days", "puppetversion"=>"2.6.4", "environment"=>"production", "serialnumber"=>"0", "macaddress"=>"08:00:27:8e:9e:65", "facterversion"=>"1.5.8", "ipaddress_eth0"=>"10.0.2.15", "kernelmajversion"=>"2.6", "swapsize"=>"1011.00 MB", "sshrsakey"=>"AAAAB3NzaC1yc2EAAAADAQABAAABAQDrJ0U+iSED6LNmnC0bHfpLKaDvWh0UXgQzdElhtY1xOS065TRMJIrb6LPaAxJH3iWXLP57rM8Vldurx+pOJWVz6ZmsGSz/EOugg6rC6K2cPAS8V5nRq6SUKksb/eBR0LbM0ygMoVEKi0ogBIkQuZNeVqdUtIcT5P7DBrasPkxzkBqxqAgYxzMeR82vYSLBEwhpeyrVUzpOHLh9mVxfCQoZRdEpnckGmMxYSOW7OBzf46CBhPrXAB5ZX3aZB+iDxNhMVvMYZFircDb/ZKZ4ph6qHFVWvBtd54N9yXf+XSLZPfox4zIRR8BRoUz8rx7ccZDZ6SWVdMCZ6jO3MuxEIeFJ", "operatingsystem"=>"Ubuntu", "serverversion"=>"2.6.4", "network_eth0"=>"10.0.2.0", "lsbdistid"=>"Ubuntu", "architecture"=>"i386", "uptime_seconds"=>"779012", "sshdsakey"=>"AAAAB3NzaC1kc3MAAACBAJNDwltq7JCbq7ql1a3g2IxLWwhJNLxk0jYj/oLLc7LvpiN1kcfuNoK2bZooUCnGkcyZs+6U3jbz2idISOncq+hfFjrMv0qIGKHXWANh1qLZuhRjBHwRlkD6ll5gw4ioTohmt3VfUbE4hJfK0z3wtQn/SLLoz4MSNnR09OIZOCgzAAAAFQDLW+bfrDl+WdsE4ixDCRr4vEW1lwAAAIBt+Bz62PhQLZSWpLaHCOOaFeoHwv9IPvQ/ors0zDOxDEoK5GHOY3BcCO8s4rHr17aQKmMP5ztNwzUBl+OlkSlQ7kAEMBRvehFbhK+SOcjvqIt6i2i9/3nn9ba0AZ8bQ+1T8Z+A/6lRmWtaeUqsZNRJitfzez7eWRfvd+X/GK1eaAAAAIAqyACyAElrdGd4YfwV/YPGjiUpIjvzqiBCogO2qvMj6/ohzdN7wBmIdIUmYQ1uYsQ6avd3GL5S2I/xJ1uGosIh6xlKa0Dk4SqOq1LGebdCpv1aUKIwecQlkxrQE6Da9Q4zVgHrtlglOW7A8uq8gfl/VQdwU1sow36scLSE8PCn/g==", "rubyversion"=>"1.8.7", "ipaddress_eth1"=>"33.33.33.33", "productname"=>"VirtualBox", "clientcert"=>"devstructure.hsd1.ca.comcast.net."}, @time=Sat Jan 08 22:17:33 +0000 2011, @environment="production">
foo is indeed bar but where’d the rest of the facts come from?lib/puppet/ indirector/node/exec.rb def create_node(name, result)
node = Puppet::Node.new(name)
set = false
[:parameters, :classes, :environment].each do |param|
if value = result[param]
node.send(param.to_s + "=", value)
set = true
end
end
node.fact_merge
node
endnode? def create_node(name, result)
node = Puppet::Node.new(name)
set = false
[:parameters, :classes, :environment].each do |param|
if value = result[param]
node.send(param.to_s + "=", value)
set = true
end
end
puts node.inspect
node.fact_merge
node
endnode?
#<Puppet::Node:0xb71eb704
@name="devstructure.hsd1.ca.comcast.net.",
@classes={}, @parameters={"foo"=>"bar"},
@time=Sat Jan 08 22:23:12 +0000 2011>
lib/puppet/node.pp def fact_merge
if facts = Puppet::Node::Facts.indirection.find(name)
merge(facts.values)
end
rescue => detail
error = Puppet::Error.new(
"Could not retrieve facts for #{name}: #{detail}")
error.set_backtrace(detail.backtrace)
raise error
end$ puppet --genconfig | grep facts_terminus
# facts_terminus = facter
$
lib/puppet/ indirector/facts/facter.rb def find(request)
result = Puppet::Node::Facts.new(request.key, Facter.to_hash)
result.add_local_facts
result.stringify
result.downcase_if_necessary
result
end$ ls -l lib/puppet/indirector/facts/
total 24
-rw-r--r-- 1 rcrowley rcrowley 1067 2010-12-22 01:10 active_record.rb
-rw-r--r-- 1 rcrowley rcrowley 674 2010-12-22 01:10 couch.rb
-rw-r--r-- 1 rcrowley rcrowley 2296 2010-12-22 01:10 facter.rb
-rw-r--r-- 1 rcrowley rcrowley 358 2010-12-22 01:10 memory.rb
-rw-r--r-- 1 rcrowley rcrowley 262 2010-12-22 01:10 rest.rb
-rw-r--r-- 1 rcrowley rcrowley 235 2010-12-22 01:10 yaml.rb
$
lib/puppet/indirector/node/plain.rb:15:in `find'
lib/puppet/indirector/indirection.rb:193:in `find'
lib/puppet/indirector/catalog/compiler.rb:91:in `find_node'
lib/puppet/indirector/catalog/compiler.rb:119:in `node_from_request'
lib/puppet/indirector/catalog/compiler.rb:33:in `find'
lib/puppet/indirector/indirection.rb:193:in `find'
# ...
lib/puppet/indirector/node/exec.rb:17:in `find'
lib/puppet/indirector/indirection.rb:193:in `find'
lib/puppet/indirector/catalog/compiler.rb:91:in `find_node'
lib/puppet/indirector/catalog/compiler.rb:119:in `node_from_request'
lib/puppet/indirector/catalog/compiler.rb:33:in `find'
lib/puppet/indirector/indirection.rb:193:in `find'
# ...
$ puppet --genconfig | grep catalog_terminus
# catalog_terminus = compiler
$
$ ls -l lib/puppet/indictor/catalog/
total 24
-rw-r--r-- 1 rcrowley rcrowley 1198 2010-12-22 01:11 active_record.rb
-rw-r--r-- 1 rcrowley rcrowley 4933 2011-01-08 22:18 compiler.rb
-rw-r--r-- 1 rcrowley rcrowley 140 2010-12-22 01:10 queue.rb
-rw-r--r-- 1 rcrowley rcrowley 189 2010-12-22 01:10 rest.rb
-rw-r--r-- 1 rcrowley rcrowley 534 2010-12-22 01:10 yaml.rb
$
lib/puppet/indirector/node/dns.rbrequire 'puppet/node'
require 'puppet/indirector/plain'
require 'resolv'
class Puppet::Node::Dns < Puppet::Indirector::Plain
def find(request)
node = super
begin
resolver = Resolv::DNS.new
resource = resolver.getresource(
request.key, Resolv::DNS::Resource::IN::TXT)
node.classes += resource.data.split
rescue Resolv::ResolvError
end
node.fact_merge
node
end
endfoo.example.com. IN TXT foo bar baz quux
# certname # # @classes #
lib/puppet/indirector/facts/hier.rbrequire 'puppet/indirector/facts/facter'
class HierValue < Hash
attr_accessor :top
def initialize(top=nil)
@top = top
end
def to_s
@top
end
end
class Puppet::Node::Facts::Hier < Puppet::Node::Facts::Facter
def destroy(facts)
end
def save(facts)
end
endlib/puppet/indirector/facts/hier.rbclass Puppet::Node::Facts::Hier < Puppet::Node::Facts::Facter
def find(request)
hier = {}
super.values.reject do |key, value|
Symbol === key
end.each do |key, value|
value = value.split(",") if value.index(",")
h = hier
if key.index("_") and keys = key.split("_")
while 1 < keys.length and key = keys.shift
h = HierValue === h[key] ?
h[key] : h[key] = HierValue.new(h[key])
end
key = keys.shift
end
if HierValue === h[key]
h[key].top = value
else
h[key] = value
end
end
Puppet::Node::Facts.new(request.key, hier)
end
end{"kernel"=>"Linux",
"netmask"=>{"eth0"=>"255.255.255.0", "eth1"=>"255.255.255.0"},
"ipaddress"=>{"eth0"=>"10.0.2.15", "eth1"=>"33.33.33.33"},
"kernelrelease"=>"2.6.35-22-generic-pae",
"ps"=>"ps -ef",
"network"=>{"eth0"=>"10.0.2.0", "eth1"=>"33.33.33.0"},
"interfaces"=>["eth0", "eth1"],
"kernelversion"=>"2.6.35",
"puppetversion"=>"2.6.4",
"hostname"=>"devstructure",
"uptime"=>{"seconds"=>"858930", "days"=>"9", "hours"=>"238"}}
facts["ipaddress"] # => "10.0.2.15"
facts["ipaddress"]["eth1"] # => "33.33.33.33"
lib/puppet/indirector/ catalog/caching_compiler.rbrequire 'puppet/indirector/catalog/compiler'
class Puppet::Resource::Catalog::CachingCompiler <
Puppet::Resource::Catalog::Compiler
@commit, @cache = `git rev-parse HEAD`.chomp, {}
def self.cache
commit = `git rev-parse HEAD`.chomp
@commit, @cache = commit, {} if @commit != commit
@cache
end
def find(request)
self.class.cache[request.key] ||= super
end
endHEAD moves.puppet-interfaces.require 'puppet/face'
Puppet::Face.define(:configurer, '0.0.1') do
action(:synchronize) do
when_invoked do |certname, options|
facts = Puppet::Face[:facts, '0.0.1'].find(certname)
catalog = Puppet::Face[:catalog, '0.0.1'].
download(certname, facts)
report = Puppet::Face[:catalog, '0.0.1'].apply(catalog)
report
end
end
end