diff --git a/lib/puppet/application/resource.rb b/lib/puppet/application/resource.rb
index f914a1b53..0046fc1d8 100644
--- a/lib/puppet/application/resource.rb
+++ b/lib/puppet/application/resource.rb
@@ -1,152 +1,124 @@
require 'puppet'
require 'puppet/application'
require 'facter'
Puppet::Application.new(:resource) do
should_not_parse_config
attr_accessor :host, :extra_params
preinit do
@extra_params = []
@host = nil
Facter.loadfacts
end
option("--debug","-d")
option("--verbose","-v")
option("--edit","-e")
option("--host HOST","-H") do |arg|
@host = arg
end
option("--types", "-t") do |arg|
types = []
Puppet::Type.loadall
Puppet::Type.eachtype do |t|
next if t.name == :component
types << t.name.to_s
end
puts types.sort
exit
end
option("--param PARAM", "-p") do |arg|
@extra_params << arg.to_sym
end
command(:main) do
type = ARGV.shift or raise "You must specify the type to display"
typeobj = Puppet::Type.type(type) or raise "Could not find type #{type}"
name = ARGV.shift
params = {}
ARGV.each do |setting|
if setting =~ /^(\w+)=(.+)$/
params[$1] = $2
else
raise "Invalid parameter setting %s" % setting
end
end
if options[:edit] and @host
raise "You cannot edit a remote host"
end
properties = typeobj.properties.collect { |s| s.name }
format = proc {|trans|
trans.dup.collect do |param, value|
if value.nil? or value.to_s.empty?
trans.delete(param)
elsif value.to_s == "absent" and param.to_s != "ensure"
trans.delete(param)
end
unless properties.include?(param) or @extra_params.include?(param)
trans.delete(param)
end
end
trans.to_manifest
}
- text = if @host
- client = Puppet::Network::Client.resource.new(:Server => @host, :Port => Puppet[:puppetport])
- unless client.read_cert
- raise "client.read_cert failed"
- end
- begin
- # They asked for a single resource.
- if name
- transbucket = [client.describe(type, name)]
- else
- # Else, list the whole thing out.
- transbucket = client.instances(type)
- end
- rescue Puppet::Network::XMLRPCClientError => exc
- raise "client.list(#{type}) failed: #{exc.message}"
- end
- transbucket.sort { |a,b| a.name <=> b.name }.collect(&format)
+ if @host
+ Puppet::Resource.indirection.terminus_class = :rest
+ port = Puppet[:puppetport]
+ key = ["https://#{host}:#{port}", "production", "resources", type, name].join('/')
else
- if name
- obj = typeobj.instances.find { |o| o.name == name } || typeobj.new(:name => name, :check => properties)
- vals = obj.retrieve
-
- unless params.empty?
- params.each do |param, value|
- obj[param] = value
- end
- catalog = Puppet::Resource::Catalog.new
- catalog.add_resource obj
- begin
- catalog.apply
- rescue => detail
- if Puppet[:trace]
- puts detail.backtrace
- end
- end
+ key = [type, name].join('/')
+ end
- end
- [format.call(obj.to_trans(true))]
+ text = if name
+ if params.empty?
+ [ Puppet::Resource.find( key ) ]
else
- typeobj.instances.collect do |obj|
- next if ARGV.length > 0 and ! ARGV.include? obj.name
- trans = obj.to_trans(true)
- format.call(trans)
- end
+ request = Puppet::Indirector::Request.new(:resource, :save, key) # Yuck.
+ [ Puppet::Resource.new( type, name, params ).save( request ) ]
end
- end.compact.join("\n")
+ else
+ Puppet::Resource.search( key, {} )
+ end.map(&format).join("\n")
if options[:edit]
file = "/tmp/x2puppet-#{Process.pid}.pp"
begin
File.open(file, "w") do |f|
f.puts text
end
ENV["EDITOR"] ||= "vi"
system(ENV["EDITOR"], file)
system("puppet -v " + file)
ensure
#if FileTest.exists? file
# File.unlink(file)
#end
end
else
puts text
end
end
setup do
Puppet::Util::Log.newdestination(:console)
# Now parse the config
Puppet.parse_config
if options[:debug]
Puppet::Util::Log.level = :debug
elsif options[:verbose]
Puppet::Util::Log.level = :info
end
end
end
diff --git a/lib/puppet/indirector/resource/ral.rb b/lib/puppet/indirector/resource/ral.rb
new file mode 100644
index 000000000..f2c3f847d
--- /dev/null
+++ b/lib/puppet/indirector/resource/ral.rb
@@ -0,0 +1,48 @@
+class Puppet::Resource::Ral < Puppet::Indirector::Code
+ def find( request )
+ # find by name
+ res = type(request).instances.find { |o| o.name == resource_name(request) }
+ res ||= type(request).new(:name => resource_name(request), :check => type(request).properties.collect { |s| s.name })
+
+ return res.to_resource
+ end
+
+ def search( request )
+ conditions = request.options.dup
+ conditions[:name] = resource_name(request) if resource_name(request)
+
+ type(request).instances.map do |res|
+ res.to_resource
+ end.find_all do |res|
+ conditions.all? {|property, value| res.to_resource[property].to_s == value.to_s}
+ end.sort do |a,b|
+ a.title <=> b.title
+ end
+ end
+
+ def save( request )
+ # In RAL-land, to "save" means to actually try to change machine state
+ res = request.instance
+ ral_res = res.to_ral
+
+ catalog = Puppet::Resource::Catalog.new
+ catalog.add_resource ral_res
+ catalog.apply
+
+ return ral_res.to_resource
+ end
+
+ private
+
+ def type_name( request )
+ request.key.split('/')[0]
+ end
+
+ def resource_name( request )
+ request.key.split('/')[1]
+ end
+
+ def type( request )
+ Puppet::Type.type(type_name(request)) or raise Puppet::Error "Could not find type #{type}"
+ end
+end
diff --git a/lib/puppet/indirector/resource/rest.rb b/lib/puppet/indirector/resource/rest.rb
new file mode 100644
index 000000000..7848ae65e
--- /dev/null
+++ b/lib/puppet/indirector/resource/rest.rb
@@ -0,0 +1,5 @@
+require 'puppet/indirector/status'
+require 'puppet/indirector/rest'
+
+class Puppet::Resource::Rest < Puppet::Indirector::REST
+end
diff --git a/lib/puppet/indirector/rest.rb b/lib/puppet/indirector/rest.rb
index a89e98606..4fd385919 100644
--- a/lib/puppet/indirector/rest.rb
+++ b/lib/puppet/indirector/rest.rb
@@ -1,96 +1,96 @@
require 'net/http'
require 'uri'
require 'puppet/network/http_pool'
require 'puppet/network/http/api/v1'
# Access objects via REST
class Puppet::Indirector::REST < Puppet::Indirector::Terminus
include Puppet::Network::HTTP::API::V1
class << self
attr_reader :server_setting, :port_setting
end
# Specify the setting that we should use to get the server name.
def self.use_server_setting(setting)
@server_setting = setting
end
def self.server
return Puppet.settings[server_setting || :server]
end
# Specify the setting that we should use to get the port.
def self.use_port_setting(setting)
@port_setting = setting
end
def self.port
return Puppet.settings[port_setting || :masterport].to_i
end
# Figure out the content type, turn that into a format, and use the format
# to extract the body of the response.
def deserialize(response, multiple = false)
case response.code
when "404"
return nil
when /^2/
unless response['content-type']
raise "No content type in http response; cannot parse"
end
content_type = response['content-type'].gsub(/\s*;.*$/,'') # strip any appended charset
# Convert the response to a deserialized object.
if multiple
model.convert_from_multiple(content_type, response.body)
else
model.convert_from(content_type, response.body)
end
else
# Raise the http error if we didn't get a 'success' of some kind.
message = "Error %s on SERVER: %s" % [response.code, (response.body||'').empty? ? response.message : response.body]
raise Net::HTTPError.new(message, response)
end
end
# Provide appropriate headers.
def headers
{"Accept" => model.supported_formats.join(", ")}
end
def network(request)
Puppet::Network::HttpPool.http_instance(request.server || self.class.server, request.port || self.class.port)
end
def find(request)
return nil unless result = deserialize(network(request).get(indirection2uri(request), headers))
- result.name = request.key
+ result.name = request.key if result.respond_to?(:name=)
result
end
def search(request)
unless result = deserialize(network(request).get(indirection2uri(request), headers), true)
return []
end
return result
end
def destroy(request)
raise ArgumentError, "DELETE does not accept options" unless request.options.empty?
deserialize network(request).delete(indirection2uri(request), headers)
end
def save(request)
raise ArgumentError, "PUT does not accept options" unless request.options.empty?
deserialize network(request).put(indirection2uri(request), request.instance.render, headers.merge({ "Content-Type" => request.instance.mime }))
end
private
def environment
Puppet::Node::Environment.new
end
end
diff --git a/lib/puppet/network/rest_authconfig.rb b/lib/puppet/network/rest_authconfig.rb
index 01ed412cd..7c0ef9cf3 100644
--- a/lib/puppet/network/rest_authconfig.rb
+++ b/lib/puppet/network/rest_authconfig.rb
@@ -1,90 +1,91 @@
require 'puppet/network/authconfig'
module Puppet
class Network::RestAuthConfig < Network::AuthConfig
attr_accessor :rights
DEFAULT_ACL = [
{ :acl => "~ ^\/catalog\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true },
# this one will allow all file access, and thus delegate
# to fileserver.conf
{ :acl => "/file" },
{ :acl => "/certificate_revocation_list/ca", :method => :find, :authenticated => true },
{ :acl => "/report", :method => :save, :authenticated => true },
{ :acl => "/certificate/ca", :method => :find, :authenticated => false },
{ :acl => "/certificate/", :method => :find, :authenticated => false },
{ :acl => "/certificate_request", :method => [:find, :save], :authenticated => false },
{ :acl => "/status", :method => [:find], :authenticated => true },
+ { :acl => "/resource", :method => [:find, :save, :search], :authenticated => true },
]
def self.main
add_acl = @main.nil?
super
@main.insert_default_acl if add_acl and !@main.exists?
@main
end
# check wether this request is allowed in our ACL
# raise an Puppet::Network::AuthorizedError if the request
# is denied.
def allowed?(request)
read()
# we're splitting the request in part because
# fail_on_deny could as well be called in the XMLRPC context
# with a ClientRequest.
@rights.fail_on_deny(build_uri(request),
:node => request.node,
:ip => request.ip,
:method => request.method,
:environment => request.environment,
:authenticated => request.authenticated)
end
def initialize(file = nil, parsenow = true)
super(file || Puppet[:rest_authconfig], parsenow)
# if we didn't read a file (ie it doesn't exist)
# make sure we can create some default rights
@rights ||= Puppet::Network::Rights.new
end
def parse()
super()
insert_default_acl
end
# force regular ACLs to be present
def insert_default_acl
DEFAULT_ACL.each do |acl|
unless rights[acl[:acl]]
Puppet.info "Inserting default '#{acl[:acl]}'(%s) acl because %s" % [acl[:authenticated] ? "auth" : "non-auth" , ( !exists? ? "#{Puppet[:rest_authconfig]} doesn't exist" : "none where found in '#{@file}'")]
mk_acl(acl)
end
end
# queue an empty (ie deny all) right for every other path
# actually this is not strictly necessary as the rights system
# denies not explicitely allowed paths
unless rights["/"]
rights.newright("/")
rights.restrict_authenticated("/", :any)
end
end
def mk_acl(acl)
@rights.newright(acl[:acl])
@rights.allow(acl[:acl], acl[:allow] || "*")
if method = acl[:method]
method = [method] unless method.is_a?(Array)
method.each { |m| @rights.restrict_method(acl[:acl], m) }
end
@rights.restrict_authenticated(acl[:acl], acl[:authenticated]) unless acl[:authenticated].nil?
end
def build_uri(request)
"/#{request.indirection_name}/#{request.key}"
end
end
end
diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb
index e733f3ee0..bdd11fcc5 100644
--- a/lib/puppet/resource.rb
+++ b/lib/puppet/resource.rb
@@ -1,264 +1,279 @@
require 'puppet'
require 'puppet/util/tagging'
-require 'puppet/resource/reference'
+#require 'puppet/resource/reference'
require 'puppet/util/pson'
# The simplest resource class. Eventually it will function as the
# base class for all resource-like behaviour.
class Puppet::Resource
include Puppet::Util::Tagging
extend Puppet::Util::Pson
include Enumerable
attr_accessor :file, :line, :catalog, :exported, :virtual
attr_writer :type, :title
+ require 'puppet/indirector'
+ extend Puppet::Indirector
+ indirects :resource, :terminus_class => :ral
+
ATTRIBUTES = [:file, :line, :exported]
def self.from_pson(pson)
raise ArgumentError, "No resource type provided in pson data" unless type = pson['type']
raise ArgumentError, "No resource title provided in pson data" unless title = pson['title']
resource = new(type, title)
if params = pson['parameters']
params.each { |param, value| resource[param] = value }
end
if tags = pson['tags']
tags.each { |tag| resource.tag(tag) }
end
ATTRIBUTES.each do |a|
if value = pson[a.to_s]
resource.send(a.to_s + "=", value)
end
end
resource.exported ||= false
resource
end
def to_pson_data_hash
data = ([:type, :title, :tags] + ATTRIBUTES).inject({}) do |hash, param|
next hash unless value = self.send(param)
hash[param.to_s] = value
hash
end
data["exported"] ||= false
params = self.to_hash.inject({}) do |hash, ary|
param, value = ary
# Don't duplicate the title as the namevar
next hash if param == namevar and value == title
hash[param] = value
hash
end
data["parameters"] = params unless params.empty?
data
end
def to_pson(*args)
to_pson_data_hash.to_pson(*args)
end
# Proxy these methods to the parameters hash. It's likely they'll
# be overridden at some point, but this works for now.
%w{has_key? keys length delete empty? <<}.each do |method|
define_method(method) do |*args|
@parameters.send(method, *args)
end
end
# Set a given parameter. Converts all passed names
# to lower-case symbols.
def []=(param, value)
@parameters[parameter_name(param)] = value
end
# Return a given parameter's value. Converts all passed names
# to lower-case symbols.
def [](param)
@parameters[parameter_name(param)]
end
# Compatibility method.
def builtin?
builtin_type?
end
# Is this a builtin resource type?
def builtin_type?
@reference.builtin_type?
end
# Iterate over each param/value pair, as required for Enumerable.
def each
@parameters.each { |p,v| yield p, v }
end
def include?(parameter)
super || @parameters.keys.include?( parameter_name(parameter) )
end
%w{exported virtual}.each do |m|
define_method(m+"?") do
self.send(m)
end
end
# Create our resource.
def initialize(type, title, parameters = {})
@reference = Puppet::Resource::Reference.new(type, title)
@parameters = {}
parameters.each do |param, value|
self[param] = value
end
tag(@reference.type)
tag(@reference.title) if valid_tag?(@reference.title)
end
# Provide a reference to our resource in the canonical form.
def ref
@reference.to_s
end
# Get our title information from the reference, since it will canonize it for us.
def title
@reference.title
end
# Get our type information from the reference, since it will canonize it for us.
def type
@reference.type
end
# Produce a simple hash of our parameters.
def to_hash
result = @parameters.dup
unless result.include?(namevar)
result[namevar] = title
end
result
end
def to_s
return ref
end
# Convert our resource to Puppet code.
def to_manifest
"%s { '%s':\n%s\n}" % [self.type.to_s.downcase, self.title,
@parameters.collect { |p, v|
if v.is_a? Array
" #{p} => [\'#{v.join("','")}\']"
else
" #{p} => \'#{v}\'"
end
}.join(",\n")
]
end
def to_ref
ref
end
# Convert our resource to a RAL resource instance. Creates component
# instances for resource types that don't exist.
def to_ral
if typeklass = Puppet::Type.type(self.type)
return typeklass.new(self)
else
return Puppet::Type::Component.new(self)
end
end
# Translate our object to a backward-compatible transportable object.
def to_trans
if @reference.builtin_type?
result = to_transobject
else
result = to_transbucket
end
result.file = self.file
result.line = self.line
return result
end
# Create an old-style TransObject instance, for builtin resource types.
def to_transobject
# Now convert to a transobject
result = Puppet::TransObject.new(@reference.title, @reference.type)
to_hash.each do |p, v|
if v.is_a?(Puppet::Resource::Reference)
v = v.to_trans_ref
elsif v.is_a?(Array)
v = v.collect { |av|
if av.is_a?(Puppet::Resource::Reference)
av = av.to_trans_ref
end
av
}
end
# If the value is an array with only one value, then
# convert it to a single value. This is largely so that
# the database interaction doesn't have to worry about
# whether it returns an array or a string.
result[p.to_s] = if v.is_a?(Array) and v.length == 1
v[0]
else
v
end
end
result.tags = self.tags
return result
end
+ def name
+ # this is potential namespace conflict
+ # between the notion of an "indirector name"
+ # and a "resource name"
+ [ type, title ].join('/')
+ end
+
+ def to_resource
+ self
+ end
+
private
# Produce a canonical method name.
def parameter_name(param)
param = param.to_s.downcase.to_sym
if param == :name and n = namevar()
param = namevar
end
param
end
# The namevar for our resource type. If the type doesn't exist,
# always use :name.
def namevar
if t = resource_type
t.namevar
else
:name
end
end
# Retrieve the resource type.
def resource_type
Puppet::Type.type(type)
end
# Create an old-style TransBucket instance, for non-builtin resource types.
def to_transbucket
bucket = Puppet::TransBucket.new([])
bucket.type = self.type
bucket.name = self.title
# TransBuckets don't support parameters, which is why they're being deprecated.
return bucket
end
end
diff --git a/lib/puppet/resource/catalog.rb b/lib/puppet/resource/catalog.rb
index c5ae8f52d..6e064a9a1 100644
--- a/lib/puppet/resource/catalog.rb
+++ b/lib/puppet/resource/catalog.rb
@@ -1,615 +1,616 @@
+require 'puppet/resource'
require 'puppet/node'
require 'puppet/indirector'
require 'puppet/simple_graph'
require 'puppet/transaction'
require 'puppet/util/cacher'
require 'puppet/util/pson'
require 'puppet/util/tagging'
# This class models a node catalog. It is the thing
# meant to be passed from server to client, and it contains all
# of the information in the catalog, including the resources
# and the relationships between them.
class Puppet::Resource::Catalog < Puppet::SimpleGraph
class DuplicateResourceError < Puppet::Error; end
extend Puppet::Indirector
indirects :catalog, :terminus_class => :compiler
include Puppet::Util::Tagging
extend Puppet::Util::Pson
include Puppet::Util::Cacher::Expirer
# The host name this is a catalog for.
attr_accessor :name
# The catalog version. Used for testing whether a catalog
# is up to date.
attr_accessor :version
# How long this catalog took to retrieve. Used for reporting stats.
attr_accessor :retrieval_duration
# How we should extract the catalog for sending to the client.
attr_reader :extraction_format
# Whether this is a host catalog, which behaves very differently.
# In particular, reports are sent, graphs are made, and state is
# stored in the state database. If this is set incorrectly, then you often
# end up in infinite loops, because catalogs are used to make things
# that the host catalog needs.
attr_accessor :host_config
# Whether this catalog was retrieved from the cache, which affects
# whether it is written back out again.
attr_accessor :from_cache
# Some metadata to help us compile and generally respond to the current state.
attr_accessor :client_version, :server_version
# Add classes to our class list.
def add_class(*classes)
classes.each do |klass|
@classes << klass
end
# Add the class names as tags, too.
tag(*classes)
end
# Add one or more resources to our graph and to our resource table.
# This is actually a relatively complicated method, because it handles multiple
# aspects of Catalog behaviour:
# * Add the resource to the resource table
# * Add the resource to the resource graph
# * Add the resource to the relationship graph
# * Add any aliases that make sense for the resource (e.g., name != title)
def add_resource(*resources)
resources.each do |resource|
unless resource.respond_to?(:ref)
raise ArgumentError, "Can only add objects that respond to :ref, not instances of %s" % resource.class
end
end.each { |resource| fail_unless_unique(resource) }.each do |resource|
ref = resource.ref
@transient_resources << resource if applying?
@resource_table[ref] = resource
# If the name and title differ, set up an alias
- if resource.respond_to?(:name) and resource.respond_to?(:title) and resource.name != resource.title
+ if resource.respond_to?(:name) and resource.respond_to?(:title) and resource.respond_to?(:isomorphic?) and resource.name != resource.title
self.alias(resource, resource.name) if resource.isomorphic?
end
resource.catalog = self if resource.respond_to?(:catalog=)
add_vertex(resource)
if @relationship_graph
@relationship_graph.add_vertex(resource)
end
yield(resource) if block_given?
end
end
# Create an alias for a resource.
def alias(resource, name)
#set $1
resource.ref =~ /^(.+)\[/
newref = "%s[%s]" % [$1 || resource.class.name, name]
# LAK:NOTE It's important that we directly compare the references,
# because sometimes an alias is created before the resource is
# added to the catalog, so comparing inside the below if block
# isn't sufficient.
return if newref == resource.ref
if existing = @resource_table[newref]
return if existing == resource
raise(ArgumentError, "Cannot alias %s to %s; resource %s already exists" % [resource.ref, name, newref])
end
@resource_table[newref] = resource
@aliases[resource.ref] ||= []
@aliases[resource.ref] << newref
end
# Apply our catalog to the local host. Valid options
# are:
# :tags - set the tags that restrict what resources run
# during the transaction
# :ignoreschedules - tell the transaction to ignore schedules
# when determining the resources to run
def apply(options = {})
@applying = true
# Expire all of the resource data -- this ensures that all
# data we're operating against is entirely current.
expire()
Puppet::Util::Storage.load if host_config?
transaction = Puppet::Transaction.new(self)
transaction.tags = options[:tags] if options[:tags]
transaction.ignoreschedules = true if options[:ignoreschedules]
transaction.add_times :config_retrieval => self.retrieval_duration || 0
begin
transaction.evaluate
rescue Puppet::Error => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "Could not apply complete catalog: %s" % detail
rescue => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "Got an uncaught exception of type %s: %s" % [detail.class, detail]
ensure
# Don't try to store state unless we're a host config
# too recursive.
Puppet::Util::Storage.store if host_config?
end
yield transaction if block_given?
return transaction
ensure
@applying = false
cleanup()
transaction.cleanup if defined? transaction and transaction
end
# Are we in the middle of applying the catalog?
def applying?
@applying
end
def clear(remove_resources = true)
super()
# We have to do this so that the resources clean themselves up.
@resource_table.values.each { |resource| resource.remove } if remove_resources
@resource_table.clear
if defined?(@relationship_graph) and @relationship_graph
@relationship_graph.clear
@relationship_graph = nil
end
end
def classes
@classes.dup
end
# Create a new resource and register it in the catalog.
def create_resource(type, options)
unless klass = Puppet::Type.type(type)
raise ArgumentError, "Unknown resource type %s" % type
end
return unless resource = klass.new(options)
add_resource(resource)
resource
end
def dependent_data_expired?(ts)
if applying?
return super
else
return true
end
end
# Make sure we support the requested extraction format.
def extraction_format=(value)
unless respond_to?("extract_to_%s" % value)
raise ArgumentError, "Invalid extraction format %s" % value
end
@extraction_format = value
end
# Turn our catalog graph into whatever the client is expecting.
def extract
send("extract_to_%s" % extraction_format)
end
# Create the traditional TransBuckets and TransObjects from our catalog
# graph. LAK:NOTE(20081211): This is a pre-0.25 backward compatibility method.
# It can be removed as soon as xmlrpc is killed.
def extract_to_transportable
top = nil
current = nil
buckets = {}
unless main = vertices.find { |res| res.type == "Class" and res.title == :main }
raise Puppet::DevError, "Could not find 'main' class; cannot generate catalog"
end
# Create a proc for examining edges, which we'll use to build our tree
# of TransBuckets and TransObjects.
bucket = nil
walk(main, :out) do |source, target|
# The sources are always non-builtins.
unless tmp = buckets[source.to_s]
if tmp = buckets[source.to_s] = source.to_trans
bucket = tmp
else
# This is because virtual resources return nil. If a virtual
# container resource contains realized resources, we still need to get
# to them. So, we keep a reference to the last valid bucket
# we returned and use that if the container resource is virtual.
end
end
bucket = tmp || bucket
if child = target.to_trans
unless bucket
raise "No bucket created for %s" % source
end
bucket.push child
# It's important that we keep a reference to any TransBuckets we've created, so
# we don't create multiple buckets for children.
unless target.builtin?
buckets[target.to_s] = child
end
end
end
# Retrieve the bucket for the top-level scope and set the appropriate metadata.
unless result = buckets[main.to_s]
# This only happens when the catalog is entirely empty.
result = buckets[main.to_s] = main.to_trans
end
result.classes = classes
# Clear the cache to encourage the GC
buckets.clear
return result
end
# Make sure all of our resources are "finished".
def finalize
make_default_resources
@resource_table.values.each { |resource| resource.finish }
write_graph(:resources)
end
def host_config?
host_config || false
end
def initialize(name = nil)
super()
@name = name if name
@extraction_format ||= :transportable
@classes = []
@resource_table = {}
@transient_resources = []
@applying = false
@relationship_graph = nil
@aliases = {}
if block_given?
yield(self)
finalize()
end
end
# Make the default objects necessary for function.
def make_default_resources
# We have to add the resources to the catalog, or else they won't get cleaned up after
# the transaction.
# First create the default scheduling objects
Puppet::Type.type(:schedule).mkdefaultschedules.each { |res| add_resource(res) unless resource(res.ref) }
# And filebuckets
if bucket = Puppet::Type.type(:filebucket).mkdefaultbucket
add_resource(bucket) unless resource(bucket.ref)
end
end
# Create a graph of all of the relationships in our catalog.
def relationship_graph
unless defined? @relationship_graph and @relationship_graph
# It's important that we assign the graph immediately, because
# the debug messages below use the relationships in the
# relationship graph to determine the path to the resources
# spitting out the messages. If this is not set,
# then we get into an infinite loop.
@relationship_graph = Puppet::SimpleGraph.new
# First create the dependency graph
self.vertices.each do |vertex|
@relationship_graph.add_vertex vertex
vertex.builddepends.each do |edge|
@relationship_graph.add_edge(edge)
end
end
# Lastly, add in any autorequires
@relationship_graph.vertices.each do |vertex|
vertex.autorequire(self).each do |edge|
unless @relationship_graph.edge?(edge.source, edge.target) # don't let automatic relationships conflict with manual ones.
unless @relationship_graph.edge?(edge.target, edge.source)
vertex.debug "Autorequiring %s" % [edge.source]
@relationship_graph.add_edge(edge)
else
vertex.debug "Skipping automatic relationship with %s" % (edge.source == vertex ? edge.target : edge.source)
end
end
end
end
@relationship_graph.write_graph(:relationships) if host_config?
# Then splice in the container information
@relationship_graph.splice!(self, Puppet::Type::Component)
@relationship_graph.write_graph(:expanded_relationships) if host_config?
end
@relationship_graph
end
# Remove the resource from our catalog. Notice that we also call
# 'remove' on the resource, at least until resource classes no longer maintain
# references to the resource instances.
def remove_resource(*resources)
resources.each do |resource|
@resource_table.delete(resource.ref)
if aliases = @aliases[resource.ref]
aliases.each { |res_alias| @resource_table.delete(res_alias) }
@aliases.delete(resource.ref)
end
remove_vertex!(resource) if vertex?(resource)
@relationship_graph.remove_vertex!(resource) if @relationship_graph and @relationship_graph.vertex?(resource)
resource.remove
end
end
# Look a resource up by its reference (e.g., File[/etc/passwd]).
def resource(type, title = nil)
# Always create a resource reference, so that it always canonizes how we
# are referring to them.
if title
ref = Puppet::Resource::Reference.new(type, title).to_s
else
# If they didn't provide a title, then we expect the first
# argument to be of the form 'Class[name]', which our
# Reference class canonizes for us.
ref = Puppet::Resource::Reference.new(nil, type).to_s
end
@resource_table[ref]
end
# Return an array of all resources.
def resources
@resource_table.keys
end
def self.from_pson(data)
result = new(data['name'])
if tags = data['tags']
result.tag(*tags)
end
if version = data['version']
result.version = version
end
if resources = data['resources']
resources = PSON.parse(resources) if resources.is_a?(String)
resources.each do |res|
resource_from_pson(result, res)
end
end
if edges = data['edges']
edges = PSON.parse(edges) if edges.is_a?(String)
edges.each do |edge|
edge_from_pson(result, edge)
end
end
if classes = data['classes']
result.add_class(*classes)
end
result
end
def self.edge_from_pson(result, edge)
# If no type information was presented, we manually find
# the class.
edge = Puppet::Relationship.from_pson(edge) if edge.is_a?(Hash)
unless source = result.resource(edge.source)
raise ArgumentError, "Could not convert from pson: Could not find relationship source #{edge.source.inspect}"
end
edge.source = source
unless target = result.resource(edge.target)
raise ArgumentError, "Could not convert from pson: Could not find relationship target #{edge.target.inspect}"
end
edge.target = target
result.add_edge(edge)
end
def self.resource_from_pson(result, res)
res = Puppet::Resource.from_pson(res) if res.is_a? Hash
result.add_resource(res)
end
PSON.register_document_type('Catalog',self)
def to_pson_data_hash
{
'document_type' => 'Catalog',
'data' => {
'tags' => tags,
'name' => name,
'version' => version,
'resources' => vertices.collect { |v| v.to_pson_data_hash },
'edges' => edges. collect { |e| e.to_pson_data_hash },
'classes' => classes
},
'metadata' => {
'api_version' => 1
}
}
end
def to_pson(*args)
to_pson_data_hash.to_pson(*args)
end
# Convert our catalog into a RAL catalog.
def to_ral
to_catalog :to_ral
end
# Convert our catalog into a catalog of Puppet::Resource instances.
def to_resource
to_catalog :to_resource
end
# filter out the catalog, applying +block+ to each resource.
# If the block result is false, the resource will
# be kept otherwise it will be skipped
def filter(&block)
to_catalog :to_resource, &block
end
# Store the classes in the classfile.
def write_class_file
begin
::File.open(Puppet[:classfile], "w") do |f|
f.puts classes.join("\n")
end
rescue => detail
Puppet.err "Could not create class file %s: %s" % [Puppet[:classfile], detail]
end
end
# Produce the graph files if requested.
def write_graph(name)
# We only want to graph the main host catalog.
return unless host_config?
super
end
private
def cleanup
unless @transient_resources.empty?
remove_resource(*@transient_resources)
@transient_resources.clear
@relationship_graph = nil
end
# Expire any cached data the resources are keeping.
expire()
end
# Verify that the given resource isn't defined elsewhere.
def fail_unless_unique(resource)
# Short-curcuit the common case,
return unless existing_resource = @resource_table[resource.ref]
# If we've gotten this far, it's a real conflict
# Either it's a defined type, which are never
# isomorphic, or it's a non-isomorphic type, so
# we should throw an exception.
msg = "Duplicate definition: %s is already defined" % resource.ref
if existing_resource.file and existing_resource.line
msg << " in file %s at line %s" %
[existing_resource.file, existing_resource.line]
end
if resource.line or resource.file
msg << "; cannot redefine"
end
raise DuplicateResourceError.new(msg)
end
# An abstracted method for converting one catalog into another type of catalog.
# This pretty much just converts all of the resources from one class to another, using
# a conversion method.
def to_catalog(convert)
result = self.class.new(self.name)
result.version = self.version
map = {}
vertices.each do |resource|
next if virtual_not_exported?(resource)
next if block_given? and yield resource
#This is hackity hack for 1094
#Aliases aren't working in the ral catalog because the current instance of the resource
#has a reference to the catalog being converted. . . So, give it a reference to the new one
#problem solved. . .
if resource.is_a?(Puppet::Resource)
resource = resource.dup
resource.catalog = result
elsif resource.is_a?(Puppet::TransObject)
resource = resource.dup
resource.catalog = result
elsif resource.is_a?(Puppet::Parser::Resource)
resource = resource.to_resource
resource.catalog = result
end
if resource.is_a?(Puppet::Resource) and convert.to_s == "to_resource"
newres = resource
else
newres = resource.send(convert)
end
# We can't guarantee that resources don't munge their names
# (like files do with trailing slashes), so we have to keep track
# of what a resource got converted to.
map[resource.ref] = newres
result.add_resource newres
end
message = convert.to_s.gsub "_", " "
edges.each do |edge|
# Skip edges between virtual resources.
next if virtual_not_exported?(edge.source)
next if block_given? and yield edge.source
next if virtual_not_exported?(edge.target)
next if block_given? and yield edge.target
unless source = map[edge.source.ref]
raise Puppet::DevError, "Could not find resource %s when converting %s resources" % [edge.source.ref, message]
end
unless target = map[edge.target.ref]
raise Puppet::DevError, "Could not find resource %s when converting %s resources" % [edge.target.ref, message]
end
result.add_edge(source, target, edge.label)
end
map.clear
result.add_class(*self.classes)
result.tag(*self.tags)
return result
end
def virtual_not_exported?(resource)
resource.respond_to?(:virtual?) and resource.virtual? and (resource.respond_to?(:exported?) and not resource.exported?)
end
end
diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb
index 1df84f2df..2fb4abca8 100644
--- a/lib/puppet/type.rb
+++ b/lib/puppet/type.rb
@@ -1,2040 +1,2046 @@
require 'puppet'
require 'puppet/util/log'
require 'puppet/util/metric'
require 'puppet/property'
require 'puppet/parameter'
require 'puppet/util'
require 'puppet/util/autoload'
require 'puppet/metatype/manager'
require 'puppet/util/errors'
require 'puppet/util/log_paths'
require 'puppet/util/logging'
require 'puppet/resource/reference'
require 'puppet/util/cacher'
require 'puppet/file_collection/lookup'
require 'puppet/util/tagging'
# see the bottom of the file for the rest of the inclusions
module Puppet
class Type
include Puppet::Util
include Puppet::Util::Errors
include Puppet::Util::LogPaths
include Puppet::Util::Logging
include Puppet::Util::Cacher
include Puppet::FileCollection::Lookup
include Puppet::Util::Tagging
###############################
# Code related to resource type attributes.
class << self
include Puppet::Util::ClassGen
include Puppet::Util::Warnings
attr_reader :properties
end
def self.states
warnonce "The states method is deprecated; use properties"
properties()
end
# All parameters, in the appropriate order. The namevar comes first, then
# the provider, then the properties, and finally the params and metaparams
# in the order they were specified in the files.
def self.allattrs
# Cache this, since it gets called multiple times
namevar = self.namevar
order = [namevar]
if self.parameters.include?(:provider)
order << :provider
end
order << [self.properties.collect { |property| property.name },
self.parameters - [:provider],
self.metaparams].flatten.reject { |param|
# we don't want our namevar in there multiple times
param == namevar
}
order.flatten!
return order
end
# Retrieve an attribute alias, if there is one.
def self.attr_alias(param)
@attr_aliases[symbolize(param)]
end
# Create an alias to an existing attribute. This will cause the aliased
# attribute to be valid when setting and retrieving values on the instance.
def self.set_attr_alias(hash)
hash.each do |new, old|
@attr_aliases[symbolize(new)] = symbolize(old)
end
end
# Find the class associated with any given attribute.
def self.attrclass(name)
@attrclasses ||= {}
# We cache the value, since this method gets called such a huge number
# of times (as in, hundreds of thousands in a given run).
unless @attrclasses.include?(name)
@attrclasses[name] = case self.attrtype(name)
when :property; @validproperties[name]
when :meta; @@metaparamhash[name]
when :param; @paramhash[name]
end
end
@attrclasses[name]
end
# What type of parameter are we dealing with? Cache the results, because
# this method gets called so many times.
def self.attrtype(attr)
@attrtypes ||= {}
unless @attrtypes.include?(attr)
@attrtypes[attr] = case
when @validproperties.include?(attr); :property
when @paramhash.include?(attr); :param
when @@metaparamhash.include?(attr); :meta
end
end
@attrtypes[attr]
end
# Copy an existing class parameter. This allows other types to avoid
# duplicating a parameter definition, and is mostly used by subclasses
# of the File class.
def self.copyparam(klass, name)
param = klass.attrclass(name)
unless param
raise Puppet::DevError, "Class %s has no param %s" % [klass, name]
end
@parameters << param
@parameters.each { |p| @paramhash[name] = p }
if param.isnamevar?
@namevar = param.name
end
end
def self.eachmetaparam
@@metaparams.each { |p| yield p.name }
end
# Create the 'ensure' class. This is a separate method so other types
# can easily call it and create their own 'ensure' values.
def self.ensurable(&block)
if block_given?
self.newproperty(:ensure, :parent => Puppet::Property::Ensure, &block)
else
self.newproperty(:ensure, :parent => Puppet::Property::Ensure) do
self.defaultvalues
end
end
end
# Should we add the 'ensure' property to this class?
def self.ensurable?
# If the class has all three of these methods defined, then it's
# ensurable.
ens = [:exists?, :create, :destroy].inject { |set, method|
set &&= self.public_method_defined?(method)
}
return ens
end
# Deal with any options passed into parameters.
def self.handle_param_options(name, options)
# If it's a boolean parameter, create a method to test the value easily
if options[:boolean]
define_method(name.to_s + "?") do
val = self[name]
if val == :true or val == true
return true
end
end
end
end
# Is the parameter in question a meta-parameter?
def self.metaparam?(param)
@@metaparamhash.include?(symbolize(param))
end
# Find the metaparameter class associated with a given metaparameter name.
def self.metaparamclass(name)
@@metaparamhash[symbolize(name)]
end
def self.metaparams
@@metaparams.collect { |param| param.name }
end
def self.metaparamdoc(metaparam)
@@metaparamhash[metaparam].doc
end
# Create a new metaparam. Requires a block and a name, stores it in the
# @parameters array, and does some basic checking on it.
def self.newmetaparam(name, options = {}, &block)
@@metaparams ||= []
@@metaparamhash ||= {}
name = symbolize(name)
param = genclass(name,
:parent => options[:parent] || Puppet::Parameter,
:prefix => "MetaParam",
:hash => @@metaparamhash,
:array => @@metaparams,
:attributes => options[:attributes],
&block
)
# Grr.
if options[:required_features]
param.required_features = options[:required_features]
end
handle_param_options(name, options)
param.metaparam = true
return param
end
# Find the namevar
def self.namevar_parameter
@namevar_parameter ||= (
params = @parameters.find_all { |param|
param.isnamevar? or param.name == :name
}
if params.length > 1
raise Puppet::DevError, "Found multiple namevars for %s" % self.name
elsif params.length == 1
params.first
else
raise Puppet::DevError, "No namevar for %s" % self.name
end
)
end
def self.namevar
@namevar ||= namevar_parameter.name
end
def self.canonicalize_ref(s)
namevar_parameter.canonicalize(s)
end
# Create a new parameter. Requires a block and a name, stores it in the
# @parameters array, and does some basic checking on it.
def self.newparam(name, options = {}, &block)
options[:attributes] ||= {}
param = genclass(name,
:parent => options[:parent] || Puppet::Parameter,
:attributes => options[:attributes],
:block => block,
:prefix => "Parameter",
:array => @parameters,
:hash => @paramhash
)
handle_param_options(name, options)
# Grr.
if options[:required_features]
param.required_features = options[:required_features]
end
param.isnamevar if options[:namevar]
if param.isnamevar?
@namevar = param.name
end
return param
end
def self.newstate(name, options = {}, &block)
Puppet.warning "newstate() has been deprecrated; use newproperty(%s)" %
name
newproperty(name, options, &block)
end
# Create a new property. The first parameter must be the name of the property;
# this is how users will refer to the property when creating new instances.
# The second parameter is a hash of options; the options are:
# * :parent: The parent class for the property. Defaults to Puppet::Property.
# * :retrieve: The method to call on the provider or @parent object (if
# the provider is not set) to retrieve the current value.
def self.newproperty(name, options = {}, &block)
name = symbolize(name)
# This is here for types that might still have the old method of defining
# a parent class.
unless options.is_a? Hash
raise Puppet::DevError,
"Options must be a hash, not %s" % options.inspect
end
if @validproperties.include?(name)
raise Puppet::DevError, "Class %s already has a property named %s" %
[self.name, name]
end
if parent = options[:parent]
options.delete(:parent)
else
parent = Puppet::Property
end
# We have to create our own, new block here because we want to define
# an initial :retrieve method, if told to, and then eval the passed
# block if available.
prop = genclass(name, :parent => parent, :hash => @validproperties, :attributes => options) do
# If they've passed a retrieve method, then override the retrieve
# method on the class.
if options[:retrieve]
define_method(:retrieve) do
provider.send(options[:retrieve])
end
end
if block
class_eval(&block)
end
end
# If it's the 'ensure' property, always put it first.
if name == :ensure
@properties.unshift prop
else
@properties << prop
end
return prop
end
def self.paramdoc(param)
@paramhash[param].doc
end
# Return the parameter names
def self.parameters
return [] unless defined? @parameters
@parameters.collect { |klass| klass.name }
end
# Find the parameter class associated with a given parameter name.
def self.paramclass(name)
@paramhash[name]
end
# Return the property class associated with a name
def self.propertybyname(name)
@validproperties[name]
end
def self.validattr?(name)
name = symbolize(name)
return true if name == :name
@validattrs ||= {}
unless @validattrs.include?(name)
if self.validproperty?(name) or self.validparameter?(name) or self.metaparam?(name)
@validattrs[name] = true
else
@validattrs[name] = false
end
end
@validattrs[name]
end
# does the name reflect a valid property?
def self.validproperty?(name)
name = symbolize(name)
if @validproperties.include?(name)
return @validproperties[name]
else
return false
end
end
# Return the list of validproperties
def self.validproperties
return {} unless defined? @parameters
return @validproperties.keys
end
# does the name reflect a valid parameter?
def self.validparameter?(name)
unless defined? @parameters
raise Puppet::DevError, "Class %s has not defined parameters" % self
end
if @paramhash.include?(name) or @@metaparamhash.include?(name)
return true
else
return false
end
end
# Return either the attribute alias or the attribute.
def attr_alias(name)
name = symbolize(name)
if synonym = self.class.attr_alias(name)
return synonym
else
return name
end
end
# Are we deleting this resource?
def deleting?
obj = @parameters[:ensure] and obj.should == :absent
end
# Create a new property if it is valid but doesn't exist
# Returns: true if a new parameter was added, false otherwise
def add_property_parameter(prop_name)
if self.class.validproperty?(prop_name) && !@parameters[prop_name]
self.newattr(prop_name)
return true
end
return false
end
# abstract accessing parameters and properties, and normalize
# access to always be symbols, not strings
# This returns a value, not an object. It returns the 'is'
# value, but you can also specifically return 'is' and 'should'
# values using 'object.is(:property)' or 'object.should(:property)'.
def [](name)
name = attr_alias(name)
unless self.class.validattr?(name)
fail("Invalid parameter %s(%s)" % [name, name.inspect])
end
if name == :name
name = self.class.namevar
end
if obj = @parameters[name]
# Note that if this is a property, then the value is the "should" value,
# not the current value.
obj.value
else
return nil
end
end
# Abstract setting parameters and properties, and normalize
# access to always be symbols, not strings. This sets the 'should'
# value on properties, and otherwise just sets the appropriate parameter.
def []=(name,value)
name = attr_alias(name)
unless self.class.validattr?(name)
fail("Invalid parameter %s" % [name])
end
if name == :name
name = self.class.namevar
end
if value.nil?
raise Puppet::Error.new("Got nil value for %s" % name)
end
if obj = @parameters[name]
obj.value = value
return nil
else
self.newattr(name, :value => value)
end
nil
end
# remove a property from the object; useful in testing or in cleanup
# when an error has been encountered
def delete(attr)
attr = symbolize(attr)
if @parameters.has_key?(attr)
@parameters.delete(attr)
else
raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}")
end
end
# iterate across the existing properties
def eachproperty
# properties() is a private method
properties().each { |property|
yield property
}
end
# Create a transaction event. Called by Transaction or by
# a property.
def event(options = {})
Puppet::Transaction::Event.new({:resource => self, :file => file, :line => line, :tags => tags, :version => version}.merge(options))
end
# Let the catalog determine whether a given cached value is
# still valid or has expired.
def expirer
catalog
end
# retrieve the 'should' value for a specified property
def should(name)
name = attr_alias(name)
if prop = @parameters[name] and prop.is_a?(Puppet::Property)
return prop.should
else
return nil
end
end
# Create the actual attribute instance. Requires either the attribute
# name or class as the first argument, then an optional hash of
# attributes to set during initialization.
def newattr(name, options = {})
if name.is_a?(Class)
klass = name
name = klass.name
end
unless klass = self.class.attrclass(name)
raise Puppet::Error, "Resource type %s does not support parameter %s" % [self.class.name, name]
end
if @parameters.include?(name)
raise Puppet::Error, "Parameter '%s' is already defined in %s" %
[name, self.ref]
end
if provider and ! provider.class.supports_parameter?(klass)
missing = klass.required_features.find_all { |f| ! provider.class.feature?(f) }
info "Provider %s does not support features %s; not managing attribute %s" % [provider.class.name, missing.join(", "), name]
return nil
end
# Add resource information at creation time, so it's available
# during validation.
options[:resource] = self
begin
# make sure the parameter doesn't have any errors
return @parameters[name] = klass.new(options)
rescue => detail
error = Puppet::Error.new("Parameter %s failed: %s" %
[name, detail])
error.set_backtrace(detail.backtrace)
raise error
end
end
# return the value of a parameter
def parameter(name)
@parameters[name.to_sym]
end
def parameters
@parameters.dup
end
# Is the named property defined?
def propertydefined?(name)
unless name.is_a? Symbol
name = name.intern
end
return @parameters.include?(name)
end
# Return an actual property instance by name; to return the value, use 'resource[param]'
# LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method,
# this one should probably go away at some point.
def property(name)
if obj = @parameters[symbolize(name)] and obj.is_a?(Puppet::Property)
return obj
else
return nil
end
end
# For any parameters or properties that have defaults and have not yet been
# set, set them now. This method can be handed a list of attributes,
# and if so it will only set defaults for those attributes.
def set_default(attr)
return unless klass = self.class.attrclass(attr)
return unless klass.method_defined?(:default)
return if @parameters.include?(klass.name)
return unless parameter = newattr(klass.name)
if value = parameter.default and ! value.nil?
parameter.value = value
else
@parameters.delete(parameter.name)
end
end
# Convert our object to a hash. This just includes properties.
def to_hash
rethash = {}
@parameters.each do |name, obj|
rethash[name] = obj.value
end
rethash
end
def type
self.class.name
end
# Return a specific value for an attribute.
def value(name)
name = attr_alias(name)
if obj = @parameters[name] and obj.respond_to?(:value)
return obj.value
else
return nil
end
end
def version
return 0 unless catalog
catalog.version
end
# Return all of the property objects, in the order specified in the
# class.
def properties
self.class.properties.collect { |prop| @parameters[prop.name] }.compact
end
# Is this type's name isomorphic with the object? That is, if the
# name conflicts, does it necessarily mean that the objects conflict?
# Defaults to true.
def self.isomorphic?
if defined? @isomorphic
return @isomorphic
else
return true
end
end
def isomorphic?
self.class.isomorphic?
end
# is the instance a managed instance? A 'yes' here means that
# the instance was created from the language, vs. being created
# in order resolve other questions, such as finding a package
# in a list
def managed?
# Once an object is managed, it always stays managed; but an object
# that is listed as unmanaged might become managed later in the process,
# so we have to check that every time
if defined? @managed and @managed
return @managed
else
@managed = false
properties.each { |property|
s = property.should
if s and ! property.class.unmanaged
@managed = true
break
end
}
return @managed
end
end
###############################
# Code related to the container behaviour.
# this is a retarded hack method to get around the difference between
# component children and file children
def self.depthfirst?
if defined? @depthfirst
return @depthfirst
else
return false
end
end
def depthfirst?
self.class.depthfirst?
end
# Remove an object. The argument determines whether the object's
# subscriptions get eliminated, too.
def remove(rmdeps = true)
# This is hackish (mmm, cut and paste), but it works for now, and it's
# better than warnings.
@parameters.each do |name, obj|
obj.remove
end
@parameters.clear
@parent = nil
# Remove the reference to the provider.
if self.provider
@provider.clear
@provider = nil
end
end
###############################
# Code related to evaluating the resources.
# Flush the provider, if it supports it. This is called by the
# transaction.
def flush
if self.provider and self.provider.respond_to?(:flush)
self.provider.flush
end
end
# if all contained objects are in sync, then we're in sync
# FIXME I don't think this is used on the type instances any more,
# it's really only used for testing
def insync?(is)
insync = true
if property = @parameters[:ensure]
unless is.include? property
raise Puppet::DevError,
"The is value is not in the is array for '%s'" %
[property.name]
end
ensureis = is[property]
if property.insync?(ensureis) and property.should == :absent
return true
end
end
properties.each { |property|
unless is.include? property
raise Puppet::DevError,
"The is value is not in the is array for '%s'" %
[property.name]
end
propis = is[property]
unless property.insync?(propis)
property.debug("Not in sync: %s vs %s" %
[propis.inspect, property.should.inspect])
insync = false
#else
# property.debug("In sync")
end
}
#self.debug("%s sync status is %s" % [self,insync])
return insync
end
# retrieve the current value of all contained properties
def retrieve
if self.provider.is_a?(Puppet::Provider) and ! provider.class.suitable?
fail "Provider #{provider.class.name} is not functional on this host"
end
result = Puppet::Resource.new(type, title)
# Provide the name, so we know we'll always refer to a real thing
result[:name] = self[:name] unless self[:name] == title
if ensure_prop = property(:ensure) or (self.class.validattr?(:ensure) and ensure_prop = newattr(:ensure))
result[:ensure] = ensure_state = ensure_prop.retrieve
else
ensure_state = nil
end
properties.each do |property|
next if property.name == :ensure
if ensure_state == :absent
result[property] = :absent
else
result[property] = property.retrieve
end
end
result
end
# Get a hash of the current properties. Returns a hash with
# the actual property instance as the key and the current value
# as the, um, value.
def currentpropvalues
# It's important to use the 'properties' method here, as it follows the order
# in which they're defined in the class. It also guarantees that 'ensure'
# is the first property, which is important for skipping 'retrieve' on
# all the properties if the resource is absent.
ensure_state = false
return properties().inject({}) do | prophash, property|
if property.name == :ensure
ensure_state = property.retrieve
prophash[property] = ensure_state
else
if ensure_state == :absent
prophash[property] = :absent
else
prophash[property] = property.retrieve
end
end
prophash
end
end
# Are we running in noop mode?
def noop?
if defined?(@noop)
@noop
else
Puppet[:noop]
end
end
def noop
noop?
end
###############################
# Code related to managing resource instances.
require 'puppet/transportable'
# retrieve a named instance of the current type
def self.[](name)
raise "Global resource access is deprecated"
@objects[name] || @aliases[name]
end
# add an instance by name to the class list of instances
def self.[]=(name,object)
raise "Global resource storage is deprecated"
newobj = nil
if object.is_a?(Puppet::Type)
newobj = object
else
raise Puppet::DevError, "must pass a Puppet::Type object"
end
if exobj = @objects[name] and self.isomorphic?
msg = "Object '%s[%s]' already exists" %
[newobj.class.name, name]
if exobj.file and exobj.line
msg += ("in file %s at line %s" %
[object.file, object.line])
end
if object.file and object.line
msg += ("and cannot be redefined in file %s at line %s" %
[object.file, object.line])
end
error = Puppet::Error.new(msg)
raise error
else
#Puppet.info("adding %s of type %s to class list" %
# [name,object.class])
@objects[name] = newobj
end
end
# Create an alias. We keep these in a separate hash so that we don't encounter
# the objects multiple times when iterating over them.
def self.alias(name, obj)
raise "Global resource aliasing is deprecated"
if @objects.include?(name)
unless @objects[name] == obj
raise Puppet::Error.new(
"Cannot create alias %s: object already exists" %
[name]
)
end
end
if @aliases.include?(name)
unless @aliases[name] == obj
raise Puppet::Error.new(
"Object %s already has alias %s" %
[@aliases[name].name, name]
)
end
end
@aliases[name] = obj
end
# remove all of the instances of a single type
def self.clear
raise "Global resource removal is deprecated"
if defined? @objects
@objects.each do |name, obj|
obj.remove(true)
end
@objects.clear
end
if defined? @aliases
@aliases.clear
end
end
# Force users to call this, so that we can merge objects if
# necessary.
def self.create(args)
# LAK:DEP Deprecation notice added 12/17/2008
Puppet.warning "Puppet::Type.create is deprecated; use Puppet::Type.new"
new(args)
end
# remove a specified object
def self.delete(resource)
raise "Global resource removal is deprecated"
return unless defined? @objects
if @objects.include?(resource.title)
@objects.delete(resource.title)
end
if @aliases.include?(resource.title)
@aliases.delete(resource.title)
end
if @aliases.has_value?(resource)
names = []
@aliases.each do |name, otherres|
if otherres == resource
names << name
end
end
names.each { |name| @aliases.delete(name) }
end
end
# iterate across each of the type's instances
def self.each
raise "Global resource iteration is deprecated"
return unless defined? @objects
@objects.each { |name,instance|
yield instance
}
end
# does the type have an object with the given name?
def self.has_key?(name)
raise "Global resource access is deprecated"
return @objects.has_key?(name)
end
# Retrieve all known instances. Either requires providers or must be overridden.
def self.instances
if provider_hash.empty?
raise Puppet::DevError, "%s has no providers and has not overridden 'instances'" % self.name
end
# Put the default provider first, then the rest of the suitable providers.
provider_instances = {}
providers_by_source.collect do |provider|
provider.instances.collect do |instance|
# We always want to use the "first" provider instance we find, unless the resource
# is already managed and has a different provider set
if other = provider_instances[instance.name]
Puppet.warning "%s %s found in both %s and %s; skipping the %s version" %
[self.name.to_s.capitalize, instance.name, other.class.name, instance.class.name, instance.class.name]
next
end
provider_instances[instance.name] = instance
new(:name => instance.name, :provider => instance, :check => :all)
end
end.flatten.compact
end
# Return a list of one suitable provider per source, with the default provider first.
def self.providers_by_source
# Put the default provider first, then the rest of the suitable providers.
sources = []
[defaultprovider, suitableprovider].flatten.uniq.collect do |provider|
next if sources.include?(provider.source)
sources << provider.source
provider
end.compact
end
# Convert a simple hash into a Resource instance. This is a convenience method,
# so people can create RAL resources with a hash and get the same behaviour
# as we get internally when we use Resource instances.
# This should only be used directly from Ruby -- it's not used when going through
# normal Puppet usage.
def self.hash2resource(hash)
hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result }
if title = hash[:title]
hash.delete(:title)
else
if self.namevar != :name
if hash.include?(:name) and hash.include?(self.namevar)
raise Puppet::Error, "Cannot provide both name and %s to resources of type %s" % [self.namevar, self.name]
end
if title = hash[self.namevar]
hash.delete(self.namevar)
end
end
unless title ||= hash[:name]
raise Puppet::Error, "You must specify a name or title for resources"
end
end
# Now create our resource.
resource = Puppet::Resource.new(self.name, title)
[:catalog].each do |attribute|
if value = hash[attribute]
hash.delete(attribute)
resource.send(attribute.to_s + "=", value)
end
end
hash.each do |param, value|
resource[param] = value
end
return resource
end
# Create the path for logging and such.
def pathbuilder
if p = parent
[p.pathbuilder, self.ref].flatten
else
[self.ref]
end
end
###############################
# Add all of the meta parameters.
newmetaparam(:noop) do
desc "Boolean flag indicating whether work should actually
be done."
newvalues(:true, :false)
munge do |value|
case value
when true, :true, "true"; @resource.noop = true
when false, :false, "false"; @resource.noop = false
end
end
end
newmetaparam(:schedule) do
desc "On what schedule the object should be managed. You must create a
schedule object, and then reference the name of that object to use
that for your schedule::
schedule { daily:
period => daily,
range => \"2-4\"
}
exec { \"/usr/bin/apt-get update\":
schedule => daily
}
The creation of the schedule object does not need to appear in the
configuration before objects that use it."
end
newmetaparam(:check) do
desc "Propertys which should have their values retrieved
but which should not actually be modified. This is currently used
internally, but will eventually be used for querying, so that you
could specify that you wanted to check the install state of all
packages, and then query the Puppet client daemon to get reports
on all packages."
munge do |args|
# If they've specified all, collect all known properties
if args == :all
args = @resource.class.properties.find_all do |property|
# Only get properties supported by our provider
if @resource.provider
@resource.provider.class.supports_parameter?(property)
else
true
end
end.collect do |property|
property.name
end
end
unless args.is_a?(Array)
args = [args]
end
unless defined? @resource
self.devfail "No parent for %s, %s?" %
[self.class, self.name]
end
args.each { |property|
unless property.is_a?(Symbol)
property = property.intern
end
next if @resource.propertydefined?(property)
unless propertyklass = @resource.class.validproperty?(property)
if @resource.class.validattr?(property)
next
else
raise Puppet::Error, "%s is not a valid attribute for %s" %
[property, self.class.name]
end
end
next unless propertyklass.checkable?
@resource.newattr(property)
}
end
end
newmetaparam(:loglevel) do
desc "Sets the level that information will be logged.
The log levels have the biggest impact when logs are sent to
syslog (which is currently the default)."
defaultto :notice
newvalues(*Puppet::Util::Log.levels)
newvalues(:verbose)
munge do |loglevel|
val = super(loglevel)
if val == :verbose
val = :info
end
val
end
end
newmetaparam(:alias) do
desc "Creates an alias for the object. Puppet uses this internally when you
provide a symbolic name::
file { sshdconfig:
path => $operatingsystem ? {
solaris => \"/usr/local/etc/ssh/sshd_config\",
default => \"/etc/ssh/sshd_config\"
},
source => \"...\"
}
service { sshd:
subscribe => file[sshdconfig]
}
When you use this feature, the parser sets ``sshdconfig`` as the name,
and the library sets that as an alias for the file so the dependency
lookup for ``sshd`` works. You can use this parameter yourself,
but note that only the library can use these aliases; for instance,
the following code will not work::
file { \"/etc/ssh/sshd_config\":
owner => root,
group => root,
alias => sshdconfig
}
file { sshdconfig:
mode => 644
}
There's no way here for the Puppet parser to know that these two stanzas
should be affecting the same file.
See the `LanguageTutorial language tutorial`:trac: for more information.
"
munge do |aliases|
unless aliases.is_a?(Array)
aliases = [aliases]
end
raise(ArgumentError, "Cannot add aliases without a catalog") unless @resource.catalog
aliases.each do |other|
if obj = @resource.catalog.resource(@resource.class.name, other)
unless obj.object_id == @resource.object_id
self.fail("%s can not create alias %s: object already exists" % [@resource.title, other])
end
next
end
# Newschool, add it to the catalog.
@resource.catalog.alias(@resource, other)
end
end
end
newmetaparam(:tag) do
desc "Add the specified tags to the associated resource. While all resources
are automatically tagged with as much information as possible
(e.g., each class and definition containing the resource), it can
be useful to add your own tags to a given resource.
Tags are currently useful for things like applying a subset of a
host's configuration::
puppetd --test --tags mytag
This way, when you're testing a configuration you can run just the
portion you're testing."
munge do |tags|
tags = [tags] unless tags.is_a? Array
tags.each do |tag|
@resource.tag(tag)
end
end
end
class RelationshipMetaparam < Puppet::Parameter
class << self
attr_accessor :direction, :events, :callback, :subclasses
end
@subclasses = []
def self.inherited(sub)
@subclasses << sub
end
def munge(references)
references = [references] unless references.is_a?(Array)
references.collect do |ref|
if ref.is_a?(Puppet::Resource::Reference)
ref
else
Puppet::Resource::Reference.new(ref)
end
end
end
def validate_relationship
@value.each do |ref|
unless @resource.catalog.resource(ref.to_s)
description = self.class.direction == :in ? "dependency" : "dependent"
fail "Could not find %s %s for %s" % [description, ref.to_s, resource.ref]
end
end
end
# Create edges from each of our relationships. :in
# relationships are specified by the event-receivers, and :out
# relationships are specified by the event generator. This
# way 'source' and 'target' are consistent terms in both edges
# and events -- that is, an event targets edges whose source matches
# the event's source. The direction of the relationship determines
# which resource is applied first and which resource is considered
# to be the event generator.
def to_edges
@value.collect do |reference|
reference.catalog = resource.catalog
# Either of the two retrieval attempts could have returned
# nil.
unless related_resource = reference.resolve
self.fail "Could not retrieve dependency '%s' of %s" % [reference, @resource.ref]
end
# Are we requiring them, or vice versa? See the method docs
# for futher info on this.
if self.class.direction == :in
source = related_resource
target = @resource
else
source = @resource
target = related_resource
end
if method = self.class.callback
subargs = {
:event => self.class.events,
:callback => method
}
self.debug("subscribes to %s" % [related_resource.ref])
else
# If there's no callback, there's no point in even adding
# a label.
subargs = nil
self.debug("requires %s" % [related_resource.ref])
end
rel = Puppet::Relationship.new(source, target, subargs)
end
end
end
def self.relationship_params
RelationshipMetaparam.subclasses
end
# Note that the order in which the relationships params is defined
# matters. The labelled params (notify and subcribe) must be later,
# so that if both params are used, those ones win. It's a hackish
# solution, but it works.
newmetaparam(:require, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :NONE}) do
desc "One or more objects that this object depends on.
This is used purely for guaranteeing that changes to required objects
happen before the dependent object. For instance::
# Create the destination directory before you copy things down
file { \"/usr/local/scripts\":
ensure => directory
}
file { \"/usr/local/scripts/myscript\":
source => \"puppet://server/module/myscript\",
mode => 755,
require => File[\"/usr/local/scripts\"]
}
Multiple dependencies can be specified by providing a comma-seperated list
of resources, enclosed in square brackets::
require => [ File[\"/usr/local\"], File[\"/usr/local/scripts\"] ]
Note that Puppet will autorequire everything that it can, and
there are hooks in place so that it's easy for resources to add new
ways to autorequire objects, so if you think Puppet could be
smarter here, let us know.
In fact, the above code was redundant -- Puppet will autorequire
any parent directories that are being managed; it will
automatically realize that the parent directory should be created
before the script is pulled down.
Currently, exec resources will autorequire their CWD (if it is
specified) plus any fully qualified paths that appear in the
command. For instance, if you had an ``exec`` command that ran
the ``myscript`` mentioned above, the above code that pulls the
file down would be automatically listed as a requirement to the
``exec`` code, so that you would always be running againts the
most recent version.
"
end
newmetaparam(:subscribe, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :ALL_EVENTS, :callback => :refresh}) do
desc "One or more objects that this object depends on. Changes in the
subscribed to objects result in the dependent objects being
refreshed (e.g., a service will get restarted). For instance::
class nagios {
file { \"/etc/nagios/nagios.conf\":
source => \"puppet://server/module/nagios.conf\",
alias => nagconf # just to make things easier for me
}
service { nagios:
running => true,
subscribe => File[nagconf]
}
}
Currently the ``exec``, ``mount`` and ``service`` type support
refreshing.
"
end
newmetaparam(:before, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :NONE}) do
desc %{This parameter is the opposite of **require** -- it guarantees
that the specified object is applied later than the specifying
object::
file { "/var/nagios/configuration":
source => "...",
recurse => true,
before => Exec["nagios-rebuid"]
}
exec { "nagios-rebuild":
command => "/usr/bin/make",
cwd => "/var/nagios/configuration"
}
This will make sure all of the files are up to date before the
make command is run.}
end
newmetaparam(:notify, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :ALL_EVENTS, :callback => :refresh}) do
desc %{This parameter is the opposite of **subscribe** -- it sends events
to the specified object::
file { "/etc/sshd_config":
source => "....",
notify => Service[sshd]
}
service { sshd:
ensure => running
}
This will restart the sshd service if the sshd config file changes.}
end
###############################
# All of the provider plumbing for the resource types.
require 'puppet/provider'
require 'puppet/util/provider_features'
# Add the feature handling module.
extend Puppet::Util::ProviderFeatures
attr_reader :provider
# the Type class attribute accessors
class << self
attr_accessor :providerloader
attr_writer :defaultprovider
end
# Find the default provider.
def self.defaultprovider
unless defined? @defaultprovider and @defaultprovider
suitable = suitableprovider()
# Find which providers are a default for this system.
defaults = suitable.find_all { |provider| provider.default? }
# If we don't have any default we use suitable providers
defaults = suitable if defaults.empty?
max = defaults.collect { |provider| provider.specificity }.max
defaults = defaults.find_all { |provider| provider.specificity == max }
retval = nil
if defaults.length > 1
Puppet.warning(
"Found multiple default providers for %s: %s; using %s" %
[self.name, defaults.collect { |i| i.name.to_s }.join(", "),
defaults[0].name]
)
retval = defaults.shift
elsif defaults.length == 1
retval = defaults.shift
else
raise Puppet::DevError, "Could not find a default provider for %s" %
self.name
end
@defaultprovider = retval
end
return @defaultprovider
end
def self.provider_hash_by_type(type)
@provider_hashes ||= {}
@provider_hashes[type] ||= {}
end
def self.provider_hash
Puppet::Type.provider_hash_by_type(self.name)
end
# Retrieve a provider by name.
def self.provider(name)
name = Puppet::Util.symbolize(name)
# If we don't have it yet, try loading it.
unless provider_hash.has_key?(name)
@providerloader.load(name)
end
return provider_hash[name]
end
# Just list all of the providers.
def self.providers
provider_hash.keys
end
def self.validprovider?(name)
name = Puppet::Util.symbolize(name)
return (provider_hash.has_key?(name) && provider_hash[name].suitable?)
end
# Create a new provider of a type. This method must be called
# directly on the type that it's implementing.
def self.provide(name, options = {}, &block)
name = Puppet::Util.symbolize(name)
if obj = provider_hash[name]
Puppet.debug "Reloading %s %s provider" % [name, self.name]
unprovide(name)
end
parent = if pname = options[:parent]
options.delete(:parent)
if pname.is_a? Class
pname
else
if provider = self.provider(pname)
provider
else
raise Puppet::DevError,
"Could not find parent provider %s of %s" %
[pname, name]
end
end
else
Puppet::Provider
end
options[:resource_type] ||= self
self.providify
provider = genclass(name,
:parent => parent,
:hash => provider_hash,
:prefix => "Provider",
:block => block,
:include => feature_module,
:extend => feature_module,
:attributes => options
)
return provider
end
# Make sure we have a :provider parameter defined. Only gets called if there
# are providers.
def self.providify
return if @paramhash.has_key? :provider
newparam(:provider) do
desc "The specific backend for #{self.name.to_s} to use. You will
seldom need to specify this -- Puppet will usually discover the
appropriate provider for your platform."
# This is so we can refer back to the type to get a list of
# providers for documentation.
class << self
attr_accessor :parenttype
end
# We need to add documentation for each provider.
def self.doc
@doc + " Available providers are:\n\n" + parenttype().providers.sort { |a,b|
a.to_s <=> b.to_s
}.collect { |i|
"* **%s**: %s" % [i, parenttype().provider(i).doc]
}.join("\n")
end
defaultto {
@resource.class.defaultprovider.name
}
validate do |provider_class|
provider_class = provider_class[0] if provider_class.is_a? Array
if provider_class.is_a?(Puppet::Provider)
provider_class = provider_class.class.name
end
unless provider = @resource.class.provider(provider_class)
raise ArgumentError, "Invalid %s provider '%s'" % [@resource.class.name, provider_class]
end
end
munge do |provider|
provider = provider[0] if provider.is_a? Array
if provider.is_a? String
provider = provider.intern
end
@resource.provider = provider
if provider.is_a?(Puppet::Provider)
provider.class.name
else
provider
end
end
end.parenttype = self
end
def self.unprovide(name)
if provider_hash.has_key? name
rmclass(name,
:hash => provider_hash,
:prefix => "Provider"
)
if @defaultprovider and @defaultprovider.name == name
@defaultprovider = nil
end
end
end
# Return an array of all of the suitable providers.
def self.suitableprovider
if provider_hash.empty?
providerloader.loadall
end
provider_hash.find_all { |name, provider|
provider.suitable?
}.collect { |name, provider|
provider
}.reject { |p| p.name == :fake } # For testing
end
def provider=(name)
if name.is_a?(Puppet::Provider)
@provider = name
@provider.resource = self
elsif klass = self.class.provider(name)
@provider = klass.new(self)
else
raise ArgumentError, "Could not find %s provider of %s" %
[name, self.class.name]
end
end
###############################
# All of the relationship code.
# Specify a block for generating a list of objects to autorequire. This
# makes it so that you don't have to manually specify things that you clearly
# require.
def self.autorequire(name, &block)
@autorequires ||= {}
@autorequires[name] = block
end
# Yield each of those autorequires in turn, yo.
def self.eachautorequire
@autorequires ||= {}
@autorequires.each { |type, block|
yield(type, block)
}
end
# Figure out of there are any objects we can automatically add as
# dependencies.
def autorequire(rel_catalog = nil)
rel_catalog ||= catalog
raise(Puppet::DevError, "You cannot add relationships without a catalog") unless rel_catalog
reqs = []
self.class.eachautorequire { |type, block|
# Ignore any types we can't find, although that would be a bit odd.
next unless typeobj = Puppet::Type.type(type)
# Retrieve the list of names from the block.
next unless list = self.instance_eval(&block)
unless list.is_a?(Array)
list = [list]
end
# Collect the current prereqs
list.each { |dep|
obj = nil
# Support them passing objects directly, to save some effort.
unless dep.is_a? Puppet::Type
# Skip autorequires that we aren't managing
unless dep = rel_catalog.resource(type, dep)
next
end
end
reqs << Puppet::Relationship.new(dep, self)
}
}
return reqs
end
# Build the dependencies associated with an individual object.
def builddepends
# Handle the requires
self.class.relationship_params.collect do |klass|
if param = @parameters[klass.name]
param.to_edges
end
end.flatten.reject { |r| r.nil? }
end
###############################
# All of the scheduling code.
# Look up the schedule and set it appropriately. This is done after
# the instantiation phase, so that the schedule can be anywhere in the
# file.
def schedule
unless catalog
warning "Cannot schedule without a schedule-containing catalog"
return nil
end
unless defined? @schedule
if name = self[:schedule]
if sched = catalog.resource(:schedule, name)
@schedule = sched
else
self.fail "Could not find schedule %s" % name
end
else
@schedule = nil
end
end
@schedule
end
# Check whether we are scheduled to run right now or not.
def scheduled?
return true if Puppet[:ignoreschedules]
return true unless schedule = self.schedule
# We use 'checked' here instead of 'synced' because otherwise we'll
# end up checking most resources most times, because they will generally
# have been synced a long time ago (e.g., a file only gets updated
# once a month on the server and its schedule is daily; the last sync time
# will have been a month ago, so we'd end up checking every run).
return schedule.match?(self.cached(:checked).to_i)
end
# Define the initial list of tags.
def tags=(list)
tag(self.class.name)
tag(*list)
end
# Types (which map to resources in the languages) are entirely composed of
# attribute value pairs. Generally, Puppet calls any of these things an
# 'attribute', but these attributes always take one of three specific
# forms: parameters, metaparams, or properties.
# In naming methods, I have tried to consistently name the method so
# that it is clear whether it operates on all attributes (thus has 'attr' in
# the method name, or whether it operates on a specific type of attributes.
attr_writer :title
attr_writer :noop
include Enumerable
# class methods dealing with Type management
public
# the Type class attribute accessors
class << self
attr_reader :name
attr_accessor :self_refresh
include Enumerable, Puppet::Util::ClassGen
include Puppet::MetaType::Manager
include Puppet::Util
include Puppet::Util::Logging
end
# all of the variables that must be initialized for each subclass
def self.initvars
# all of the instances of this class
@objects = Hash.new
@aliases = Hash.new
@defaults = {}
unless defined? @parameters
@parameters = []
end
@validproperties = {}
@properties = []
@parameters = []
@paramhash = {}
@attr_aliases = {}
@paramdoc = Hash.new { |hash,key|
if key.is_a?(String)
key = key.intern
end
if hash.include?(key)
hash[key]
else
"Param Documentation for %s not found" % key
end
}
unless defined? @doc
@doc = ""
end
end
def self.to_s
if defined? @name
"Puppet::Type::" + @name.to_s.capitalize
else
super
end
end
# Create a block to validate that our object is set up entirely. This will
# be run before the object is operated on.
def self.validate(&block)
define_method(:validate, &block)
#@validate = block
end
# The catalog that this resource is stored in.
attr_accessor :catalog
# is the resource exported
attr_accessor :exported
# is the resource virtual (it should not :-))
attr_accessor :virtual
# create a log at specified level
def log(msg)
Puppet::Util::Log.create(
:level => @parameters[:loglevel].value,
:message => msg,
:source => self
)
end
# instance methods related to instance intrinsics
# e.g., initialize() and name()
public
attr_reader :original_parameters
# initialize the type instance
def initialize(resource)
raise Puppet::DevError, "Got TransObject instead of Resource or hash" if resource.is_a?(Puppet::TransObject)
resource = self.class.hash2resource(resource) unless resource.is_a?(Puppet::Resource)
# The list of parameter/property instances.
@parameters = {}
# Set the title first, so any failures print correctly.
if resource.type.to_s.downcase.to_sym == self.class.name
self.title = resource.title
else
# This should only ever happen for components
self.title = resource.ref
end
[:file, :line, :catalog, :exported, :virtual].each do |getter|
setter = getter.to_s + "="
if val = resource.send(getter)
self.send(setter, val)
end
end
@tags = resource.tags
@original_parameters = resource.to_hash
set_name(@original_parameters)
set_default(:provider)
set_parameters(@original_parameters)
self.validate if self.respond_to?(:validate)
end
private
# Set our resource's name.
def set_name(hash)
n = self.class.namevar
self[n] = hash[n]
hash.delete(n)
end
# Set all of the parameters from a hash, in the appropriate order.
def set_parameters(hash)
# Use the order provided by allattrs, but add in any
# extra attributes from the resource so we get failures
# on invalid attributes.
no_values = []
(self.class.allattrs + hash.keys).uniq.each do |attr|
begin
# Set any defaults immediately. This is mostly done so
# that the default provider is available for any other
# property validation.
if hash.has_key?(attr)
self[attr] = hash[attr]
else
no_values << attr
end
rescue ArgumentError, Puppet::Error, TypeError
raise
rescue => detail
error = Puppet::DevError.new( "Could not set %s on %s: %s" % [attr, self.class.name, detail])
error.set_backtrace(detail.backtrace)
raise error
end
end
no_values.each do |attr|
set_default(attr)
end
end
public
# Set up all of our autorequires.
def finish
# Scheduling has to be done when the whole config is instantiated, so
# that file order doesn't matter in finding them.
self.schedule
# Make sure all of our relationships are valid. Again, must be done
# when the entire catalog is instantiated.
self.class.relationship_params.collect do |klass|
if param = @parameters[klass.name]
param.validate_relationship
end
end.flatten.reject { |r| r.nil? }
end
# Return a cached value
def cached(name)
Puppet::Util::Storage.cache(self)[name]
#@cache[name] ||= nil
end
# Cache a value
def cache(name, value)
Puppet::Util::Storage.cache(self)[name] = value
#@cache[name] = value
end
# For now, leave the 'name' method functioning like it used to. Once 'title'
# works everywhere, I'll switch it.
def name
return self[:name]
end
# Look up our parent in the catalog, if we have one.
def parent
return nil unless catalog
unless defined?(@parent)
if parents = catalog.adjacent(self, :direction => :in)
# We should never have more than one parent, so let's just ignore
# it if we happen to.
@parent = parents.shift
else
@parent = nil
end
end
@parent
end
# Return the "type[name]" style reference.
def ref
"%s[%s]" % [self.class.name.to_s.capitalize, self.title]
end
def self_refresh?
self.class.self_refresh
end
# Mark that we're purging.
def purging
@purging = true
end
# Is this resource being purged? Used by transactions to forbid
# deletion when there are dependencies.
def purging?
if defined? @purging
@purging
else
false
end
end
# Retrieve the title of an object. If no title was set separately,
# then use the object's name.
def title
unless defined? @title and @title
namevar = self.class.namevar
if self.class.validparameter?(namevar)
@title = self[:name]
elsif self.class.validproperty?(namevar)
@title = self.should(namevar)
else
self.devfail "Could not find namevar %s for %s" %
[namevar, self.class.name]
end
end
return @title
end
# convert to a string
def to_s
self.ref
end
# Convert to a transportable object
def to_trans(ret = true)
trans = TransObject.new(self.title, self.class.name)
values = retrieve()
values.each do |name, value|
trans[name.name] = value
end
@parameters.each do |name, param|
# Avoid adding each instance name as both the name and the namevar
next if param.class.isnamevar? and param.value == self.title
# We've already got property values
next if param.is_a?(Puppet::Property)
trans[name] = param.value
end
trans.tags = self.tags
# FIXME I'm currently ignoring 'parent' and 'path'
return trans
end
+ def to_resource
+ # this 'type instance' versus 'resource' distinction seems artificial
+ # I'd like to see it collapsed someday ~JW
+ self.to_trans.to_resource
+ end
+
%w{exported virtual}.each do |m|
define_method(m+"?") do
self.send(m)
end
end
end # Puppet::Type
end
require 'puppet/provider'
# Always load these types.
require 'puppet/type/component'
diff --git a/lib/puppet/util/settings.rb b/lib/puppet/util/settings.rb
index 08d42d913..ad1b947b3 100644
--- a/lib/puppet/util/settings.rb
+++ b/lib/puppet/util/settings.rb
@@ -1,965 +1,964 @@
require 'puppet'
require 'sync'
-require 'puppet/transportable'
require 'getoptlong'
require 'puppet/external/event-loop'
require 'puppet/util/cacher'
require 'puppet/util/loadedfile'
# The class for handling configuration files.
class Puppet::Util::Settings
include Enumerable
include Puppet::Util::Cacher
require 'puppet/util/settings/setting'
require 'puppet/util/settings/file_setting'
require 'puppet/util/settings/boolean_setting'
attr_accessor :file
attr_reader :timer
# Retrieve a config value
def [](param)
value(param)
end
# Set a config value. This doesn't set the defaults, it sets the value itself.
def []=(param, value)
set_value(param, value, :memory)
end
# Generate the list of valid arguments, in a format that GetoptLong can
# understand, and add them to the passed option list.
def addargs(options)
# Add all of the config parameters as valid options.
self.each { |name, setting|
setting.getopt_args.each { |args| options << args }
}
return options
end
# Generate the list of valid arguments, in a format that OptionParser can
# understand, and add them to the passed option list.
def optparse_addargs(options)
# Add all of the config parameters as valid options.
self.each { |name, setting|
options << setting.optparse_args
}
return options
end
# Is our parameter a boolean parameter?
def boolean?(param)
param = param.to_sym
if @config.include?(param) and @config[param].kind_of? BooleanSetting
return true
else
return false
end
end
# Remove all set values, potentially skipping cli values.
def clear(exceptcli = false)
@sync.synchronize do
unsafe_clear(exceptcli)
end
end
# Remove all set values, potentially skipping cli values.
def unsafe_clear(exceptcli = false)
@values.each do |name, values|
@values.delete(name) unless exceptcli and name == :cli
end
# Don't clear the 'used' in this case, since it's a config file reparse,
# and we want to retain this info.
unless exceptcli
@used = []
end
@cache.clear
@name = nil
end
# This is mostly just used for testing.
def clearused
@cache.clear
@used = []
end
# Do variable interpolation on the value.
def convert(value, environment = nil)
return value unless value
return value unless value.is_a? String
newval = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |value|
varname = $2 || $1
if varname == "environment" and environment
environment
elsif pval = self.value(varname)
pval
else
raise Puppet::DevError, "Could not find value for %s" % value
end
end
return newval
end
# Return a value's description.
def description(name)
if obj = @config[name.to_sym]
obj.desc
else
nil
end
end
def each
@config.each { |name, object|
yield name, object
}
end
# Iterate over each section name.
def eachsection
yielded = []
@config.each do |name, object|
section = object.section
unless yielded.include? section
yield section
yielded << section
end
end
end
# Return an object by name.
def setting(param)
param = param.to_sym
@config[param]
end
# Handle a command-line argument.
def handlearg(opt, value = nil)
@cache.clear
value = munge_value(value) if value
str = opt.sub(/^--/,'')
bool = true
newstr = str.sub(/^no-/, '')
if newstr != str
str = newstr
bool = false
end
str = str.intern
if @config[str].is_a?(Puppet::Util::Settings::BooleanSetting)
if value == "" or value.nil?
value = bool
end
end
set_value(str, value, :cli)
end
def without_noop
old_noop = value(:noop,:cli) and set_value(:noop, false, :cli) if valid?(:noop)
yield
ensure
set_value(:noop, old_noop, :cli) if valid?(:noop)
end
def include?(name)
name = name.intern if name.is_a? String
@config.include?(name)
end
# check to see if a short name is already defined
def shortinclude?(short)
short = short.intern if name.is_a? String
@shortnames.include?(short)
end
# Create a new collection of config settings.
def initialize
@config = {}
@shortnames = {}
@created = []
@searchpath = nil
# Mutex-like thing to protect @values
@sync = Sync.new
# Keep track of set values.
@values = Hash.new { |hash, key| hash[key] = {} }
# And keep a per-environment cache
@cache = Hash.new { |hash, key| hash[key] = {} }
# A central concept of a name.
@name = nil
# The list of sections we've used.
@used = []
end
# NOTE: ACS ahh the util classes. . .sigh
# as part of a fix for 1183, I pulled the logic for the following 5 methods out of the executables and puppet.rb
# They probably deserve their own class, but I don't want to do that until I can refactor environments
# its a little better than where they were
# Prints the contents of a config file with the available config settings, or it
# prints a single value of a config setting.
def print_config_options
env = value(:environment)
val = value(:configprint)
if val == "all"
hash = {}
each do |name, obj|
val = value(name,env)
val = val.inspect if val == ""
hash[name] = val
end
hash.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, val|
puts "%s = %s" % [name, val]
end
else
val.split(/\s*,\s*/).sort.each do |v|
if include?(v)
#if there is only one value, just print it for back compatibility
if v == val
puts value(val,env)
break
end
puts "%s = %s" % [v, value(v,env)]
else
puts "invalid parameter: %s" % v
return false
end
end
end
true
end
def generate_config
puts to_config
true
end
def generate_manifest
puts to_manifest
true
end
def print_configs
return print_config_options if value(:configprint) != ""
return generate_config if value(:genconfig)
return generate_manifest if value(:genmanifest)
end
def print_configs?
return (value(:configprint) != "" || value(:genconfig) || value(:genmanifest)) && true
end
# Return a given object's file metadata.
def metadata(param)
if obj = @config[param.to_sym] and obj.is_a?(FileSetting)
return [:owner, :group, :mode].inject({}) do |meta, p|
if v = obj.send(p)
meta[p] = v
end
meta
end
else
nil
end
end
# Make a directory with the appropriate user, group, and mode
def mkdir(default)
obj = get_config_file_default(default)
Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do
mode = obj.mode || 0750
Dir.mkdir(obj.value, mode)
end
end
# Figure out our name.
def name
unless @name
unless @config[:name]
return nil
end
searchpath.each do |source|
next if source == :name
@sync.synchronize do
@name = @values[source][:name]
end
break if @name
end
unless @name
@name = convert(@config[:name].default).intern
end
end
@name
end
# Return all of the parameters associated with a given section.
def params(section = nil)
if section
section = section.intern if section.is_a? String
@config.find_all { |name, obj|
obj.section == section
}.collect { |name, obj|
name
}
else
@config.keys
end
end
# Parse the configuration file. Just provides
# thread safety.
def parse
raise "No :config setting defined; cannot parse unknown config file" unless self[:config]
# Create a timer so that this file will get checked automatically
# and reparsed if necessary.
set_filetimeout_timer()
@sync.synchronize do
unsafe_parse(self[:config])
end
end
# Unsafely parse the file -- this isn't thread-safe and causes plenty of problems if used directly.
def unsafe_parse(file)
return unless FileTest.exist?(file)
begin
data = parse_file(file)
rescue => details
puts details.backtrace if Puppet[:trace]
Puppet.err "Could not parse #{file}: #{details}"
return
end
unsafe_clear(true)
data.each do |area, values|
@values[area] = values
end
# Determine our environment, if we have one.
if @config[:environment]
env = self.value(:environment).to_sym
else
env = "none"
end
# Call any hooks we should be calling.
settings_with_hooks.each do |setting|
each_source(env) do |source|
if value = @values[source][setting.name]
# We still have to use value() to retrieve the value, since
# we want the fully interpolated value, not $vardir/lib or whatever.
# This results in extra work, but so few of the settings
# will have associated hooks that it ends up being less work this
# way overall.
setting.handle(self.value(setting.name, env))
break
end
end
end
# We have to do it in the reverse of the search path,
# because multiple sections could set the same value
# and I'm too lazy to only set the metadata once.
searchpath.reverse.each do |source|
if meta = @values[source][:_meta]
set_metadata(meta)
end
end
end
# Create a new setting. The value is passed in because it's used to determine
# what kind of setting we're creating, but the value itself might be either
# a default or a value, so we can't actually assign it.
def newsetting(hash)
klass = nil
if hash[:section]
hash[:section] = hash[:section].to_sym
end
if type = hash[:type]
unless klass = {:setting => Setting, :file => FileSetting, :boolean => BooleanSetting}[type]
raise ArgumentError, "Invalid setting type '%s'" % type
end
hash.delete(:type)
else
case hash[:default]
when true, false, "true", "false"
klass = BooleanSetting
when /^\$\w+\//, /^\//
klass = FileSetting
when String, Integer, Float # nothing
klass = Setting
else
raise Puppet::Error, "Invalid value '%s' for %s" % [value.inspect, hash[:name]]
end
end
hash[:settings] = self
setting = klass.new(hash)
return setting
end
# This has to be private, because it doesn't add the settings to @config
private :newsetting
# Iterate across all of the objects in a given section.
def persection(section)
section = section.to_sym
self.each { |name, obj|
if obj.section == section
yield obj
end
}
end
# Cache this in an easily clearable way, since we were
# having trouble cleaning it up after tests.
cached_attr(:file) do
if path = self[:config] and FileTest.exist?(path)
Puppet::Util::LoadedFile.new(path)
end
end
# Reparse our config file, if necessary.
def reparse
if file and file.changed?
Puppet.notice "Reparsing %s" % file.file
parse
reuse()
end
end
def reuse
return unless defined? @used
@sync.synchronize do # yay, thread-safe
new = @used
@used = []
self.use(*new)
end
end
# The order in which to search for values.
def searchpath(environment = nil)
if environment
[:cli, :memory, environment, :name, :main]
else
[:cli, :memory, :name, :main]
end
end
# Get a list of objects per section
def sectionlist
sectionlist = []
self.each { |name, obj|
section = obj.section || "puppet"
sections[section] ||= []
unless sectionlist.include?(section)
sectionlist << section
end
sections[section] << obj
}
return sectionlist, sections
end
def service_user_available?
return @service_user_available if defined?(@service_user_available)
return @service_user_available = false unless user_name = self[:user]
user = Puppet::Type.type(:user).new :name => self[:user], :check => :ensure
return @service_user_available = user.exists?
end
def set_value(param, value, type)
param = param.to_sym
unless setting = @config[param]
raise ArgumentError,
"Attempt to assign a value to unknown configuration parameter %s" % param.inspect
end
if setting.respond_to?(:munge)
value = setting.munge(value)
end
if setting.respond_to?(:handle)
setting.handle(value)
end
# Reset the name, so it's looked up again.
if param == :name
@name = nil
end
@sync.synchronize do # yay, thread-safe
@values[type][param] = value
@cache.clear
clearused
# Clear the list of environments, because they cache, at least, the module path.
# We *could* preferentially just clear them if the modulepath is changed,
# but we don't really know if, say, the vardir is changed and the modulepath
# is defined relative to it. We need the defined? stuff because of loading
# order issues.
Puppet::Node::Environment.clear if defined?(Puppet::Node) and defined?(Puppet::Node::Environment)
end
return value
end
private :set_value
# Set a bunch of defaults in a given section. The sections are actually pretty
# pointless, but they help break things up a bit, anyway.
def setdefaults(section, defs)
section = section.to_sym
call = []
defs.each { |name, hash|
if hash.is_a? Array
unless hash.length == 2
raise ArgumentError, "Defaults specified as an array must contain only the default value and the decription"
end
tmp = hash
hash = {}
[:default, :desc].zip(tmp).each { |p,v| hash[p] = v }
end
name = name.to_sym
hash[:name] = name
hash[:section] = section
if @config.include?(name)
raise ArgumentError, "Parameter %s is already defined" % name
end
tryconfig = newsetting(hash)
if short = tryconfig.short
if other = @shortnames[short]
raise ArgumentError, "Parameter %s is already using short name '%s'" % [other.name, short]
end
@shortnames[short] = tryconfig
end
@config[name] = tryconfig
# Collect the settings that need to have their hooks called immediately.
# We have to collect them so that we can be sure we're fully initialized before
# the hook is called.
call << tryconfig if tryconfig.call_on_define
}
call.each { |setting| setting.handle(self.value(setting.name)) }
end
# Create a timer to check whether the file should be reparsed.
def set_filetimeout_timer
return unless timeout = self[:filetimeout] and timeout = Integer(timeout) and timeout > 0
timer = EventLoop::Timer.new(:interval => timeout, :tolerance => 1, :start? => true) { self.reparse() }
end
# Convert the settings we manage into a catalog full of resources that model those settings.
# We currently have to go through Trans{Object,Bucket} instances,
# because this hasn't been ported yet.
def to_catalog(*sections)
sections = nil if sections.empty?
catalog = Puppet::Resource::Catalog.new("Settings")
@config.values.find_all { |value| value.is_a?(FileSetting) }.each do |file|
next unless (sections.nil? or sections.include?(file.section))
next unless resource = file.to_resource
next if catalog.resource(resource.ref)
catalog.add_resource(resource)
end
add_user_resources(catalog, sections)
catalog
end
# Convert our list of config settings into a configuration file.
def to_config
str = %{The configuration file for #{Puppet[:name]}. Note that this file
is likely to have unused configuration parameters in it; any parameter that's
valid anywhere in Puppet can be in any config file, even if it's not used.
Every section can specify three special parameters: owner, group, and mode.
These parameters affect the required permissions of any files specified after
their specification. Puppet will sometimes use these parameters to check its
own configured state, so they can be used to make Puppet a bit more self-managing.
Generated on #{Time.now}.
}.gsub(/^/, "# ")
# Add a section heading that matches our name.
if @config.include?(:name)
str += "[%s]\n" % self[:name]
end
eachsection do |section|
persection(section) do |obj|
str += obj.to_config + "\n"
end
end
return str
end
# Convert to a parseable manifest
def to_manifest
catalog = to_catalog
# The resource list is a list of references, not actual instances.
catalog.resources.collect do |ref|
catalog.resource(ref).to_manifest
end.join("\n\n")
end
# Create the necessary objects to use a section. This is idempotent;
# you can 'use' a section as many times as you want.
def use(*sections)
sections = sections.collect { |s| s.to_sym }
@sync.synchronize do # yay, thread-safe
sections = sections.reject { |s| @used.include?(s) }
return if sections.empty?
begin
catalog = to_catalog(*sections).to_ral
rescue => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "Could not create resources for managing Puppet's files and directories in sections %s: %s" % [sections.inspect, detail]
# We need some way to get rid of any resources created during the catalog creation
# but not cleaned up.
return
end
without_noop do
catalog.host_config = false
catalog.apply do |transaction|
if transaction.any_failed?
report = transaction.report
failures = report.logs.find_all { |log| log.level == :err }
raise "Got %s failure(s) while initializing: %s" % [failures.length, failures.collect { |l| l.to_s }.join("; ")]
end
end
end
sections.each { |s| @used << s }
@used.uniq!
end
end
def valid?(param)
param = param.to_sym
@config.has_key?(param)
end
def uninterpolated_value(param, environment = nil)
param = param.to_sym
environment = environment.to_sym if environment
# See if we can find it within our searchable list of values
val = catch :foundval do
each_source(environment) do |source|
# Look for the value. We have to test the hash for whether
# it exists, because the value might be false.
@sync.synchronize do
if @values[source].include?(param)
throw :foundval, @values[source][param]
end
end
end
throw :foundval, nil
end
# If we didn't get a value, use the default
val = @config[param].default if val.nil?
return val
end
# Find the correct value using our search path. Optionally accept an environment
# in which to search before the other configuration sections.
def value(param, environment = nil)
param = param.to_sym
environment = environment.to_sym if environment
# Short circuit to nil for undefined parameters.
return nil unless @config.include?(param)
# Yay, recursion.
#self.reparse() unless [:config, :filetimeout].include?(param)
# Check the cache first. It needs to be a per-environment
# cache so that we don't spread values from one env
# to another.
if cached = @cache[environment||"none"][param]
return cached
end
val = uninterpolated_value(param, environment)
# Convert it if necessary
val = convert(val, environment)
# And cache it
@cache[environment||"none"][param] = val
return val
end
# Open a file with the appropriate user, group, and mode
def write(default, *args, &bloc)
obj = get_config_file_default(default)
writesub(default, value(obj.name), *args, &bloc)
end
# Open a non-default file under a default dir with the appropriate user,
# group, and mode
def writesub(default, file, *args, &bloc)
obj = get_config_file_default(default)
chown = nil
if Puppet::Util::SUIDManager.uid == 0
chown = [obj.owner, obj.group]
else
chown = [nil, nil]
end
Puppet::Util::SUIDManager.asuser(*chown) do
mode = obj.mode || 0640
if args.empty?
args << "w"
end
args << mode
# Update the umask to make non-executable files
Puppet::Util.withumask(File.umask ^ 0111) do
File.open(file, *args) do |file|
yield file
end
end
end
end
def readwritelock(default, *args, &bloc)
file = value(get_config_file_default(default).name)
tmpfile = file + ".tmp"
sync = Sync.new
unless FileTest.directory?(File.dirname(tmpfile))
raise Puppet::DevError, "Cannot create %s; directory %s does not exist" %
[file, File.dirname(file)]
end
sync.synchronize(Sync::EX) do
File.open(file, ::File::CREAT|::File::RDWR, 0600) do |rf|
rf.lock_exclusive do
if File.exist?(tmpfile)
raise Puppet::Error, ".tmp file already exists for %s; Aborting locked write. Check the .tmp file and delete if appropriate" %
[file]
end
# If there's a failure, remove our tmpfile
begin
writesub(default, tmpfile, *args, &bloc)
rescue
File.unlink(tmpfile) if FileTest.exist?(tmpfile)
raise
end
begin
File.rename(tmpfile, file)
rescue => detail
Puppet.err "Could not rename %s to %s: %s" % [file, tmpfile, detail]
File.unlink(tmpfile) if FileTest.exist?(tmpfile)
end
end
end
end
end
private
def get_config_file_default(default)
obj = nil
unless obj = @config[default]
raise ArgumentError, "Unknown default %s" % default
end
unless obj.is_a? FileSetting
raise ArgumentError, "Default %s is not a file" % default
end
return obj
end
# Create the transportable objects for users and groups.
def add_user_resources(catalog, sections)
return unless Puppet.features.root?
return unless self[:mkusers]
@config.each do |name, setting|
next unless setting.respond_to?(:owner)
next unless sections.nil? or sections.include?(setting.section)
if user = setting.owner and user != "root" and catalog.resource(:user, user).nil?
resource = Puppet::Resource.new(:user, user, :ensure => :present)
if self[:group]
resource[:gid] = self[:group]
end
catalog.add_resource resource
end
if group = setting.group and ! %w{root wheel}.include?(group) and catalog.resource(:group, group).nil?
catalog.add_resource Puppet::Resource.new(:group, group, :ensure => :present)
end
end
end
# Yield each search source in turn.
def each_source(environment)
searchpath(environment).each do |source|
# Modify the source as necessary.
source = self.name if source == :name
yield source
end
end
# Return all settings that have associated hooks; this is so
# we can call them after parsing the configuration file.
def settings_with_hooks
@config.values.find_all { |setting| setting.respond_to?(:handle) }
end
# Extract extra setting information for files.
def extract_fileinfo(string)
result = {}
value = string.sub(/\{\s*([^}]+)\s*\}/) do
params = $1
params.split(/\s*,\s*/).each do |str|
if str =~ /^\s*(\w+)\s*=\s*([\w\d]+)\s*$/
param, value = $1.intern, $2
result[param] = value
unless [:owner, :mode, :group].include?(param)
raise ArgumentError, "Invalid file option '%s'" % param
end
if param == :mode and value !~ /^\d+$/
raise ArgumentError, "File modes must be numbers"
end
else
raise ArgumentError, "Could not parse '%s'" % string
end
end
''
end
result[:value] = value.sub(/\s*$/, '')
return result
end
# Convert arguments into booleans, integers, or whatever.
def munge_value(value)
# Handle different data types correctly
return case value
when /^false$/i; false
when /^true$/i; true
when /^\d+$/i; Integer(value)
when true; true
when false; false
else
value.gsub(/^["']|["']$/,'').sub(/\s+$/, '')
end
end
# This is an abstract method that just turns a file in to a hash of hashes.
# We mostly need this for backward compatibility -- as of May 2007 we need to
# support parsing old files with any section, or new files with just two
# valid sections.
def parse_file(file)
text = read_file(file)
result = Hash.new { |names, name|
names[name] = {}
}
count = 0
# Default to 'main' for the section.
section = :main
result[section][:_meta] = {}
text.split(/\n/).each { |line|
count += 1
case line
when /^\s*\[(\w+)\]$/
section = $1.intern # Section names
# Add a meta section
result[section][:_meta] ||= {}
when /^\s*#/; next # Skip comments
when /^\s*$/; next # Skip blanks
when /^\s*(\w+)\s*=\s*(.*)$/ # settings
var = $1.intern
# We don't want to munge modes, because they're specified in octal, so we'll
# just leave them as a String, since Puppet handles that case correctly.
if var == :mode
value = $2
else
value = munge_value($2)
end
# Check to see if this is a file argument and it has extra options
begin
if value.is_a?(String) and options = extract_fileinfo(value)
value = options[:value]
options.delete(:value)
result[section][:_meta][var] = options
end
result[section][var] = value
rescue Puppet::Error => detail
detail.file = file
detail.line = line
raise
end
else
error = Puppet::Error.new("Could not match line %s" % line)
error.file = file
error.line = line
raise error
end
}
return result
end
# Read the file in.
def read_file(file)
begin
return File.read(file)
rescue Errno::ENOENT
raise ArgumentError, "No such file %s" % file
rescue Errno::EACCES
raise ArgumentError, "Permission denied to file %s" % file
end
end
# Set file metadata.
def set_metadata(meta)
meta.each do |var, values|
values.each do |param, value|
@config[var].send(param.to_s + "=", value)
end
end
end
end
diff --git a/spec/unit/application/resource.rb b/spec/unit/application/resource.rb
index 98b4485ed..9d47ba56e 100755
--- a/spec/unit/application/resource.rb
+++ b/spec/unit/application/resource.rb
@@ -1,253 +1,254 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
require 'puppet/application/resource'
describe "resource" do
before :each do
@resource = Puppet::Application[:resource]
Puppet::Util::Log.stubs(:newdestination)
Puppet::Util::Log.stubs(:level=)
end
it "should ask Puppet::Application to not parse Puppet configuration file" do
@resource.should_parse_config?.should be_false
end
it "should declare a main command" do
@resource.should respond_to(:main)
end
it "should declare a host option" do
@resource.should respond_to(:handle_host)
end
it "should declare a types option" do
@resource.should respond_to(:handle_types)
end
it "should declare a param option" do
@resource.should respond_to(:handle_param)
end
it "should declare a preinit block" do
@resource.should respond_to(:run_preinit)
end
describe "in preinit" do
it "should set hosts to nil" do
@resource.run_preinit
@resource.host.should be_nil
end
it "should init extra_params to empty array" do
@resource.run_preinit
@resource.extra_params.should == []
end
it "should load Facter facts" do
Facter.expects(:loadfacts).once
@resource.run_preinit
end
end
describe "when handling options" do
[:debug, :verbose, :edit].each do |option|
it "should declare handle_#{option} method" do
@resource.should respond_to("handle_#{option}".to_sym)
end
it "should store argument value when calling handle_#{option}" do
@resource.options.expects(:[]=).with(option, 'arg')
@resource.send("handle_#{option}".to_sym, 'arg')
end
end
it "should set options[:host] to given host" do
@resource.handle_host(:whatever)
@resource.host.should == :whatever
end
it "should load an display all types with types option" do
type1 = stub_everything 'type1', :name => :type1
type2 = stub_everything 'type2', :name => :type2
Puppet::Type.stubs(:loadall)
Puppet::Type.stubs(:eachtype).multiple_yields(type1,type2)
@resource.stubs(:exit)
@resource.expects(:puts).with(['type1','type2'])
@resource.handle_types(nil)
end
it "should add param to extra_params list" do
@resource.extra_params = [ :param1 ]
@resource.handle_param("whatever")
@resource.extra_params.should == [ :param1, :whatever ]
end
end
describe "during setup" do
before :each do
Puppet::Log.stubs(:newdestination)
Puppet::Log.stubs(:level=)
Puppet.stubs(:parse_config)
end
it "should set console as the log destination" do
Puppet::Log.expects(:newdestination).with(:console)
@resource.run_setup
end
it "should set log level to debug if --debug was passed" do
@resource.options.stubs(:[]).with(:debug).returns(true)
Puppet::Log.expects(:level=).with(:debug)
@resource.run_setup
end
it "should set log level to info if --verbose was passed" do
@resource.options.stubs(:[]).with(:debug).returns(false)
@resource.options.stubs(:[]).with(:verbose).returns(true)
Puppet::Log.expects(:level=).with(:info)
@resource.run_setup
end
it "should Parse puppet config" do
Puppet.expects(:parse_config)
@resource.run_setup
end
end
describe "when running" do
def set_args(args)
(ARGV.clear << args).flatten!
end
def push_args(*args)
@args_stack ||= []
@args_stack << ARGV.dup
set_args(args)
end
def pop_args
set_args(@args_stack.pop)
end
before :each do
@type = stub_everything 'type', :properties => []
push_args('type')
Puppet::Type.stubs(:type).returns(@type)
end
after :each do
pop_args
end
it "should raise an error if no type is given" do
push_args
lambda { @resource.main }.should raise_error
pop_args
end
it "should raise an error when editing a remote host" do
@resource.options.stubs(:[]).with(:edit).returns(true)
@resource.host = 'host'
lambda { @resource.main }.should raise_error
end
it "should raise an error if the type is not found" do
Puppet::Type.stubs(:type).returns(nil)
lambda { @resource.main }.should raise_error
end
describe "with a host" do
before :each do
@resource.stubs(:puts)
@resource.host = 'host'
- @client = stub_everything 'client'
- @client.stubs(:read_cert).returns(true)
- @client.stubs(:instances).returns([])
- Puppet::Network::Client.resource.stubs(:new).returns(@client)
+
+ Puppet::Resource.stubs(:find ).never
+ Puppet::Resource.stubs(:search).never
+ Puppet::Resource.stubs(:save ).never
end
- it "should connect to it" do
- Puppet::Network::Client.resource.expects(:new).with { |h| h[:Server] == 'host' }.returns(@client)
+ it "should search for resources" do
+ Puppet::Resource.expects(:search).with('https://host:8139/production/resources/type/', {}).returns([])
@resource.main
end
- it "should raise an error if there are no certs" do
- @client.stubs(:read_cert).returns(nil)
-
- lambda { @resource.main }.should raise_error
+ it "should describe the given resource" do
+ push_args('type','name')
+ x = stub_everything 'resource'
+ Puppet::Resource.expects(:find).with('https://host:8139/production/resources/type/name').returns(x)
+ @resource.main
+ pop_args
end
- it "should retrieve all the instances if there is no name" do
- @client.expects(:instances).returns([])
+ it "should add given parameters to the object" do
+ push_args('type','name','param=temp')
- @resource.main
- end
+ res = stub "resource"
+ res.expects(:save).with{|x| x.uri == 'https://host:8139/production/resources/type/name'}.returns(res)
+ res.expects(:collect)
+ res.expects(:to_manifest)
+ Puppet::Resource.expects(:new).with('type', 'name', {'param' => 'temp'}).returns(res)
- it "should describe the given resource" do
- push_args('type','name')
- @client.expects(:describe).returns(stub_everything)
@resource.main
pop_args
end
+
end
describe "without a host" do
before :each do
@resource.stubs(:puts)
@resource.host = nil
+
+ Puppet::Resource.stubs(:find ).never
+ Puppet::Resource.stubs(:search).never
+ Puppet::Resource.stubs(:save ).never
end
- it "should retrieve all the instances if there is no name" do
- @type.expects(:instances).returns([])
+ it "should search for resources" do
+ Puppet::Resource.expects(:search).with('type/', {}).returns([])
+ @resource.main
+ end
+ it "should describe the given resource" do
+ push_args('type','name')
+ x = stub_everything 'resource'
+ Puppet::Resource.expects(:find).with('type/name').returns(x)
@resource.main
+ pop_args
end
- describe 'but with a given name' do
- before :each do
- push_args('type','name')
- @type.stubs(:new).returns(:bob)
- end
-
- after :each do
- pop_args
- end
-
- it "should retrieve a specific instance if it exists" do
- pending
- end
-
- it "should create a stub instance if it doesn't exist" do
- pending
- end
-
- it "should add given parameters to the object" do
- push_args('type','name','param=temp')
- pending
- @object.expects(:[]=).with('param','temp')
- @resource.main
- pop_args
- end
+ it "should add given parameters to the object" do
+ push_args('type','name','param=temp')
+
+ res = stub "resource"
+ res.expects(:save).with{|x| x.uri == nil}.returns(res)
+ res.expects(:collect)
+ res.expects(:to_manifest)
+ Puppet::Resource.expects(:new).with('type', 'name', {'param' => 'temp'}).returns(res)
+
+ @resource.main
+ pop_args
end
+
end
end
end
diff --git a/spec/unit/indirector/resource/ral.rb b/spec/unit/indirector/resource/ral.rb
new file mode 100644
index 000000000..f74bf3d60
--- /dev/null
+++ b/spec/unit/indirector/resource/ral.rb
@@ -0,0 +1,129 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+describe "Puppet::Resource::Ral" do
+ describe "find" do
+ before do
+ @request = stub 'request', :key => "user/root"
+ end
+
+ it "should find an existing instance" do
+ my_resource = stub "my user resource"
+
+ wrong_instance = stub "wrong user", :name => "bob"
+ my_instance = stub "my user", :name => "root", :to_resource => my_resource
+
+ require 'puppet/type/user'
+ Puppet::Type::User.expects(:instances).returns([ wrong_instance, my_instance, wrong_instance ])
+ Puppet::Resource::Ral.new.find(@request).should == my_resource
+ end
+
+ it "if there is no instance, it should create one" do
+ wrong_instance = stub "wrong user", :name => "bob"
+
+ require 'puppet/type/user'
+ Puppet::Type::User.expects(:instances).returns([ wrong_instance, wrong_instance ])
+ result = Puppet::Resource::Ral.new.find(@request)
+ result.should be_is_a Puppet::Resource
+ result.title.should == "root"
+ end
+ end
+
+ describe "search" do
+ before do
+ @request = stub 'request', :key => "user/", :options => {}
+ end
+
+ it "should convert ral resources into regular resources" do
+ my_resource = stub "my user resource"
+ my_instance = stub "my user", :name => "root", :to_resource => my_resource
+
+ require 'puppet/type/user'
+ Puppet::Type::User.expects(:instances).returns([ my_instance ])
+ Puppet::Resource::Ral.new.search(@request).should == [my_resource]
+ end
+
+ it "should filter results by name if there's a name in the key" do
+ my_resource = stub "my user resource"
+ my_resource.stubs(:to_resource).returns(my_resource)
+ my_resource.stubs(:[]).with(:name).returns("root")
+
+ wrong_resource = stub "wrong resource"
+ wrong_resource.stubs(:to_resource).returns(wrong_resource)
+ wrong_resource.stubs(:[]).with(:name).returns("bad")
+
+ my_instance = stub "my user", :to_resource => my_resource
+ wrong_instance = stub "wrong user", :to_resource => wrong_resource
+
+ @request = stub 'request', :key => "user/root", :options => {}
+
+ require 'puppet/type/user'
+ Puppet::Type::User.expects(:instances).returns([ my_instance, wrong_instance ])
+ Puppet::Resource::Ral.new.search(@request).should == [my_resource]
+ end
+
+ it "should filter results by query parameters" do
+ wrong_resource = stub "my user resource"
+ wrong_resource.stubs(:to_resource).returns(wrong_resource)
+ wrong_resource.stubs(:[]).with(:name).returns("root")
+
+ my_resource = stub "wrong resource"
+ my_resource.stubs(:to_resource).returns(my_resource)
+ my_resource.stubs(:[]).with(:name).returns("bob")
+
+ my_instance = stub "my user", :to_resource => my_resource
+ wrong_instance = stub "wrong user", :to_resource => wrong_resource
+
+ @request = stub 'request', :key => "user/", :options => {:name => "bob"}
+
+ require 'puppet/type/user'
+ Puppet::Type::User.expects(:instances).returns([ my_instance, wrong_instance ])
+ Puppet::Resource::Ral.new.search(@request).should == [my_resource]
+ end
+
+ it "should return sorted results" do
+ a_resource = stub "alice resource"
+ a_resource.stubs(:to_resource).returns(a_resource)
+ a_resource.stubs(:title).returns("alice")
+
+ b_resource = stub "bob resource"
+ b_resource.stubs(:to_resource).returns(b_resource)
+ b_resource.stubs(:title).returns("bob")
+
+ a_instance = stub "alice user", :to_resource => a_resource
+ b_instance = stub "bob user", :to_resource => b_resource
+
+ @request = stub 'request', :key => "user/", :options => {}
+
+ require 'puppet/type/user'
+ Puppet::Type::User.expects(:instances).returns([ b_instance, a_instance ])
+ Puppet::Resource::Ral.new.search(@request).should == [a_resource, b_resource]
+ end
+ end
+
+ describe "save" do
+ before do
+ @rebuilt_res = stub 'rebuilt instance'
+ @ral_res = stub 'ral resource', :to_resource => @rebuilt_res
+ @instance = stub 'instance', :to_ral => @ral_res
+ @request = stub 'request', :key => "user/", :instance => @instance
+ @catalog = stub 'catalog'
+
+ Puppet::Resource::Catalog.stubs(:new).returns(@catalog)
+ @catalog.stubs(:apply)
+ @catalog.stubs(:add_resource)
+ end
+
+ it "should apply a new catalog with a ral object in it" do
+ Puppet::Resource::Catalog.expects(:new).returns(@catalog)
+ @catalog.expects(:add_resource).with(@ral_res)
+ @catalog.expects(:apply)
+ Puppet::Resource::Ral.new.save(@request)
+ end
+
+ it "should return a regular resource that used to be the ral resource" do
+ Puppet::Resource::Ral.new.save(@request).should == @rebuilt_res
+ end
+ end
+end
diff --git a/spec/unit/indirector/resource/rest.rb b/spec/unit/indirector/resource/rest.rb
new file mode 100755
index 000000000..d5f2a9d46
--- /dev/null
+++ b/spec/unit/indirector/resource/rest.rb
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+
+Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") }
+
+require 'puppet/indirector/resource/rest'
+
+describe Puppet::Resource::Rest do
+ it "should be a sublcass of Puppet::Indirector::REST" do
+ Puppet::Resource::Rest.superclass.should equal(Puppet::Indirector::REST)
+ end
+end
diff --git a/spec/unit/indirector/rest.rb b/spec/unit/indirector/rest.rb
index d12e3c642..1cb34c4f4 100755
--- a/spec/unit/indirector/rest.rb
+++ b/spec/unit/indirector/rest.rb
@@ -1,411 +1,422 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
require 'puppet/indirector/rest'
describe "a REST http call", :shared => true do
it "should accept a path" do
lambda { @search.send(@method, *@arguments) }.should_not raise_error(ArgumentError)
end
it "should require a path" do
lambda { @searcher.send(@method) }.should raise_error(ArgumentError)
end
it "should return the results of deserializing the response to the request" do
conn = mock 'connection'
conn.stubs(:put).returns @response
conn.stubs(:delete).returns @response
conn.stubs(:get).returns @response
Puppet::Network::HttpPool.stubs(:http_instance).returns conn
@searcher.expects(:deserialize).with(@response).returns "myobject"
@searcher.send(@method, *@arguments).should == 'myobject'
end
end
describe Puppet::Indirector::REST do
before do
Puppet::Indirector::Terminus.stubs(:register_terminus_class)
@model = stub('model', :supported_formats => %w{}, :convert_from => nil)
@instance = stub('model instance', :name= => nil)
@indirection = stub('indirection', :name => :mystuff, :register_terminus_type => nil, :model => @model)
Puppet::Indirector::Indirection.stubs(:instance).returns(@indirection)
@rest_class = Class.new(Puppet::Indirector::REST) do
def self.to_s
"This::Is::A::Test::Class"
end
end
@response = stub('mock response', :body => 'result', :code => "200")
@response.stubs(:[]).with('content-type').returns "text/plain"
@searcher = @rest_class.new
@searcher.stubs(:model).returns @model
end
it "should include the v1 REST API module" do
Puppet::Indirector::REST.ancestors.should be_include(Puppet::Network::HTTP::API::V1)
end
it "should have a method for specifying what setting a subclass should use to retrieve its server" do
@rest_class.should respond_to(:use_server_setting)
end
it "should use any specified setting to pick the server" do
@rest_class.expects(:server_setting).returns :servset
Puppet.settings.expects(:value).with(:servset).returns "myserver"
@rest_class.server.should == "myserver"
end
it "should default to :server for the server setting" do
@rest_class.expects(:server_setting).returns nil
Puppet.settings.expects(:value).with(:server).returns "myserver"
@rest_class.server.should == "myserver"
end
it "should have a method for specifying what setting a subclass should use to retrieve its port" do
@rest_class.should respond_to(:use_port_setting)
end
it "should use any specified setting to pick the port" do
@rest_class.expects(:port_setting).returns :servset
Puppet.settings.expects(:value).with(:servset).returns "321"
@rest_class.port.should == 321
end
it "should default to :port for the port setting" do
@rest_class.expects(:port_setting).returns nil
Puppet.settings.expects(:value).with(:masterport).returns "543"
@rest_class.port.should == 543
end
describe "when deserializing responses" do
it "should return nil if the response code is 404" do
response = mock 'response'
response.expects(:code).returns "404"
@searcher.deserialize(response).should be_nil
end
[300,400,403,405,500,501,502,503,504].each { |rc|
describe "when the response code is #{rc}" do
before :each do
@model.expects(:convert_from).never
@response = mock 'response'
@response.stubs(:code).returns rc.to_s
@response.stubs(:message).returns "There was a problem (header)"
end
it "should fail" do
@response.stubs(:body).returns nil
lambda { @searcher.deserialize(@response) }.should raise_error(Net::HTTPError)
end
it "should take the error message from the body, if present" do
@response.stubs(:body).returns "There was a problem (body)"
lambda { @searcher.deserialize(@response) }.should raise_error(Net::HTTPError,"Error #{rc} on SERVER: There was a problem (body)")
end
it "should take the error message from the response header if the body is empty" do
@response.stubs(:body).returns ""
lambda { @searcher.deserialize(@response) }.should raise_error(Net::HTTPError,"Error #{rc} on SERVER: There was a problem (header)")
end
it "should take the error message from the response header if the body is absent" do
@response.stubs(:body).returns nil
lambda { @searcher.deserialize(@response) }.should raise_error(Net::HTTPError,"Error #{rc} on SERVER: There was a problem (header)")
end
end
}
it "should return the results of converting from the format specified by the content-type header if the response code is in the 200s" do
@model.expects(:convert_from).with("myformat", "mydata").returns "myobject"
response = mock 'response'
response.stubs(:[]).with("content-type").returns "myformat"
response.stubs(:body).returns "mydata"
response.stubs(:code).returns "200"
@searcher.deserialize(response).should == "myobject"
end
it "should convert and return multiple instances if the return code is in the 200s and 'multiple' is specified" do
@model.expects(:convert_from_multiple).with("myformat", "mydata").returns "myobjects"
response = mock 'response'
response.stubs(:[]).with("content-type").returns "myformat"
response.stubs(:body).returns "mydata"
response.stubs(:code).returns "200"
@searcher.deserialize(response, true).should == "myobjects"
end
it "should strip the content-type header to keep only the mime-type" do
@model.expects(:convert_from).with("text/plain", "mydata").returns "myobject"
response = mock 'response'
response.stubs(:[]).with("content-type").returns "text/plain; charset=utf-8"
response.stubs(:body).returns "mydata"
response.stubs(:code).returns "200"
@searcher.deserialize(response)
end
end
describe "when creating an HTTP client" do
before do
Puppet.settings.stubs(:value).returns("rest_testing")
end
it "should use the class's server and port if the indirection request provides neither" do
@request = stub 'request', :key => "foo", :server => nil, :port => nil
@searcher.class.expects(:port).returns 321
@searcher.class.expects(:server).returns "myserver"
Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn"
@searcher.network(@request).should == "myconn"
end
it "should use the server from the indirection request if one is present" do
@request = stub 'request', :key => "foo", :server => "myserver", :port => nil
@searcher.class.stubs(:port).returns 321
Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn"
@searcher.network(@request).should == "myconn"
end
it "should use the port from the indirection request if one is present" do
@request = stub 'request', :key => "foo", :server => nil, :port => 321
@searcher.class.stubs(:server).returns "myserver"
Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn"
@searcher.network(@request).should == "myconn"
end
end
describe "when doing a find" do
before :each do
@connection = stub('mock http connection', :get => @response)
@searcher.stubs(:network).returns(@connection) # neuter the network connection
# Use a key with spaces, so we can test escaping
@request = Puppet::Indirector::Request.new(:foo, :find, "foo bar")
end
it "should call the GET http method on a network connection" do
@searcher.expects(:network).returns @connection
@connection.expects(:get).returns @response
@searcher.find(@request)
end
- it "should deserialize and return the http response" do
+ it "should deserialize and return the http response, setting name" do
@connection.expects(:get).returns @response
- instance = stub 'object', :name= => nil
+ instance = stub 'object'
+ instance.expects(:name=)
@searcher.expects(:deserialize).with(@response).returns instance
@searcher.find(@request).should == instance
end
+ it "should deserialize and return the http response, and not require name=" do
+ @connection.expects(:get).returns @response
+
+ instance = stub 'object'
+ @searcher.expects(:deserialize).with(@response).returns instance
+
+ @searcher.find(@request).should == instance
+ end
+
+
it "should use the URI generated by the Handler module" do
@searcher.expects(:indirection2uri).with(@request).returns "/my/uri"
@connection.expects(:get).with { |path, args| path == "/my/uri" }.returns(@response)
@searcher.find(@request)
end
it "should provide an Accept header containing the list of supported formats joined with commas" do
@connection.expects(:get).with { |path, args| args["Accept"] == "supported, formats" }.returns(@response)
@searcher.model.expects(:supported_formats).returns %w{supported formats}
@searcher.find(@request)
end
it "should deserialize and return the network response" do
@searcher.expects(:deserialize).with(@response).returns @instance
@searcher.find(@request).should equal(@instance)
end
it "should set the name of the resulting instance to the asked-for name" do
@searcher.expects(:deserialize).with(@response).returns @instance
@instance.expects(:name=).with "foo bar"
@searcher.find(@request)
end
it "should generate an error when result data deserializes fails" do
@searcher.expects(:deserialize).raises(ArgumentError)
lambda { @searcher.find(@request) }.should raise_error(ArgumentError)
end
end
describe "when doing a search" do
before :each do
@connection = stub('mock http connection', :get => @response)
@searcher.stubs(:network).returns(@connection) # neuter the network connection
@model.stubs(:convert_from_multiple)
@request = Puppet::Indirector::Request.new(:foo, :search, "foo bar")
end
it "should call the GET http method on a network connection" do
@searcher.expects(:network).returns @connection
@connection.expects(:get).returns @response
@searcher.search(@request)
end
it "should deserialize as multiple instances and return the http response" do
@connection.expects(:get).returns @response
@searcher.expects(:deserialize).with(@response, true).returns "myobject"
@searcher.search(@request).should == 'myobject'
end
it "should use the URI generated by the Handler module" do
@searcher.expects(:indirection2uri).with(@request).returns "/mys/uri"
@connection.expects(:get).with { |path, args| path == "/mys/uri" }.returns(@response)
@searcher.search(@request)
end
it "should provide an Accept header containing the list of supported formats joined with commas" do
@connection.expects(:get).with { |path, args| args["Accept"] == "supported, formats" }.returns(@response)
@searcher.model.expects(:supported_formats).returns %w{supported formats}
@searcher.search(@request)
end
it "should return an empty array if serialization returns nil" do
@model.stubs(:convert_from_multiple).returns nil
@searcher.search(@request).should == []
end
it "should generate an error when result data deserializes fails" do
@searcher.expects(:deserialize).raises(ArgumentError)
lambda { @searcher.search(@request) }.should raise_error(ArgumentError)
end
end
describe "when doing a destroy" do
before :each do
@connection = stub('mock http connection', :delete => @response)
@searcher.stubs(:network).returns(@connection) # neuter the network connection
@request = Puppet::Indirector::Request.new(:foo, :destroy, "foo bar")
end
it "should call the DELETE http method on a network connection" do
@searcher.expects(:network).returns @connection
@connection.expects(:delete).returns @response
@searcher.destroy(@request)
end
it "should fail if any options are provided, since DELETE apparently does not support query options" do
@request.stubs(:options).returns(:one => "two", :three => "four")
lambda { @searcher.destroy(@request) }.should raise_error(ArgumentError)
end
it "should deserialize and return the http response" do
@connection.expects(:delete).returns @response
@searcher.expects(:deserialize).with(@response).returns "myobject"
@searcher.destroy(@request).should == 'myobject'
end
it "should use the URI generated by the Handler module" do
@searcher.expects(:indirection2uri).with(@request).returns "/my/uri"
@connection.expects(:delete).with { |path, args| path == "/my/uri" }.returns(@response)
@searcher.destroy(@request)
end
it "should not include the query string" do
@connection.stubs(:delete).returns @response
@searcher.destroy(@request)
end
it "should provide an Accept header containing the list of supported formats joined with commas" do
@connection.expects(:delete).with { |path, args| args["Accept"] == "supported, formats" }.returns(@response)
@searcher.model.expects(:supported_formats).returns %w{supported formats}
@searcher.destroy(@request)
end
it "should deserialize and return the network response" do
@searcher.expects(:deserialize).with(@response).returns @instance
@searcher.destroy(@request).should equal(@instance)
end
it "should generate an error when result data deserializes fails" do
@searcher.expects(:deserialize).raises(ArgumentError)
lambda { @searcher.destroy(@request) }.should raise_error(ArgumentError)
end
end
describe "when doing a save" do
before :each do
@connection = stub('mock http connection', :put => @response)
@searcher.stubs(:network).returns(@connection) # neuter the network connection
@instance = stub 'instance', :render => "mydata", :mime => "mime"
@request = Puppet::Indirector::Request.new(:foo, :save, "foo bar")
@request.instance = @instance
end
it "should call the PUT http method on a network connection" do
@searcher.expects(:network).returns @connection
@connection.expects(:put).returns @response
@searcher.save(@request)
end
it "should fail if any options are provided, since DELETE apparently does not support query options" do
@request.stubs(:options).returns(:one => "two", :three => "four")
lambda { @searcher.save(@request) }.should raise_error(ArgumentError)
end
it "should use the URI generated by the Handler module" do
@searcher.expects(:indirection2uri).with(@request).returns "/my/uri"
@connection.expects(:put).with { |path, args| path == "/my/uri" }.returns(@response)
@searcher.save(@request)
end
it "should serialize the instance using the default format and pass the result as the body of the request" do
@instance.expects(:render).returns "serial_instance"
@connection.expects(:put).with { |path, data, args| data == "serial_instance" }.returns @response
@searcher.save(@request)
end
it "should deserialize and return the http response" do
@connection.expects(:put).returns @response
@searcher.expects(:deserialize).with(@response).returns "myobject"
@searcher.save(@request).should == 'myobject'
end
it "should provide an Accept header containing the list of supported formats joined with commas" do
@connection.expects(:put).with { |path, data, args| args["Accept"] == "supported, formats" }.returns(@response)
@searcher.model.expects(:supported_formats).returns %w{supported formats}
@searcher.save(@request)
end
it "should provide a Content-Type header containing the mime-type of the sent object" do
@connection.expects(:put).with { |path, data, args| args['Content-Type'] == "mime" }.returns(@response)
@instance.expects(:mime).returns "mime"
@searcher.save(@request)
end
it "should deserialize and return the network response" do
@searcher.expects(:deserialize).with(@response).returns @instance
@searcher.save(@request).should equal(@instance)
end
it "should generate an error when result data deserializes fails" do
@searcher.expects(:deserialize).raises(ArgumentError)
lambda { @searcher.save(@request) }.should raise_error(ArgumentError)
end
end
end
diff --git a/spec/unit/network/rest_authconfig.rb b/spec/unit/network/rest_authconfig.rb
index 407fc43f4..fe17d5626 100755
--- a/spec/unit/network/rest_authconfig.rb
+++ b/spec/unit/network/rest_authconfig.rb
@@ -1,147 +1,148 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
require 'puppet/network/rest_authconfig'
describe Puppet::Network::RestAuthConfig do
DEFAULT_ACL = [
{ :acl => "~ ^\/catalog\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true },
# this one will allow all file access, and thus delegate
# to fileserver.conf
{ :acl => "/file" },
{ :acl => "/certificate_revocation_list/ca", :method => :find, :authenticated => true },
{ :acl => "/report", :method => :save, :authenticated => true },
{ :acl => "/certificate/ca", :method => :find, :authenticated => false },
{ :acl => "/certificate/", :method => :find, :authenticated => false },
{ :acl => "/certificate_request", :method => [:find, :save], :authenticated => false },
{ :acl => "/status", :method => [:find], :authenticated => true },
+ { :acl => "/resource", :method => [:find, :save, :search], :authenticated => true },
]
before :each do
FileTest.stubs(:exists?).returns(true)
File.stubs(:stat).returns(stub('stat', :ctime => :now))
Time.stubs(:now).returns :now
@authconfig = Puppet::Network::RestAuthConfig.new("dummy", false)
@authconfig.stubs(:read)
@acl = stub_everything 'rights'
@authconfig.rights = @acl
@request = stub 'request', :indirection_name => "path", :key => "to/resource", :ip => "127.0.0.1",
:node => "me", :method => :save, :environment => :env, :authenticated => true
end
it "should use the puppet default rest authorization file" do
Puppet.expects(:[]).with(:rest_authconfig).returns("dummy")
Puppet::Network::RestAuthConfig.new(nil, false)
end
it "should read the config file when needed" do
@authconfig.expects(:read)
@authconfig.allowed?(@request)
end
it "should ask for authorization to the ACL subsystem" do
@acl.expects(:fail_on_deny).with("/path/to/resource", :node => "me", :ip => "127.0.0.1", :method => :save, :environment => :env, :authenticated => true)
@authconfig.allowed?(@request)
end
describe "when defining an acl with mk_acl" do
it "should create a new right for each default acl" do
@acl.expects(:newright).with(:path)
@authconfig.mk_acl(:acl => :path)
end
it "should allow everyone for each default right" do
@acl.expects(:allow).with(:path, "*")
@authconfig.mk_acl(:acl => :path)
end
it "should restrict the ACL to a method" do
@acl.expects(:restrict_method).with(:path, :method)
@authconfig.mk_acl(:acl => :path, :method => :method)
end
it "should restrict the ACL to a specific authentication state" do
@acl.expects(:restrict_authenticated).with(:path, :authentication)
@authconfig.mk_acl(:acl => :path, :authenticated => :authentication)
end
end
describe "when parsing the configuration file" do
it "should check for missing ACL after reading the authconfig file" do
File.stubs(:open)
@authconfig.expects(:insert_default_acl)
@authconfig.parse()
end
end
DEFAULT_ACL.each do |acl|
it "should insert #{acl[:acl]} if not present" do
@authconfig.rights.stubs(:[]).returns(true)
@authconfig.rights.stubs(:[]).with(acl[:acl]).returns(nil)
@authconfig.expects(:mk_acl).with { |h| h[:acl] == acl[:acl] }
@authconfig.insert_default_acl
end
it "should not insert #{acl[:acl]} if present" do
@authconfig.rights.stubs(:[]).returns(true)
@authconfig.rights.stubs(:[]).with(acl).returns(true)
@authconfig.expects(:mk_acl).never
@authconfig.insert_default_acl
end
end
it "should create default ACL entries if no file have been read" do
Puppet::Network::RestAuthConfig.any_instance.stubs(:exists?).returns(false)
Puppet::Network::RestAuthConfig.any_instance.expects(:insert_default_acl)
Puppet::Network::RestAuthConfig.main
end
describe "when adding default ACLs" do
DEFAULT_ACL.each do |acl|
it "should create a default right for #{acl[:acl]}" do
@authconfig.stubs(:mk_acl)
@authconfig.expects(:mk_acl).with(acl)
@authconfig.insert_default_acl
end
end
it "should log at info loglevel" do
Puppet.expects(:info).at_least_once
@authconfig.insert_default_acl
end
it "should create a last catch-all deny all rule" do
@authconfig.stubs(:mk_acl)
@acl.expects(:newright).with("/")
@authconfig.insert_default_acl
end
it "should create a last catch-all deny all rule for any authenticated request state" do
@authconfig.stubs(:mk_acl)
@acl.stubs(:newright).with("/")
@acl.expects(:restrict_authenticated).with("/", :any)
@authconfig.insert_default_acl
end
end
end
diff --git a/spec/unit/resource.rb b/spec/unit/resource.rb
index b26f4f923..2b1d49d50 100755
--- a/spec/unit/resource.rb
+++ b/spec/unit/resource.rb
@@ -1,495 +1,514 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../spec_helper'
require 'puppet/resource'
describe Puppet::Resource do
[:catalog, :file, :line].each do |attr|
it "should have an #{attr} attribute" do
resource = Puppet::Resource.new("file", "/my/file")
resource.should respond_to(attr)
resource.should respond_to(attr.to_s + "=")
end
end
describe "when initializing" do
it "should require the type and title" do
lambda { Puppet::Resource.new }.should raise_error(ArgumentError)
end
it "should create a resource reference with its type and title" do
ref = Puppet::Resource::Reference.new("file", "/f")
Puppet::Resource::Reference.expects(:new).with("file", "/f").returns ref
Puppet::Resource.new("file", "/f")
end
it "should allow setting of parameters" do
Puppet::Resource.new("file", "/f", :noop => true)[:noop].should be_true
end
it "should tag itself with its type" do
Puppet::Resource.new("file", "/f").should be_tagged("file")
end
it "should tag itself with its title if the title is a valid tag" do
Puppet::Resource.new("file", "bar").should be_tagged("bar")
end
it "should not tag itself with its title if the title is a not valid tag" do
Puppet::Resource.new("file", "/bar").should_not be_tagged("/bar")
end
end
it "should use the resource reference to determine its type" do
ref = Puppet::Resource::Reference.new("file", "/f")
Puppet::Resource::Reference.expects(:new).returns ref
resource = Puppet::Resource.new("file", "/f")
ref.expects(:type).returns "mytype"
resource.type.should == "mytype"
end
it "should use its resource reference to determine its title" do
ref = Puppet::Resource::Reference.new("file", "/f")
Puppet::Resource::Reference.expects(:new).returns ref
resource = Puppet::Resource.new("file", "/f")
ref.expects(:title).returns "mytitle"
resource.title.should == "mytitle"
end
it "should use its resource reference to determine whether it is builtin" do
ref = Puppet::Resource::Reference.new("file", "/f")
Puppet::Resource::Reference.expects(:new).returns ref
resource = Puppet::Resource.new("file", "/f")
ref.expects(:builtin_type?).returns "yep"
resource.builtin_type?.should == "yep"
end
it "should call its builtin_type? method when 'builtin?' is called" do
resource = Puppet::Resource.new("file", "/f")
resource.expects(:builtin_type?).returns "foo"
resource.builtin?.should == "foo"
end
it "should use its resource reference to produce its canonical reference string" do
ref = Puppet::Resource::Reference.new("file", "/f")
Puppet::Resource::Reference.expects(:new).returns ref
resource = Puppet::Resource.new("file", "/f")
ref.expects(:to_s).returns "Foo[bar]"
resource.ref.should == "Foo[bar]"
end
it "should be taggable" do
Puppet::Resource.ancestors.should be_include(Puppet::Util::Tagging)
end
it "should have an 'exported' attribute" do
resource = Puppet::Resource.new("file", "/f")
resource.exported = true
resource.exported.should == true
resource.should be_exported
end
describe "when managing parameters" do
before do
@resource = Puppet::Resource.new("file", "/my/file")
end
it "should allow setting and retrieving of parameters" do
@resource[:foo] = "bar"
@resource[:foo].should == "bar"
end
it "should canonicalize retrieved parameter names to treat symbols and strings equivalently" do
@resource[:foo] = "bar"
@resource["foo"].should == "bar"
end
it "should canonicalize set parameter names to treat symbols and strings equivalently" do
@resource["foo"] = "bar"
@resource[:foo].should == "bar"
end
it "should set the namevar when asked to set the name" do
Puppet::Type.type(:file).stubs(:namevar).returns :myvar
@resource[:name] = "/foo"
@resource[:myvar].should == "/foo"
end
it "should return the namevar when asked to return the name" do
Puppet::Type.type(:file).stubs(:namevar).returns :myvar
@resource[:myvar] = "/foo"
@resource[:name].should == "/foo"
end
it "should be able to set the name for non-builtin types" do
resource = Puppet::Resource.new(:foo, "bar")
lambda { resource[:name] = "eh" }.should_not raise_error
end
it "should be able to return the name for non-builtin types" do
resource = Puppet::Resource.new(:foo, "bar")
resource[:name] = "eh"
resource[:name].should == "eh"
end
it "should be able to iterate over parameters" do
@resource[:foo] = "bar"
@resource[:fee] = "bare"
params = {}
@resource.each do |key, value|
params[key] = value
end
params.should == {:foo => "bar", :fee => "bare"}
end
it "should include Enumerable" do
@resource.class.ancestors.should be_include(Enumerable)
end
it "should have a method for testing whether a parameter is included" do
@resource[:foo] = "bar"
@resource.should be_has_key(:foo)
@resource.should_not be_has_key(:eh)
end
it "should have a method for providing the list of parameters" do
@resource[:foo] = "bar"
@resource[:bar] = "foo"
keys = @resource.keys
keys.should be_include(:foo)
keys.should be_include(:bar)
end
it "should have a method for providing the number of parameters" do
@resource[:foo] = "bar"
@resource.length.should == 1
end
it "should have a method for deleting parameters" do
@resource[:foo] = "bar"
@resource.delete(:foo)
@resource[:foo].should be_nil
end
it "should have a method for testing whether the parameter list is empty" do
@resource.should be_empty
@resource[:foo] = "bar"
@resource.should_not be_empty
end
it "should be able to produce a hash of all existing parameters" do
@resource[:foo] = "bar"
@resource[:fee] = "yay"
hash = @resource.to_hash
hash[:foo].should == "bar"
hash[:fee].should == "yay"
end
it "should not provide direct access to the internal parameters hash when producing a hash" do
hash = @resource.to_hash
hash[:foo] = "bar"
@resource[:foo].should be_nil
end
it "should use the title as the namevar to the hash if no namevar is present" do
Puppet::Type.type(:file).stubs(:namevar).returns :myvar
@resource.to_hash[:myvar].should == "/my/file"
end
it "should set :name to the title if :name is not present for non-builtin types" do
resource = Puppet::Resource.new :foo, "bar"
resource.to_hash[:name].should == "bar"
end
end
describe "when serializing" do
before do
@resource = Puppet::Resource.new("file", "/my/file")
@resource["one"] = "test"
@resource["two"] = "other"
end
it "should be able to be dumped to yaml" do
proc { YAML.dump(@resource) }.should_not raise_error
end
it "should produce an equivalent yaml object" do
text = YAML.dump(@resource)
newresource = YAML.load(text)
newresource.title.should == @resource.title
newresource.type.should == @resource.type
%w{one two}.each do |param|
newresource[param].should == @resource[param]
end
end
end
describe "when converting to a RAL resource" do
before do
@resource = Puppet::Resource.new("file", "/my/file")
@resource["one"] = "test"
@resource["two"] = "other"
end
it "should use the resource type's :create method to create the resource if the resource is of a builtin type" do
type = mock 'resource type'
type.expects(:new).with(@resource).returns(:myresource)
Puppet::Type.expects(:type).with(@resource.type).returns(type)
@resource.to_ral.should == :myresource
end
it "should convert to a component instance if the resource type is not of a builtin type" do
component = mock 'component type'
Puppet::Type::Component.expects(:new).with(@resource).returns "meh"
Puppet::Type.expects(:type).with(@resource.type).returns(nil)
@resource.to_ral.should == "meh"
end
end
it "should be able to convert itself to Puppet code" do
Puppet::Resource.new("one::two", "/my/file").should respond_to(:to_manifest)
end
describe "when converting to puppet code" do
before do
@resource = Puppet::Resource.new("one::two", "/my/file", :noop => true, :foo => %w{one two})
end
it "should print the type and title" do
@resource.to_manifest.should be_include("one::two { '/my/file':\n")
end
it "should print each parameter, with the value single-quoted" do
@resource.to_manifest.should be_include(" noop => 'true'")
end
it "should print array values appropriately" do
@resource.to_manifest.should be_include(" foo => ['one','two']")
end
end
it "should be able to convert itself to a TransObject instance" do
Puppet::Resource.new("one::two", "/my/file").should respond_to(:to_trans)
end
describe "when converting to a TransObject" do
describe "and the resource is not an instance of a builtin type" do
before do
@resource = Puppet::Resource.new("foo", "bar")
end
it "should return a simple TransBucket if it is not an instance of a builtin type" do
bucket = @resource.to_trans
bucket.should be_instance_of(Puppet::TransBucket)
bucket.type.should == @resource.type
bucket.name.should == @resource.title
end
it "should copy over the resource's file" do
@resource.file = "/foo/bar"
@resource.to_trans.file.should == "/foo/bar"
end
it "should copy over the resource's line" do
@resource.line = 50
@resource.to_trans.line.should == 50
end
end
describe "and the resource is an instance of a builtin type" do
before do
@resource = Puppet::Resource.new("file", "bar")
end
it "should return a TransObject if it is an instance of a builtin resource type" do
trans = @resource.to_trans
trans.should be_instance_of(Puppet::TransObject)
trans.type.should == "file"
trans.name.should == @resource.title
end
it "should copy over the resource's file" do
@resource.file = "/foo/bar"
@resource.to_trans.file.should == "/foo/bar"
end
it "should copy over the resource's line" do
@resource.line = 50
@resource.to_trans.line.should == 50
end
# Only TransObjects support tags, annoyingly
it "should copy over the resource's tags" do
@resource.tag "foo"
@resource.to_trans.tags.should == @resource.tags
end
it "should copy the resource's parameters into the transobject and convert the parameter name to a string" do
@resource[:foo] = "bar"
@resource.to_trans["foo"].should == "bar"
end
it "should be able to copy arrays of values" do
@resource[:foo] = %w{yay fee}
@resource.to_trans["foo"].should == %w{yay fee}
end
it "should reduce single-value arrays to just a value" do
@resource[:foo] = %w{yay}
@resource.to_trans["foo"].should == "yay"
end
it "should convert resource references into the backward-compatible form" do
@resource[:foo] = Puppet::Resource::Reference.new(:file, "/f")
@resource.to_trans["foo"].should == %w{file /f}
end
it "should convert resource references into the backward-compatible form even when within arrays" do
@resource[:foo] = ["a", Puppet::Resource::Reference.new(:file, "/f")]
@resource.to_trans["foo"].should == ["a", %w{file /f}]
end
end
end
describe "when converting to pson" do
confine "Missing 'pson' library" => Puppet.features.pson?
def pson_output_should
@resource.class.expects(:pson_create).with { |hash| yield hash }
end
it "should include the pson util module" do
Puppet::Resource.metaclass.ancestors.should be_include(Puppet::Util::Pson)
end
# LAK:NOTE For all of these tests, we convert back to the resource so we can
# trap the actual data structure then.
it "should set its type to the provided type" do
Puppet::Resource.from_pson(PSON.parse(Puppet::Resource.new("File", "/foo").to_pson)).type.should == "File"
end
it "should set its title to the provided title" do
Puppet::Resource.from_pson(PSON.parse(Puppet::Resource.new("File", "/foo").to_pson)).title.should == "/foo"
end
it "should include all tags from the resource" do
resource = Puppet::Resource.new("File", "/foo")
resource.tag("yay")
Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).tags.should == resource.tags
end
it "should include the file if one is set" do
resource = Puppet::Resource.new("File", "/foo")
resource.file = "/my/file"
Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).file.should == "/my/file"
end
it "should include the line if one is set" do
resource = Puppet::Resource.new("File", "/foo")
resource.line = 50
Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).line.should == 50
end
it "should include the 'exported' value if one is set" do
resource = Puppet::Resource.new("File", "/foo")
resource.exported = true
Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).exported.should be_true
end
it "should set 'exported' to false if no value is set" do
resource = Puppet::Resource.new("File", "/foo")
Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).exported.should be_false
end
it "should set all of its parameters as the 'parameters' entry" do
resource = Puppet::Resource.new("File", "/foo")
resource[:foo] = %w{bar eh}
resource[:fee] = %w{baz}
result = Puppet::Resource.from_pson(PSON.parse(resource.to_pson))
result["foo"].should == %w{bar eh}
result["fee"].should == %w{baz}
end
end
describe "when converting from pson" do
confine "Missing 'pson' library" => Puppet.features.pson?
def pson_result_should
Puppet::Resource.expects(:new).with { |hash| yield hash }
end
before do
@data = {
'type' => "file",
'title' => "yay",
}
end
it "should set its type to the provided type" do
Puppet::Resource.from_pson(@data).type.should == "File"
end
it "should set its title to the provided title" do
Puppet::Resource.from_pson(@data).title.should == "yay"
end
it "should tag the resource with any provided tags" do
@data['tags'] = %w{foo bar}
resource = Puppet::Resource.from_pson(@data)
resource.tags.should be_include("foo")
resource.tags.should be_include("bar")
end
it "should set its file to the provided file" do
@data['file'] = "/foo/bar"
Puppet::Resource.from_pson(@data).file.should == "/foo/bar"
end
it "should set its line to the provided line" do
@data['line'] = 50
Puppet::Resource.from_pson(@data).line.should == 50
end
it "should 'exported' to true if set in the pson data" do
@data['exported'] = true
Puppet::Resource.from_pson(@data).exported.should be_true
end
it "should 'exported' to false if not set in the pson data" do
Puppet::Resource.from_pson(@data).exported.should be_false
end
it "should fail if no title is provided" do
@data.delete('title')
lambda { Puppet::Resource.from_pson(@data) }.should raise_error(ArgumentError)
end
it "should fail if no type is provided" do
@data.delete('type')
lambda { Puppet::Resource.from_pson(@data) }.should raise_error(ArgumentError)
end
it "should set each of the provided parameters" do
@data['parameters'] = {'foo' => %w{one two}, 'fee' => %w{three four}}
resource = Puppet::Resource.from_pson(@data)
resource['foo'].should == %w{one two}
resource['fee'].should == %w{three four}
end
it "should convert single-value array parameters to normal values" do
@data['parameters'] = {'foo' => %w{one}}
resource = Puppet::Resource.from_pson(@data)
resource['foo'].should == %w{one}
end
end
+
+ describe "it should implement to_resource" do
+ resource = Puppet::Resource.new("file", "/my/file")
+ resource.to_resource.should == resource
+ end
+
+ describe "because it is an indirector model" do
+ it "should include Puppet::Indirector" do
+ Puppet::Resource.should be_is_a(Puppet::Indirector)
+ end
+
+ it "should have a default terminus" do
+ Puppet::Resource.indirection.terminus_class.should == :ral
+ end
+
+ it "should have a name" do
+ Puppet::Resource.new("file", "/my/file").name.should == "File//my/file"
+ end
+ end
end