diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb
index 880711066..c3855a400 100644
--- a/lib/puppet/type.rb
+++ b/lib/puppet/type.rb
@@ -1,1885 +1,1889 @@
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/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 key_attributes come 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
key_attributes | (parameters & [:provider]) | properties.collect { |property| property.name } | parameters | metaparams
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
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)
}
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.
param.required_features = options[:required_features] if options[:required_features]
handle_param_options(name, options)
param.metaparam = true
param
end
def self.key_attribute_parameters
@key_attribute_parameters ||= (
params = @parameters.find_all { |param|
param.isnamevar? or param.name == :name
}
)
end
def self.key_attributes
key_attribute_parameters.collect { |p| p.name }
end
def self.title_patterns
case key_attributes.length
when 0; []
when 1;
identity = lambda {|x| x}
[ [ /(.*)/m, [ [key_attributes.first, identity ] ] ] ]
else
raise Puppet::DevError,"you must specify title patterns when there are two or more key attributes"
end
end
def uniqueness_key
to_resource.uniqueness_key
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.
param.required_features = options[:required_features] if options[:required_features]
param.isnamevar if options[:namevar]
param
end
def self.newstate(name, options = {}, &block)
Puppet.warning "newstate() has been deprecrated; use newproperty(#{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 #{options.inspect}"
end
raise Puppet::DevError, "Class #{self.name} already has a property named #{name}" if @validproperties.include?(name)
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
class_eval(&block) if block
end
# If it's the 'ensure' property, always put it first.
if name == :ensure
@properties.unshift prop
else
@properties << prop
end
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)
@validattrs[name] = !!(self.validproperty?(name) or self.validparameter?(name) or self.metaparam?(name))
end
@validattrs[name]
end
# does the name reflect a valid property?
def self.validproperty?(name)
name = symbolize(name)
@validproperties.include?(name) && @validproperties[name]
end
# Return the list of validproperties
def self.validproperties
return {} unless defined?(@parameters)
@validproperties.keys
end
# does the name reflect a valid parameter?
def self.validparameter?(name)
raise Puppet::DevError, "Class #{self} has not defined parameters" unless defined?(@parameters)
!!(@paramhash.include?(name) or @@metaparamhash.include?(name))
end
# This is a forward-compatibility method - it's the validity interface we'll use in Puppet::Resource.
def self.valid_parameter?(name)
validattr?(name)
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
false
end
#
# The name_var is the key_attribute in the case that there is only one.
#
def name_var
key_attributes = self.class.key_attributes
(key_attributes.length == 1) && key_attributes.first
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)
fail("Invalid parameter #{name}(#{name.inspect})") unless self.class.validattr?(name)
if name == :name
name = name_var
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)
fail("Invalid parameter #{name}") unless self.class.validattr?(name)
if name == :name
name = name_var
end
raise Puppet::Error.new("Got nil value for #{name}") if value.nil?
property = self.newattr(name)
begin
# make sure the parameter doesn't have any errors
property.value = value
rescue => detail
error = Puppet::Error.new("Parameter #{name} failed: #{detail}")
error.set_backtrace(detail.backtrace)
raise error
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)
(prop = @parameters[name] and prop.is_a?(Puppet::Property)) ? prop.should : nil
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)
if name.is_a?(Class)
klass = name
name = klass.name
end
unless klass = self.class.attrclass(name)
raise Puppet::Error, "Resource type #{self.class.name} does not support parameter #{name}"
end
return @parameters[name] if @parameters.include?(name)
@parameters[name] = klass.new(:resource => self)
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)
name = name.intern unless name.is_a? Symbol
@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)
(obj = @parameters[symbolize(name)] and obj.is_a?(Puppet::Property)) ? obj : nil
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)
(obj = @parameters[name] and obj.respond_to?(:value)) ? obj.value : nil
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 @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?
@depthfirst
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
self.provider.flush if self.provider and self.provider.respond_to?(:flush)
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 '#{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 '#{property.name}'"
end
propis = is[property]
unless property.insync?(propis)
property.debug("Not in sync: #{propis.inspect} vs #{property.should.inspect}")
insync = false
#else
# property.debug("In sync")
end
}
#self.debug("#{self} sync status is #{insync}")
insync
end
# retrieve the current value of all contained properties
def retrieve
fail "Provider #{provider.class.name} is not functional on this host" if self.provider.is_a?(Puppet::Provider) and ! provider.class.suitable?
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
def retrieve_resource
resource = retrieve
resource = Resource.new(type, title, :parameters => resource) if resource.is_a? Hash
resource
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 we're not a host_config, we're almost certainly part of
+ # Settings, and we want to ignore 'noop'
+ return false if catalog and ! catalog.host_config?
+
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 '#{newobj.class.name}[#{name}]' already exists"
msg += ("in file #{object.file} at line #{object.line}") if exobj.file and exobj.line
msg += ("and cannot be redefined in file #{object.file} at line #{object.line}") if object.file and object.line
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 #{name}: object already exists"
)
end
end
if @aliases.include?(name)
unless @aliases[name] == obj
raise Puppet::Error.new(
"Object #{@aliases[name].name} already has alias #{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
@aliases.clear if defined?(@aliases)
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)
@objects.delete(resource.title) if @objects.include?(resource.title)
@aliases.delete(resource.title) if @aliases.include?(resource.title)
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"
@objects.has_key?(name)
end
# Retrieve all known instances. Either requires providers or must be overridden.
def self.instances
raise Puppet::DevError, "#{self.name} has no providers and has not overridden 'instances'" if provider_hash.empty?
# 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, :audit => :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.
def self.hash2resource(hash)
hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result }
title = hash.delete(:title)
title ||= hash[:name]
title ||= hash[key_attributes.first] if key_attributes.length == 1
raise Puppet::Error, "Title or name must be provided" unless title
# 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
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(:audit) do
desc "Audit specified attributes of resources over time, and report if any have changed.
This attribute can be used to track changes to any resource over time, and can
provide an audit trail of every change that happens on any given machine.
Note that you cannot both audit and manage an attribute - managing it guarantees
the value, and any changes already get logged."
validate do |list|
list = Array(list)
unless list == [:all]
list.each do |param|
next if @resource.class.validattr?(param)
fail "Cannot audit #{param}: not a valid attribute for #{resource}"
end
end
end
munge do |args|
properties_to_audit(args).each do |param|
next unless resource.class.validproperty?(param)
resource.newattr(param)
end
end
def all_properties
resource.class.properties.find_all do |property|
resource.provider.nil? or resource.provider.class.supports_parameter?(property)
end.collect do |property|
property.name
end
end
def properties_to_audit(list)
if list == :all
list = all_properties if list == :all
else
list = Array(list).collect { |p| p.to_sym }
end
end
end
newmetaparam(:check) do
desc "Audit specified attributes of resources over time, and report if any have changed.
This parameter has been deprecated in favor of 'audit'."
munge do |args|
resource.warning "'check' attribute is deprecated; use 'audit' instead"
resource[:audit] = args
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|
aliases = [aliases] unless aliases.is_a?(Array)
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("#{@resource.title} can not create alias #{other}: object already exists")
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::
puppet agent --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)
ref
else
Puppet::Resource.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 #{description} #{ref} for #{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 '#{reference}' of #{@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 #{related_resource.ref}")
else
# If there's no callback, there's no point in even adding
# a label.
subargs = nil
self.debug("requires #{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:
ensure => running,
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
newmetaparam(:stage) do
desc %{Which run stage a given resource should reside in. This just creates
a dependency on or from the named milestone. For instance, saying that
this is in the 'bootstrap' stage creates a dependency on the 'bootstrap'
milestone.
By default, all classes get directly added to the
'main' stage. You can create new stages as resources:
stage { [pre, post]: }
To order stages, use standard relationships:
stage { pre: before => Stage[main] }
Or use the new relationship syntax:
Stage[pre] -> Stage[main] -> Stage[post]
Then use the new class parameters to specify a stage:
class { foo: stage => pre }
Stages can only be set on classes, not individual resources. This will
fail::
file { '/foo': stage => pre, ensure => file }
}
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 @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 #{self.name}: #{defaults.collect { |i| i.name.to_s }.join(", ")}; using #{defaults[0].name}"
)
retval = defaults.shift
elsif defaults.length == 1
retval = defaults.shift
else
raise Puppet::DevError, "Could not find a default provider for #{self.name}"
end
@defaultprovider = retval
end
@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.
@providerloader.load(name) unless provider_hash.has_key?(name)
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)
(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 #{name} #{self.name} provider"
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 #{pname} of #{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
)
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|
"* **#{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
provider_class = provider_class.class.name if provider_class.is_a?(Puppet::Provider)
unless provider = @resource.class.provider(provider_class)
raise ArgumentError, "Invalid #{@resource.class.name} provider '#{provider_class}'"
end
end
munge do |provider|
provider = provider[0] if provider.is_a? Array
provider = provider.intern if provider.is_a? String
@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
providerloader.loadall if provider_hash.empty?
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 #{name} provider of #{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)
list = [list] unless list.is_a?(Array)
# 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)
}
}
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
# 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 = {}
@parameters ||= []
@validproperties = {}
@properties = []
@parameters = []
@paramhash = {}
@attr_aliases = {}
@paramdoc = Hash.new { |hash,key|
key = key.intern if key.is_a?(String)
if hash.include?(key)
hash[key]
else
"Param Documentation for #{key} not found"
end
}
@doc ||= ""
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)
self[name_var] = hash.delete(name_var) if name_var
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 #{attr} on #{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
# 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
# For now, leave the 'name' method functioning like it used to. Once 'title'
# works everywhere, I'll switch it.
def name
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
"#{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 @title
if self.class.validparameter?(name_var)
@title = self[:name]
elsif self.class.validproperty?(name_var)
@title = self.should(name_var)
else
self.devfail "Could not find namevar #{name_var} for #{self.class.name}"
end
end
@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_resource
values.each do |name, value|
name = name.name if name.respond_to? :name
trans[name] = value
end
@parameters.each do |name, param|
# Avoid adding each instance name twice
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'
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
def virtual?; !!@virtual; end
def exported?; !!@exported; end
end
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 cbb12a816..ca4ecda35 100644
--- a/lib/puppet/util/settings.rb
+++ b/lib/puppet/util/settings.rb
@@ -1,941 +1,932 @@
require 'puppet'
require 'sync'
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
ReadOnly = [:run_mode, :name]
# 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 }
}
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
}
options
end
# Is our parameter a boolean parameter?
def boolean?(param)
param = param.to_sym
!!(@config.include?(param) and @config[param].kind_of? BooleanSetting)
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.
@used = [] unless exceptcli
@cache.clear
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 #{value}"
end
end
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)
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] = {} }
# 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 "#{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 "#{v} = #{value(v,env)}"
else
puts "invalid parameter: #{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)
generate_manifest if value(:genmanifest)
end
def print_configs?
(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 the section name for the run_mode.
def run_mode
Puppet.run_mode.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]
@sync.synchronize do
unsafe_parse(self[:config])
end
# Create a timer so that this file will get checked automatically
# and reparsed if necessary.
set_filetimeout_timer
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)
metas = {}
data.each do |area, values|
metas[area] = values.delete(:_meta)
values.each do |key,value|
set_value(key, value, area, :dont_trigger_handles => true, :ignore_bad_settings => true )
end
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|
source = run_mode if source == :run_mode
source = @name if (@name && source == :name)
if meta = metas[source]
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
hash[:section] = hash[:section].to_sym if hash[:section]
if type = hash[:type]
unless klass = {:setting => Setting, :file => FileSetting, :boolean => BooleanSetting}[type]
raise ArgumentError, "Invalid setting type '#{type}'"
end
hash.delete(:type)
else
case hash[:default]
when true, false, "true", "false"
klass = BooleanSetting
when /^\$\w+\//, /^\//, /^\w:\//
klass = FileSetting
when String, Integer, Float # nothing
klass = Setting
else
raise ArgumentError, "Invalid value '#{hash[:default].inspect}' for #{hash[:name]}"
end
end
hash[:settings] = self
setting = klass.new(hash)
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 #{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, :run_mode, :main, :mutable_defaults]
else
[:cli, :memory, :run_mode, :main, :mutable_defaults]
end
end
# Get a list of objects per section
def sectionlist
sectionlist = []
self.each { |name, obj|
section = obj.section || "puppet"
sections[section] ||= []
sectionlist << section unless sectionlist.include?(section)
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], :audit => :ensure
@service_user_available = user.exists?
end
def legacy_to_mode(type, param)
if not defined?(@app_names)
require 'puppet/util/command_line'
command_line = Puppet::Util::CommandLine.new
@app_names = Puppet::Util::CommandLine::LegacyName.inject({}) do |hash, pair|
app, legacy = pair
command_line.require_application app
hash[legacy.to_sym] = Puppet::Application.find(app).run_mode.name
hash
end
end
if new_type = @app_names[type]
Puppet.warning "You have configuration parameter $#{param} specified in [#{type}], which is a deprecated section. I'm assuming you meant [#{new_type}]"
return new_type
end
type
end
def set_value(param, value, type, options = {})
param = param.to_sym
unless setting = @config[param]
if options[:ignore_bad_settings]
return
else
raise ArgumentError,
"Attempt to assign a value to unknown configuration parameter #{param.inspect}"
end
end
value = setting.munge(value) if setting.respond_to?(:munge)
setting.handle(value) if setting.respond_to?(:handle) and not options[:dont_trigger_handles]
if ReadOnly.include? param and type != :mutable_defaults
raise ArgumentError,
"You're attempting to set configuration parameter $#{param}, which is read-only."
end
type = legacy_to_mode(type, param)
@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
value
end
# 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
raise ArgumentError, "Parameter #{name} is already defined" if @config.include?(name)
tryconfig = newsetting(hash)
if short = tryconfig.short
if other = @shortnames[short]
raise ArgumentError, "Parameter #{other.name} is already using short 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.
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?(:run_mode)
str += "[#{self[:run_mode]}]\n"
end
eachsection do |section|
persection(section) do |obj|
str += obj.to_config + "\n" unless ReadOnly.include? obj.name
end
end
return str
end
# Convert to a parseable manifest
def to_manifest
catalog = to_catalog
catalog.resource_refs.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 #{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 #{failures.length} failure(s) while initializing: #{failures.collect { |l| l.to_s }.join("; ")}"
- end
+ 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 #{failures.length} failure(s) while initializing: #{failures.collect { |l| l.to_s }.join("; ")}"
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
# 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
throw :foundval, @values[source][param] if @values[source].include?(param)
end
end
throw :foundval, nil
end
# If we didn't get a value, use the default
val = @config[param].default if val.nil?
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
# 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)
if param == :code
# if we interpolate code, all hell breaks loose.
return val
end
# Convert it if necessary
val = convert(val, environment)
# And cache it
@cache[environment||"none"][param] = val
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.features.root?
chown = [obj.owner, obj.group]
else
chown = [nil, nil]
end
Puppet::Util::SUIDManager.asuser(*chown) do
mode = obj.mode || 0640
args << "w" if args.empty?
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
raise Puppet::DevError, "Cannot create #{file}; directory #{File.dirname(file)} does not exist" unless FileTest.directory?(File.dirname(tmpfile))
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 #{file}; Aborting locked write. Check the .tmp file and delete if appropriate"
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 #{file} to #{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 #{default}"
end
raise ArgumentError, "Default #{default} is not a file" unless obj.is_a? FileSetting
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, :parameters => {:ensure => :present})
resource[:gid] = self[:group] if self[:group]
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, :parameters => {: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.run_mode if source == :run_mode
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
raise ArgumentError, "Invalid file option '#{param}'" unless [:owner, :mode, :group].include?(param)
if param == :mode and value !~ /^\d+$/
raise ArgumentError, "File modes must be numbers"
end
else
raise ArgumentError, "Could not parse '#{string}'"
end
end
''
end
result[:value] = value.sub(/\s*$/, '')
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 method just turns a file in to a hash of hashes.
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+)\]\s*$/
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*(.*?)\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 #{line}")
error.file = file
error.line = line
raise error
end
}
result
end
# Read the file in.
def read_file(file)
begin
return File.read(file)
rescue Errno::ENOENT
raise ArgumentError, "No such file #{file}"
rescue Errno::EACCES
raise ArgumentError, "Permission denied to file #{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/type_spec.rb b/spec/unit/type_spec.rb
index 71d415dc6..487750e52 100755
--- a/spec/unit/type_spec.rb
+++ b/spec/unit/type_spec.rb
@@ -1,550 +1,566 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../spec_helper'
describe Puppet::Type do
it "should include the Cacher module" do
Puppet::Type.ancestors.should be_include(Puppet::Util::Cacher)
end
it "should consider a parameter to be valid if it is a valid parameter" do
Puppet::Type.type(:mount).should be_valid_parameter(:path)
end
it "should consider a parameter to be valid if it is a valid property" do
Puppet::Type.type(:mount).should be_valid_parameter(:fstype)
end
it "should consider a parameter to be valid if it is a valid metaparam" do
Puppet::Type.type(:mount).should be_valid_parameter(:noop)
end
it "should use its catalog as its expirer" do
catalog = Puppet::Resource::Catalog.new
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
resource.catalog = catalog
resource.expirer.should equal(catalog)
end
it "should do nothing when asked to expire when it has no catalog" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
lambda { resource.expire }.should_not raise_error
end
it "should be able to retrieve a property by name" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
resource.property(:fstype).must be_instance_of(Puppet::Type.type(:mount).attrclass(:fstype))
end
it "should be able to retrieve a parameter by name" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
resource.parameter(:name).must be_instance_of(Puppet::Type.type(:mount).attrclass(:name))
end
it "should be able to retrieve a property by name using the :parameter method" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
resource.parameter(:fstype).must be_instance_of(Puppet::Type.type(:mount).attrclass(:fstype))
end
it "should be able to retrieve all set properties" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
props = resource.properties
props.should_not be_include(nil)
[:fstype, :ensure, :pass].each do |name|
props.should be_include(resource.parameter(name))
end
end
it "should have a method for setting default values for resources" do
Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:set_default)
end
it "should do nothing for attributes that have no defaults and no specified value" do
Puppet::Type.type(:mount).new(:name => "foo").parameter(:noop).should be_nil
end
it "should have a method for adding tags" do
Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:tags)
end
it "should use the tagging module" do
Puppet::Type.type(:mount).ancestors.should be_include(Puppet::Util::Tagging)
end
it "should delegate to the tagging module when tags are added" do
resource = Puppet::Type.type(:mount).new(:name => "foo")
resource.stubs(:tag).with(:mount)
resource.expects(:tag).with(:tag1, :tag2)
resource.tags = [:tag1,:tag2]
end
it "should add the current type as tag" do
resource = Puppet::Type.type(:mount).new(:name => "foo")
resource.stubs(:tag)
resource.expects(:tag).with(:mount)
resource.tags = [:tag1,:tag2]
end
it "should have a method to know if the resource is exported" do
Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:exported?)
end
it "should have a method to know if the resource is virtual" do
Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:virtual?)
end
it "should consider its version to be its catalog version" do
resource = Puppet::Type.type(:mount).new(:name => "foo")
catalog = Puppet::Resource::Catalog.new
catalog.version = 50
catalog.add_resource resource
resource.version.should == 50
end
it "should consider its version to be zero if it has no catalog" do
Puppet::Type.type(:mount).new(:name => "foo").version.should == 0
end
it "should provide source_descriptors" do
resource = Puppet::Type.type(:mount).new(:name => "foo")
catalog = Puppet::Resource::Catalog.new
catalog.version = 50
catalog.add_resource resource
resource.source_descriptors.should == {:version=>50, :tags=>["mount", "foo"], :path=>"/Mount[foo]"}
end
it "should consider its type to be the name of its class" do
Puppet::Type.type(:mount).new(:name => "foo").type.should == :mount
end
+ it "should use any provided noop value" do
+ Puppet::Type.type(:mount).new(:name => "foo", :noop => true).must be_noop
+ end
+
+ it "should use the global noop value if none is provided" do
+ Puppet[:noop] = true
+ Puppet::Type.type(:mount).new(:name => "foo").must be_noop
+ end
+
+ it "should not be noop if in a non-host_config catalog" do
+ resource = Puppet::Type.type(:mount).new(:name => "foo")
+ catalog = Puppet::Resource::Catalog.new
+ catalog.add_resource resource
+ resource.should_not be_noop
+ end
+
describe "when creating an event" do
before do
@resource = Puppet::Type.type(:mount).new :name => "foo"
end
it "should have the resource's reference as the resource" do
@resource.event.resource.should == "Mount[foo]"
end
it "should have the resource's log level as the default log level" do
@resource[:loglevel] = :warning
@resource.event.default_log_level.should == :warning
end
{:file => "/my/file", :line => 50, :tags => %{foo bar}, :version => 50}.each do |attr, value|
it "should set the #{attr}" do
@resource.stubs(attr).returns value
@resource.event.send(attr).should == value
end
end
it "should allow specification of event attributes" do
@resource.event(:status => "noop").status.should == "noop"
end
end
describe "when choosing a default provider" do
it "should choose the provider with the highest specificity" do
# Make a fake type
type = Puppet::Type.newtype(:defaultprovidertest) do
newparam(:name) do end
end
basic = type.provide(:basic) {}
greater = type.provide(:greater) {}
basic.stubs(:specificity).returns 1
greater.stubs(:specificity).returns 2
type.defaultprovider.should equal(greater)
end
end
describe "when initializing" do
describe "and passed a TransObject" do
it "should fail" do
trans = Puppet::TransObject.new("/foo", :mount)
lambda { Puppet::Type.type(:mount).new(trans) }.should raise_error(Puppet::DevError)
end
end
describe "and passed a Puppet::Resource instance" do
it "should set its title to the title of the resource if the resource type is equal to the current type" do
resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "/other"})
Puppet::Type.type(:mount).new(resource).title.should == "/foo"
end
it "should set its title to the resource reference if the resource type is not equal to the current type" do
resource = Puppet::Resource.new(:user, "foo")
Puppet::Type.type(:mount).new(resource).title.should == "User[foo]"
end
[:line, :file, :catalog, :exported, :virtual].each do |param|
it "should copy '#{param}' from the resource if present" do
resource = Puppet::Resource.new(:mount, "/foo")
resource.send(param.to_s + "=", "foo")
resource.send(param.to_s + "=", "foo")
Puppet::Type.type(:mount).new(resource).send(param).should == "foo"
end
end
it "should copy any tags from the resource" do
resource = Puppet::Resource.new(:mount, "/foo")
resource.tag "one", "two"
tags = Puppet::Type.type(:mount).new(resource).tags
tags.should be_include("one")
tags.should be_include("two")
end
it "should copy the resource's parameters as its own" do
resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:atboot => true, :fstype => "boo"})
params = Puppet::Type.type(:mount).new(resource).to_hash
params[:fstype].should == "boo"
params[:atboot].should == true
end
end
describe "and passed a Hash" do
it "should extract the title from the hash" do
Puppet::Type.type(:mount).new(:title => "/yay").title.should == "/yay"
end
it "should work when hash keys are provided as strings" do
Puppet::Type.type(:mount).new("title" => "/yay").title.should == "/yay"
end
it "should work when hash keys are provided as symbols" do
Puppet::Type.type(:mount).new(:title => "/yay").title.should == "/yay"
end
it "should use the name from the hash as the title if no explicit title is provided" do
Puppet::Type.type(:mount).new(:name => "/yay").title.should == "/yay"
end
it "should use the Resource Type's namevar to determine how to find the name in the hash" do
Puppet::Type.type(:file).new(:path => "/yay").title.should == "/yay"
end
[:catalog].each do |param|
it "should extract '#{param}' from the hash if present" do
Puppet::Type.type(:mount).new(:name => "/yay", param => "foo").send(param).should == "foo"
end
end
it "should use any remaining hash keys as its parameters" do
resource = Puppet::Type.type(:mount).new(:title => "/foo", :catalog => "foo", :atboot => true, :fstype => "boo")
resource[:fstype].must == "boo"
resource[:atboot].must == true
end
end
it "should fail if any invalid attributes have been provided" do
lambda { Puppet::Type.type(:mount).new(:title => "/foo", :nosuchattr => "whatever") }.should raise_error(Puppet::Error)
end
it "should set its name to the resource's title if the resource does not have a :name or namevar parameter set" do
resource = Puppet::Resource.new(:mount, "/foo")
Puppet::Type.type(:mount).new(resource).name.should == "/foo"
end
it "should fail if no title, name, or namevar are provided" do
lambda { Puppet::Type.type(:file).new(:atboot => true) }.should raise_error(Puppet::Error)
end
it "should set the attributes in the order returned by the class's :allattrs method" do
Puppet::Type.type(:mount).stubs(:allattrs).returns([:name, :atboot, :noop])
resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "myname", :atboot => "myboot", :noop => "whatever"})
set = []
Puppet::Type.type(:mount).any_instance.stubs(:newattr).with do |param, hash|
set << param
true
end.returns(stub_everything("a property"))
Puppet::Type.type(:mount).new(resource)
set[-1].should == :noop
set[-2].should == :atboot
end
it "should always set the name and then default provider before anything else" do
Puppet::Type.type(:mount).stubs(:allattrs).returns([:provider, :name, :atboot])
resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "myname", :atboot => "myboot"})
set = []
Puppet::Type.type(:mount).any_instance.stubs(:newattr).with do |param, hash|
set << param
true
end.returns(stub_everything("a property"))
Puppet::Type.type(:mount).new(resource)
set[0].should == :name
set[1].should == :provider
end
# This one is really hard to test :/
it "should each default immediately if no value is provided" do
defaults = []
Puppet::Type.type(:package).any_instance.stubs(:set_default).with { |value| defaults << value; true }
Puppet::Type.type(:package).new :name => "whatever"
defaults[0].should == :provider
end
it "should retain a copy of the originally provided parameters" do
Puppet::Type.type(:mount).new(:name => "foo", :atboot => true, :noop => false).original_parameters.should == {:atboot => true, :noop => false}
end
it "should delete the name via the namevar from the originally provided parameters" do
Puppet::Type.type(:file).new(:name => "/foo").original_parameters[:path].should be_nil
end
end
it "should have a class method for converting a hash into a Puppet::Resource instance" do
Puppet::Type.type(:mount).must respond_to(:hash2resource)
end
describe "when converting a hash to a Puppet::Resource instance" do
before do
@type = Puppet::Type.type(:mount)
end
it "should treat a :title key as the title of the resource" do
@type.hash2resource(:name => "/foo", :title => "foo").title.should == "foo"
end
it "should use the name from the hash as the title if no explicit title is provided" do
@type.hash2resource(:name => "foo").title.should == "foo"
end
it "should use the Resource Type's namevar to determine how to find the name in the hash" do
@type.stubs(:key_attributes).returns([ :myname ])
@type.hash2resource(:myname => "foo").title.should == "foo"
end
[:catalog].each do |attr|
it "should use any provided #{attr}" do
@type.hash2resource(:name => "foo", attr => "eh").send(attr).should == "eh"
end
end
it "should set all provided parameters on the resource" do
@type.hash2resource(:name => "foo", :fstype => "boo", :boot => "fee").to_hash.should == {:name => "foo", :fstype => "boo", :boot => "fee"}
end
it "should not set the title as a parameter on the resource" do
@type.hash2resource(:name => "foo", :title => "eh")[:title].should be_nil
end
it "should not set the catalog as a parameter on the resource" do
@type.hash2resource(:name => "foo", :catalog => "eh")[:catalog].should be_nil
end
it "should treat hash keys equivalently whether provided as strings or symbols" do
resource = @type.hash2resource("name" => "foo", "title" => "eh", "fstype" => "boo")
resource.title.should == "eh"
resource[:name].should == "foo"
resource[:fstype].should == "boo"
end
end
describe "when retrieving current property values" do
before do
@resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
@resource.property(:ensure).stubs(:retrieve).returns :absent
end
it "should fail if its provider is unsuitable" do
@resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
@resource.provider.class.expects(:suitable?).returns false
lambda { @resource.retrieve_resource }.should raise_error(Puppet::Error)
end
it "should return a Puppet::Resource instance with its type and title set appropriately" do
result = @resource.retrieve_resource
result.should be_instance_of(Puppet::Resource)
result.type.should == "Mount"
result.title.should == "foo"
end
it "should set the name of the returned resource if its own name and title differ" do
@resource[:name] = "my name"
@resource.title = "other name"
@resource.retrieve_resource[:name].should == "my name"
end
it "should provide a value for all set properties" do
values = @resource.retrieve_resource
[:ensure, :fstype, :pass].each { |property| values[property].should_not be_nil }
end
it "should provide a value for 'ensure' even if no desired value is provided" do
@resource = Puppet::Type.type(:file).new(:path => "/my/file/that/can't/exist")
end
it "should not call retrieve on non-ensure properties if the resource is absent and should consider the property absent" do
@resource.property(:ensure).expects(:retrieve).returns :absent
@resource.property(:fstype).expects(:retrieve).never
@resource.retrieve_resource[:fstype].should == :absent
end
it "should include the result of retrieving each property's current value if the resource is present" do
@resource.property(:ensure).expects(:retrieve).returns :present
@resource.property(:fstype).expects(:retrieve).returns 15
@resource.retrieve_resource[:fstype] == 15
end
end
describe ".title_patterns" do
describe "when there's one namevar" do
before do
@type_class = Puppet::Type.type(:notify)
@type_class.stubs(:key_attributes).returns([:one])
end
it "should have a default pattern for when there's one namevar" do
patterns = @type_class.title_patterns
patterns.length.should == 1
patterns[0].length.should == 2
end
it "should have a regexp that captures the entire string" do
patterns = @type_class.title_patterns
string = "abc\n\tdef"
patterns[0][0] =~ string
$1.should == "abc\n\tdef"
end
end
end
describe "when in a catalog" do
before do
@catalog = Puppet::Resource::Catalog.new
@container = Puppet::Type.type(:component).new(:name => "container")
@one = Puppet::Type.type(:file).new(:path => "/file/one")
@two = Puppet::Type.type(:file).new(:path => "/file/two")
@catalog.add_resource @container
@catalog.add_resource @one
@catalog.add_resource @two
@catalog.add_edge @container, @one
@catalog.add_edge @container, @two
end
it "should have no parent if there is no in edge" do
@container.parent.should be_nil
end
it "should set its parent to its in edge" do
@one.parent.ref.should == @container.ref
end
after do
@catalog.clear(true)
end
end
it "should have a 'stage' metaparam" do
Puppet::Type.metaparamclass(:stage).should be_instance_of(Class)
end
end
describe Puppet::Type::RelationshipMetaparam do
it "should be a subclass of Puppet::Parameter" do
Puppet::Type::RelationshipMetaparam.superclass.should equal(Puppet::Parameter)
end
it "should be able to produce a list of subclasses" do
Puppet::Type::RelationshipMetaparam.should respond_to(:subclasses)
end
describe "when munging relationships" do
before do
@resource = Puppet::Type.type(:mount).new :name => "/foo"
@metaparam = Puppet::Type.metaparamclass(:require).new :resource => @resource
end
it "should accept Puppet::Resource instances" do
ref = Puppet::Resource.new(:file, "/foo")
@metaparam.munge(ref)[0].should equal(ref)
end
it "should turn any string into a Puppet::Resource" do
@metaparam.munge("File[/ref]")[0].should be_instance_of(Puppet::Resource)
end
end
it "should be able to validate relationships" do
Puppet::Type.metaparamclass(:require).new(:resource => mock("resource")).should respond_to(:validate_relationship)
end
it "should fail if any specified resource is not found in the catalog" do
catalog = mock 'catalog'
resource = stub 'resource', :catalog => catalog, :ref => "resource"
param = Puppet::Type.metaparamclass(:require).new(:resource => resource, :value => %w{Foo[bar] Class[test]})
catalog.expects(:resource).with("Foo[bar]").returns "something"
catalog.expects(:resource).with("Class[Test]").returns nil
param.expects(:fail).with { |string| string.include?("Class[Test]") }
param.validate_relationship
end
end
describe Puppet::Type.metaparamclass(:check) do
it "should warn and create an instance of ':audit'" do
file = Puppet::Type.type(:file).new :path => "/foo"
file.expects(:warning)
file[:check] = :mode
file[:audit].should == [:mode]
end
end
describe Puppet::Type.metaparamclass(:audit) do
before do
@resource = Puppet::Type.type(:file).new :path => "/foo"
end
it "should default to being nil" do
@resource[:audit].should be_nil
end
it "should specify all possible properties when asked to audit all properties" do
@resource[:audit] = :all
list = @resource.class.properties.collect { |p| p.name }
@resource[:audit].should == list
end
it "should fail if asked to audit an invalid property" do
lambda { @resource[:audit] = :foobar }.should raise_error(Puppet::Error)
end
it "should create an attribute instance for each auditable property" do
@resource[:audit] = :mode
@resource.parameter(:mode).should_not be_nil
end
it "should accept properties specified as a string" do
@resource[:audit] = "mode"
@resource.parameter(:mode).should_not be_nil
end
it "should not create attribute instances for parameters, only properties" do
@resource[:audit] = :noop
@resource.parameter(:noop).should be_nil
end
end
diff --git a/spec/unit/util/settings_spec.rb b/spec/unit/util/settings_spec.rb
index 81cab79e7..7bca44b76 100755
--- a/spec/unit/util/settings_spec.rb
+++ b/spec/unit/util/settings_spec.rb
@@ -1,1122 +1,1099 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
describe Puppet::Util::Settings do
describe "when specifying defaults" do
before do
@settings = Puppet::Util::Settings.new
end
it "should start with no defined parameters" do
@settings.params.length.should == 0
end
it "should allow specification of default values associated with a section as an array" do
@settings.setdefaults(:section, :myvalue => ["defaultval", "my description"])
end
it "should not allow duplicate parameter specifications" do
@settings.setdefaults(:section, :myvalue => ["a", "b"])
lambda { @settings.setdefaults(:section, :myvalue => ["c", "d"]) }.should raise_error(ArgumentError)
end
it "should allow specification of default values associated with a section as a hash" do
@settings.setdefaults(:section, :myvalue => {:default => "defaultval", :desc => "my description"})
end
it "should consider defined parameters to be valid" do
@settings.setdefaults(:section, :myvalue => ["defaultval", "my description"])
@settings.valid?(:myvalue).should be_true
end
it "should require a description when defaults are specified with an array" do
lambda { @settings.setdefaults(:section, :myvalue => ["a value"]) }.should raise_error(ArgumentError)
end
it "should require a description when defaults are specified with a hash" do
lambda { @settings.setdefaults(:section, :myvalue => {:default => "a value"}) }.should raise_error(ArgumentError)
end
it "should raise an error if we can't guess the type" do
lambda { @settings.setdefaults(:section, :myvalue => {:default => Object.new, :desc => "An impossible object"}) }.should raise_error(ArgumentError)
end
it "should support specifying owner, group, and mode when specifying files" do
@settings.setdefaults(:section, :myvalue => {:default => "/some/file", :owner => "service", :mode => "boo", :group => "service", :desc => "whatever"})
end
it "should support specifying a short name" do
@settings.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"})
end
it "should support specifying the setting type" do
@settings.setdefaults(:section, :myvalue => {:default => "/w", :desc => "b", :type => :setting})
@settings.setting(:myvalue).should be_instance_of(Puppet::Util::Settings::Setting)
end
it "should fail if an invalid setting type is specified" do
lambda { @settings.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :type => :foo}) }.should raise_error(ArgumentError)
end
it "should fail when short names conflict" do
@settings.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"})
lambda { @settings.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) }.should raise_error(ArgumentError)
end
end
describe "when setting values" do
before do
@settings = Puppet::Util::Settings.new
@settings.setdefaults :main, :myval => ["val", "desc"]
@settings.setdefaults :main, :bool => [true, "desc"]
end
it "should provide a method for setting values from other objects" do
@settings[:myval] = "something else"
@settings[:myval].should == "something else"
end
it "should support a getopt-specific mechanism for setting values" do
@settings.handlearg("--myval", "newval")
@settings[:myval].should == "newval"
end
it "should support a getopt-specific mechanism for turning booleans off" do
@settings[:bool] = true
@settings.handlearg("--no-bool", "")
@settings[:bool].should == false
end
it "should support a getopt-specific mechanism for turning booleans on" do
# Turn it off first
@settings[:bool] = false
@settings.handlearg("--bool", "")
@settings[:bool].should == true
end
it "should consider a cli setting with no argument to be a boolean" do
# Turn it off first
@settings[:bool] = false
@settings.handlearg("--bool")
@settings[:bool].should == true
end
it "should consider a cli setting with an empty string as an argument to be a boolean, if the setting itself is a boolean" do
# Turn it off first
@settings[:bool] = false
@settings.handlearg("--bool", "")
@settings[:bool].should == true
end
it "should consider a cli setting with an empty string as an argument to be an empty argument, if the setting itself is not a boolean" do
@settings[:myval] = "bob"
@settings.handlearg("--myval", "")
@settings[:myval].should == ""
end
it "should consider a cli setting with a boolean as an argument to be a boolean" do
# Turn it off first
@settings[:bool] = false
@settings.handlearg("--bool", "true")
@settings[:bool].should == true
end
it "should not consider a cli setting of a non boolean with a boolean as an argument to be a boolean" do
# Turn it off first
@settings[:myval] = "bob"
@settings.handlearg("--no-myval", "")
@settings[:myval].should == ""
end
it "should clear the cache when setting getopt-specific values" do
@settings.setdefaults :mysection, :one => ["whah", "yay"], :two => ["$one yay", "bah"]
@settings[:two].should == "whah yay"
@settings.handlearg("--one", "else")
@settings[:two].should == "else yay"
end
it "should not clear other values when setting getopt-specific values" do
@settings[:myval] = "yay"
@settings.handlearg("--no-bool", "")
@settings[:myval].should == "yay"
end
it "should clear the list of used sections" do
@settings.expects(:clearused)
@settings[:myval] = "yay"
end
it "should call passed blocks when values are set" do
values = []
@settings.setdefaults(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }})
values.should == []
@settings[:hooker] = "something"
values.should == %w{something}
end
it "should call passed blocks when values are set via the command line" do
values = []
@settings.setdefaults(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }})
values.should == []
@settings.handlearg("--hooker", "yay")
values.should == %w{yay}
end
it "should provide an option to call passed blocks during definition" do
values = []
@settings.setdefaults(:section, :hooker => {:default => "yay", :desc => "boo", :call_on_define => true, :hook => lambda { |v| values << v }})
values.should == %w{yay}
end
it "should pass the fully interpolated value to the hook when called on definition" do
values = []
@settings.setdefaults(:section, :one => ["test", "a"])
@settings.setdefaults(:section, :hooker => {:default => "$one/yay", :desc => "boo", :call_on_define => true, :hook => lambda { |v| values << v }})
values.should == %w{test/yay}
end
it "should munge values using the setting-specific methods" do
@settings[:bool] = "false"
@settings[:bool].should == false
end
it "should prefer cli values to values set in Ruby code" do
@settings.handlearg("--myval", "cliarg")
@settings[:myval] = "memarg"
@settings[:myval].should == "cliarg"
end
it "should clear the list of environments" do
Puppet::Node::Environment.expects(:clear).at_least(1)
@settings[:myval] = "memarg"
end
it "should raise an error if we try to set 'name'" do
lambda{ @settings[:name] = "foo" }.should raise_error(ArgumentError)
end
it "should raise an error if we try to set 'run_mode'" do
lambda{ @settings[:run_mode] = "foo" }.should raise_error(ArgumentError)
end
it "should warn and use [master] if we ask for [puppetmasterd]" do
Puppet.expects(:warning)
@settings.set_value(:myval, "foo", :puppetmasterd)
@settings.stubs(:run_mode).returns(:master)
@settings.value(:myval).should == "foo"
end
it "should warn and use [agent] if we ask for [puppetd]" do
Puppet.expects(:warning)
@settings.set_value(:myval, "foo", :puppetd)
@settings.stubs(:run_mode).returns(:agent)
@settings.value(:myval).should == "foo"
end
end
describe "when returning values" do
before do
@settings = Puppet::Util::Settings.new
@settings.setdefaults :section, :config => ["/my/file", "eh"], :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"], :four => ["$two $three FOUR", "d"]
FileTest.stubs(:exist?).returns true
end
it "should provide a mechanism for returning set values" do
@settings[:one] = "other"
@settings[:one].should == "other"
end
it "should interpolate default values for other parameters into returned parameter values" do
@settings[:one].should == "ONE"
@settings[:two].should == "ONE TWO"
@settings[:three].should == "ONE ONE TWO THREE"
end
it "should interpolate default values that themselves need to be interpolated" do
@settings[:four].should == "ONE TWO ONE ONE TWO THREE FOUR"
end
it "should provide a method for returning uninterpolated values" do
@settings[:two] = "$one tw0"
@settings.uninterpolated_value(:two).should == "$one tw0"
@settings.uninterpolated_value(:four).should == "$two $three FOUR"
end
it "should interpolate set values for other parameters into returned parameter values" do
@settings[:one] = "on3"
@settings[:two] = "$one tw0"
@settings[:three] = "$one $two thr33"
@settings[:four] = "$one $two $three f0ur"
@settings[:one].should == "on3"
@settings[:two].should == "on3 tw0"
@settings[:three].should == "on3 on3 tw0 thr33"
@settings[:four].should == "on3 on3 tw0 on3 on3 tw0 thr33 f0ur"
end
it "should not cache interpolated values such that stale information is returned" do
@settings[:two].should == "ONE TWO"
@settings[:one] = "one"
@settings[:two].should == "one TWO"
end
it "should not cache values such that information from one environment is returned for another environment" do
text = "[env1]\none = oneval\n[env2]\none = twoval\n"
@settings.stubs(:read_file).returns(text)
@settings.parse
@settings.value(:one, "env1").should == "oneval"
@settings.value(:one, "env2").should == "twoval"
end
it "should have a run_mode that defaults to user" do
@settings.run_mode.should == :user
end
end
describe "when choosing which value to return" do
before do
@settings = Puppet::Util::Settings.new
@settings.setdefaults :section,
:config => ["/my/file", "a"],
:one => ["ONE", "a"]
FileTest.stubs(:exist?).returns true
Puppet.stubs(:run_mode).returns stub('run_mode', :name => :mymode)
end
it "should return default values if no values have been set" do
@settings[:one].should == "ONE"
end
it "should return values set on the cli before values set in the configuration file" do
text = "[main]\none = fileval\n"
@settings.stubs(:read_file).returns(text)
@settings.handlearg("--one", "clival")
@settings.parse
@settings[:one].should == "clival"
end
it "should return values set on the cli before values set in Ruby" do
@settings[:one] = "rubyval"
@settings.handlearg("--one", "clival")
@settings[:one].should == "clival"
end
it "should return values set in the mode-specific section before values set in the main section" do
text = "[main]\none = mainval\n[mymode]\none = modeval\n"
@settings.stubs(:read_file).returns(text)
@settings.parse
@settings[:one].should == "modeval"
end
it "should not return values outside of its search path" do
text = "[other]\none = oval\n"
file = "/some/file"
@settings.stubs(:read_file).returns(text)
@settings.parse
@settings[:one].should == "ONE"
end
it "should return values in a specified environment" do
text = "[env]\none = envval\n"
@settings.stubs(:read_file).returns(text)
@settings.parse
@settings.value(:one, "env").should == "envval"
end
it "should interpolate found values using the current environment" do
@settings.setdefaults :main, :myval => ["$environment/foo", "mydocs"]
@settings.value(:myval, "myenv").should == "myenv/foo"
end
it "should return values in a specified environment before values in the main or name sections" do
text = "[env]\none = envval\n[main]\none = mainval\n[myname]\none = nameval\n"
@settings.stubs(:read_file).returns(text)
@settings.parse
@settings.value(:one, "env").should == "envval"
end
end
describe "when parsing its configuration" do
before do
@settings = Puppet::Util::Settings.new
@settings.stubs(:service_user_available?).returns true
@file = "/some/file"
@settings.setdefaults :section, :user => ["suser", "doc"], :group => ["sgroup", "doc"]
@settings.setdefaults :section, :config => ["/some/file", "eh"], :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"]
FileTest.stubs(:exist?).returns true
end
it "should not ignore the report setting" do
@settings.setdefaults :section, :report => ["false", "a"]
myfile = stub "myfile"
@settings[:config] = myfile
text = <<-CONF
[puppetd]
report=true
CONF
@settings.expects(:read_file).returns(text)
@settings.parse
@settings[:report].should be_true
end
it "should use its current ':config' value for the file to parse" do
myfile = Puppet.features.posix? ? "/my/file" : "C:/myfile" # do not stub expand_path here, as this leads to a stack overflow, when mocha tries to use it
@settings[:config] = myfile
File.expects(:read).with(myfile).returns "[main]"
@settings.parse
end
it "should fail if no configuration setting is defined" do
@settings = Puppet::Util::Settings.new
lambda { @settings.parse }.should raise_error(RuntimeError)
end
it "should not try to parse non-existent files" do
FileTest.expects(:exist?).with("/some/file").returns false
File.expects(:read).with("/some/file").never
@settings.parse
end
it "should set a timer that triggers reparsing, even if the file does not exist" do
FileTest.expects(:exist?).returns false
@settings.expects(:set_filetimeout_timer)
@settings.parse
end
it "should return values set in the configuration file" do
text = "[main]
one = fileval
"
@settings.expects(:read_file).returns(text)
@settings.parse
@settings[:one].should == "fileval"
end
#484 - this should probably be in the regression area
it "should not throw an exception on unknown parameters" do
text = "[main]\nnosuchparam = mval\n"
@settings.expects(:read_file).returns(text)
lambda { @settings.parse }.should_not raise_error
end
it "should convert booleans in the configuration file into Ruby booleans" do
text = "[main]
one = true
two = false
"
@settings.expects(:read_file).returns(text)
@settings.parse
@settings[:one].should == true
@settings[:two].should == false
end
it "should convert integers in the configuration file into Ruby Integers" do
text = "[main]
one = 65
"
@settings.expects(:read_file).returns(text)
@settings.parse
@settings[:one].should == 65
end
it "should support specifying all metadata (owner, group, mode) in the configuration file" do
@settings.setdefaults :section, :myfile => ["/myfile", "a"]
text = "[main]
myfile = /other/file {owner = service, group = service, mode = 644}
"
@settings.expects(:read_file).returns(text)
@settings.parse
@settings[:myfile].should == "/other/file"
@settings.metadata(:myfile).should == {:owner => "suser", :group => "sgroup", :mode => "644"}
end
it "should support specifying a single piece of metadata (owner, group, or mode) in the configuration file" do
@settings.setdefaults :section, :myfile => ["/myfile", "a"]
text = "[main]
myfile = /other/file {owner = service}
"
file = "/some/file"
@settings.expects(:read_file).returns(text)
@settings.parse
@settings[:myfile].should == "/other/file"
@settings.metadata(:myfile).should == {:owner => "suser"}
end
it "should call hooks associated with values set in the configuration file" do
values = []
@settings.setdefaults :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }}
text = "[main]
mysetting = setval
"
@settings.expects(:read_file).returns(text)
@settings.parse
values.should == ["setval"]
end
it "should not call the same hook for values set multiple times in the configuration file" do
values = []
@settings.setdefaults :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }}
text = "[user]
mysetting = setval
[main]
mysetting = other
"
@settings.expects(:read_file).returns(text)
@settings.parse
values.should == ["setval"]
end
it "should pass the environment-specific value to the hook when one is available" do
values = []
@settings.setdefaults :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }}
@settings.setdefaults :section, :environment => ["yay", "a"]
@settings.setdefaults :section, :environments => ["yay,foo", "a"]
text = "[main]
mysetting = setval
[yay]
mysetting = other
"
@settings.expects(:read_file).returns(text)
@settings.parse
values.should == ["other"]
end
it "should pass the interpolated value to the hook when one is available" do
values = []
@settings.setdefaults :section, :base => {:default => "yay", :desc => "a", :hook => proc { |v| values << v }}
@settings.setdefaults :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }}
text = "[main]
mysetting = $base/setval
"
@settings.expects(:read_file).returns(text)
@settings.parse
values.should == ["yay/setval"]
end
it "should allow empty values" do
@settings.setdefaults :section, :myarg => ["myfile", "a"]
text = "[main]
myarg =
"
@settings.stubs(:read_file).returns(text)
@settings.parse
@settings[:myarg].should == ""
end
describe "and when reading a non-positive filetimeout value from the config file" do
before do
@settings.setdefaults :foo, :filetimeout => [5, "eh"]
somefile = "/some/file"
text = "[main]
filetimeout = -1
"
File.expects(:read).with(somefile).returns(text)
File.expects(:expand_path).with(somefile).returns somefile
@settings[:config] = somefile
end
it "should not set a timer" do
EventLoop::Timer.expects(:new).never
@settings.parse
end
end
end
describe "when reparsing its configuration" do
before do
@settings = Puppet::Util::Settings.new
@settings.setdefaults :section, :config => ["/test/file", "a"], :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"]
FileTest.stubs(:exist?).returns true
end
it "should use a LoadedFile instance to determine if the file has changed" do
file = mock 'file'
Puppet::Util::LoadedFile.expects(:new).with("/test/file").returns file
file.expects(:changed?)
@settings.stubs(:parse)
@settings.reparse
end
it "should not create the LoadedFile instance and should not parse if the file does not exist" do
FileTest.expects(:exist?).with("/test/file").returns false
Puppet::Util::LoadedFile.expects(:new).never
@settings.expects(:parse).never
@settings.reparse
end
it "should not reparse if the file has not changed" do
file = mock 'file'
Puppet::Util::LoadedFile.expects(:new).with("/test/file").returns file
file.expects(:changed?).returns false
@settings.expects(:parse).never
@settings.reparse
end
it "should reparse if the file has changed" do
file = stub 'file', :file => "/test/file"
Puppet::Util::LoadedFile.expects(:new).with("/test/file").returns file
file.expects(:changed?).returns true
@settings.expects(:parse)
@settings.reparse
end
it "should use a cached LoadedFile instance" do
first = mock 'first'
second = mock 'second'
Puppet::Util::LoadedFile.expects(:new).times(2).with("/test/file").returns(first).then.returns(second)
@settings.file.should equal(first)
Puppet::Util::Cacher.expire
@settings.file.should equal(second)
end
it "should replace in-memory values with on-file values" do
# Init the value
text = "[main]\none = disk-init\n"
file = mock 'file'
file.stubs(:changed?).returns(true)
file.stubs(:file).returns("/test/file")
@settings[:one] = "init"
@settings.file = file
# Now replace the value
text = "[main]\none = disk-replace\n"
# This is kinda ridiculous - the reason it parses twice is that
# it goes to parse again when we ask for the value, because the
# mock always says it should get reparsed.
@settings.stubs(:read_file).returns(text)
@settings.reparse
@settings[:one].should == "disk-replace"
end
it "should retain parameters set by cli when configuration files are reparsed" do
@settings.handlearg("--one", "clival")
text = "[main]\none = on-disk\n"
@settings.stubs(:read_file).returns(text)
@settings.parse
@settings[:one].should == "clival"
end
it "should remove in-memory values that are no longer set in the file" do
# Init the value
text = "[main]\none = disk-init\n"
@settings.expects(:read_file).returns(text)
@settings.parse
@settings[:one].should == "disk-init"
# Now replace the value
text = "[main]\ntwo = disk-replace\n"
@settings.expects(:read_file).returns(text)
@settings.parse
#@settings.reparse
# The originally-overridden value should be replaced with the default
@settings[:one].should == "ONE"
# and we should now have the new value in memory
@settings[:two].should == "disk-replace"
end
it "should retain in-memory values if the file has a syntax error" do
# Init the value
text = "[main]\none = initial-value\n"
@settings.expects(:read_file).returns(text)
@settings.parse
@settings[:one].should == "initial-value"
# Now replace the value with something bogus
text = "[main]\nkenny = killed-by-what-follows\n1 is 2, blah blah florp\n"
@settings.expects(:read_file).returns(text)
@settings.parse
# The originally-overridden value should not be replaced with the default
@settings[:one].should == "initial-value"
# and we should not have the new value in memory
@settings[:kenny].should be_nil
end
end
it "should provide a method for creating a catalog of resources from its configuration" do
Puppet::Util::Settings.new.should respond_to(:to_catalog)
end
describe "when creating a catalog" do
before do
@settings = Puppet::Util::Settings.new
@settings.stubs(:service_user_available?).returns true
@prefix = Puppet.features.posix? ? "" : "C:"
end
it "should add all file resources to the catalog if no sections have been specified" do
@settings.setdefaults :main, :maindir => [@prefix+"/maindir", "a"], :seconddir => [@prefix+"/seconddir", "a"]
@settings.setdefaults :other, :otherdir => [@prefix+"/otherdir", "a"]
catalog = @settings.to_catalog
[@prefix+"/maindir", @prefix+"/seconddir", @prefix+"/otherdir"].each do |path|
catalog.resource(:file, path).should be_instance_of(Puppet::Resource)
end
end
it "should add only files in the specified sections if section names are provided" do
@settings.setdefaults :main, :maindir => [@prefix+"/maindir", "a"]
@settings.setdefaults :other, :otherdir => [@prefix+"/otherdir", "a"]
catalog = @settings.to_catalog(:main)
catalog.resource(:file, @prefix+"/otherdir").should be_nil
catalog.resource(:file, @prefix+"/maindir").should be_instance_of(Puppet::Resource)
end
it "should not try to add the same file twice" do
@settings.setdefaults :main, :maindir => [@prefix+"/maindir", "a"]
@settings.setdefaults :other, :otherdir => [@prefix+"/maindir", "a"]
lambda { @settings.to_catalog }.should_not raise_error
end
it "should ignore files whose :to_resource method returns nil" do
@settings.setdefaults :main, :maindir => [@prefix+"/maindir", "a"]
@settings.setting(:maindir).expects(:to_resource).returns nil
Puppet::Resource::Catalog.any_instance.expects(:add_resource).never
@settings.to_catalog
end
describe "when adding users and groups to the catalog" do
before do
Puppet.features.stubs(:root?).returns true
@settings.setdefaults :foo, :mkusers => [true, "e"], :user => ["suser", "doc"], :group => ["sgroup", "doc"]
@settings.setdefaults :other, :otherdir => {:default => "/otherdir", :desc => "a", :owner => "service", :group => "service"}
@catalog = @settings.to_catalog
end
it "should add each specified user and group to the catalog if :mkusers is a valid setting, is enabled, and we're running as root" do
@catalog.resource(:user, "suser").should be_instance_of(Puppet::Resource)
@catalog.resource(:group, "sgroup").should be_instance_of(Puppet::Resource)
end
it "should only add users and groups to the catalog from specified sections" do
@settings.setdefaults :yay, :yaydir => {:default => "/yaydir", :desc => "a", :owner => "service", :group => "service"}
catalog = @settings.to_catalog(:other)
catalog.resource(:user, "jane").should be_nil
catalog.resource(:group, "billy").should be_nil
end
it "should not add users or groups to the catalog if :mkusers not running as root" do
Puppet.features.stubs(:root?).returns false
catalog = @settings.to_catalog
catalog.resource(:user, "suser").should be_nil
catalog.resource(:group, "sgroup").should be_nil
end
it "should not add users or groups to the catalog if :mkusers is not a valid setting" do
Puppet.features.stubs(:root?).returns true
settings = Puppet::Util::Settings.new
settings.setdefaults :other, :otherdir => {:default => "/otherdir", :desc => "a", :owner => "service", :group => "service"}
catalog = settings.to_catalog
catalog.resource(:user, "suser").should be_nil
catalog.resource(:group, "sgroup").should be_nil
end
it "should not add users or groups to the catalog if :mkusers is a valid setting but is disabled" do
@settings[:mkusers] = false
catalog = @settings.to_catalog
catalog.resource(:user, "suser").should be_nil
catalog.resource(:group, "sgroup").should be_nil
end
it "should not try to add users or groups to the catalog twice" do
@settings.setdefaults :yay, :yaydir => {:default => "/yaydir", :desc => "a", :owner => "service", :group => "service"}
# This would fail if users/groups were added twice
lambda { @settings.to_catalog }.should_not raise_error
end
it "should set :ensure to :present on each created user and group" do
@catalog.resource(:user, "suser")[:ensure].should == :present
@catalog.resource(:group, "sgroup")[:ensure].should == :present
end
it "should set each created user's :gid to the service group" do
@settings.to_catalog.resource(:user, "suser")[:gid].should == "sgroup"
end
it "should not attempt to manage the root user" do
Puppet.features.stubs(:root?).returns true
@settings.setdefaults :foo, :foodir => {:default => "/foodir", :desc => "a", :owner => "root", :group => "service"}
@settings.to_catalog.resource(:user, "root").should be_nil
end
end
end
it "should be able to be converted to a manifest" do
Puppet::Util::Settings.new.should respond_to(:to_manifest)
end
describe "when being converted to a manifest" do
it "should produce a string with the code for each resource joined by two carriage returns" do
@settings = Puppet::Util::Settings.new
@settings.setdefaults :main, :maindir => ["/maindir", "a"], :seconddir => ["/seconddir", "a"]
main = stub 'main_resource', :ref => "File[/maindir]"
main.expects(:to_manifest).returns "maindir"
second = stub 'second_resource', :ref => "File[/seconddir]"
second.expects(:to_manifest).returns "seconddir"
@settings.setting(:maindir).expects(:to_resource).returns main
@settings.setting(:seconddir).expects(:to_resource).returns second
@settings.to_manifest.split("\n\n").sort.should == %w{maindir seconddir}
end
end
describe "when using sections of the configuration to manage the local host" do
before do
@settings = Puppet::Util::Settings.new
@settings.stubs(:service_user_available?).returns true
@settings.setdefaults :main, :noop => [false, ""]
@settings.setdefaults :main, :maindir => ["/maindir", "a"], :seconddir => ["/seconddir", "a"]
@settings.setdefaults :main, :user => ["suser", "doc"], :group => ["sgroup", "doc"]
@settings.setdefaults :other, :otherdir => {:default => "/otherdir", :desc => "a", :owner => "service", :group => "service", :mode => 0755}
@settings.setdefaults :third, :thirddir => ["/thirddir", "b"]
@settings.setdefaults :files, :myfile => {:default => "/myfile", :desc => "a", :mode => 0755}
end
it "should provide a method that writes files with the correct modes" do
@settings.should respond_to(:write)
end
it "should provide a method that creates directories with the correct modes" do
Puppet::Util::SUIDManager.expects(:asuser).with("suser", "sgroup").yields
Dir.expects(:mkdir).with("/otherdir", 0755)
@settings.mkdir(:otherdir)
end
it "should create a catalog with the specified sections" do
@settings.expects(:to_catalog).with(:main, :other).returns Puppet::Resource::Catalog.new("foo")
@settings.use(:main, :other)
end
it "should canonicalize the sections" do
@settings.expects(:to_catalog).with(:main, :other).returns Puppet::Resource::Catalog.new("foo")
@settings.use("main", "other")
end
it "should ignore sections that have already been used" do
@settings.expects(:to_catalog).with(:main).returns Puppet::Resource::Catalog.new("foo")
@settings.use(:main)
@settings.expects(:to_catalog).with(:other).returns Puppet::Resource::Catalog.new("foo")
@settings.use(:main, :other)
end
it "should ignore tags and schedules when creating files and directories"
it "should be able to provide all of its parameters in a format compatible with GetOpt::Long" do
pending "Not converted from test/unit yet"
end
it "should convert the created catalog to a RAL catalog" do
@catalog = Puppet::Resource::Catalog.new("foo")
@settings.expects(:to_catalog).with(:main).returns @catalog
@catalog.expects(:to_ral).returns @catalog
@settings.use(:main)
end
it "should specify that it is not managing a host catalog" do
catalog = Puppet::Resource::Catalog.new("foo")
catalog.expects(:apply)
@settings.expects(:to_catalog).returns catalog
catalog.stubs(:to_ral).returns catalog
catalog.expects(:host_config=).with false
@settings.use(:main)
end
it "should support a method for re-using all currently used sections" do
@settings.expects(:to_catalog).with(:main, :third).times(2).returns Puppet::Resource::Catalog.new("foo")
@settings.use(:main, :third)
@settings.reuse
end
it "should fail with an appropriate message if any resources fail" do
@catalog = Puppet::Resource::Catalog.new("foo")
@catalog.stubs(:to_ral).returns @catalog
@settings.expects(:to_catalog).returns @catalog
@trans = mock("transaction")
@catalog.expects(:apply).yields(@trans)
@trans.expects(:any_failed?).returns(true)
report = mock 'report'
@trans.expects(:report).returns report
log = mock 'log', :to_s => "My failure", :level => :err
report.expects(:logs).returns [log]
@settings.expects(:raise).with { |msg| msg.include?("My failure") }
@settings.use(:whatever)
end
end
describe "when dealing with printing configs" do
before do
@settings = Puppet::Util::Settings.new
#these are the magic default values
@settings.stubs(:value).with(:configprint).returns("")
@settings.stubs(:value).with(:genconfig).returns(false)
@settings.stubs(:value).with(:genmanifest).returns(false)
@settings.stubs(:value).with(:environment).returns(nil)
end
describe "when checking print_config?" do
it "should return false when the :configprint, :genconfig and :genmanifest are not set" do
@settings.print_configs?.should be_false
end
it "should return true when :configprint has a value" do
@settings.stubs(:value).with(:configprint).returns("something")
@settings.print_configs?.should be_true
end
it "should return true when :genconfig has a value" do
@settings.stubs(:value).with(:genconfig).returns(true)
@settings.print_configs?.should be_true
end
it "should return true when :genmanifest has a value" do
@settings.stubs(:value).with(:genmanifest).returns(true)
@settings.print_configs?.should be_true
end
end
describe "when printing configs" do
describe "when :configprint has a value" do
it "should call print_config_options" do
@settings.stubs(:value).with(:configprint).returns("something")
@settings.expects(:print_config_options)
@settings.print_configs
end
it "should get the value of the option using the environment" do
@settings.stubs(:value).with(:configprint).returns("something")
@settings.stubs(:include?).with("something").returns(true)
@settings.expects(:value).with(:environment).returns("env")
@settings.expects(:value).with("something", "env").returns("foo")
@settings.stubs(:puts).with("foo")
@settings.print_configs
end
it "should print the value of the option" do
@settings.stubs(:value).with(:configprint).returns("something")
@settings.stubs(:include?).with("something").returns(true)
@settings.stubs(:value).with("something", nil).returns("foo")
@settings.expects(:puts).with("foo")
@settings.print_configs
end
it "should print the value pairs if there are multiple options" do
@settings.stubs(:value).with(:configprint).returns("bar,baz")
@settings.stubs(:include?).with("bar").returns(true)
@settings.stubs(:include?).with("baz").returns(true)
@settings.stubs(:value).with("bar", nil).returns("foo")
@settings.stubs(:value).with("baz", nil).returns("fud")
@settings.expects(:puts).with("bar = foo")
@settings.expects(:puts).with("baz = fud")
@settings.print_configs
end
it "should print a whole bunch of stuff if :configprint = all"
it "should return true after printing" do
@settings.stubs(:value).with(:configprint).returns("something")
@settings.stubs(:include?).with("something").returns(true)
@settings.stubs(:value).with("something", nil).returns("foo")
@settings.stubs(:puts).with("foo")
@settings.print_configs.should be_true
end
it "should return false if a config param is not found" do
@settings.stubs :puts
@settings.stubs(:value).with(:configprint).returns("something")
@settings.stubs(:include?).with("something").returns(false)
@settings.print_configs.should be_false
end
end
describe "when genconfig is true" do
before do
@settings.stubs :puts
end
it "should call to_config" do
@settings.stubs(:value).with(:genconfig).returns(true)
@settings.expects(:to_config)
@settings.print_configs
end
it "should return true from print_configs" do
@settings.stubs(:value).with(:genconfig).returns(true)
@settings.stubs(:to_config)
@settings.print_configs.should be_true
end
end
describe "when genmanifest is true" do
before do
@settings.stubs :puts
end
it "should call to_config" do
@settings.stubs(:value).with(:genmanifest).returns(true)
@settings.expects(:to_manifest)
@settings.print_configs
end
it "should return true from print_configs" do
@settings.stubs(:value).with(:genmanifest).returns(true)
@settings.stubs(:to_manifest)
@settings.print_configs.should be_true
end
end
end
end
describe "when setting a timer to trigger configuration file reparsing" do
before do
@settings = Puppet::Util::Settings.new
@settings.setdefaults :foo, :filetimeout => [5, "eh"]
end
it "should do nothing if no filetimeout setting is available" do
@settings.expects(:value).with(:filetimeout).returns nil
EventLoop::Timer.expects(:new).never
@settings.set_filetimeout_timer
end
it "should always convert the timer interval to an integer" do
@settings.expects(:value).with(:filetimeout).returns "10"
EventLoop::Timer.expects(:new).with(:interval => 10, :start? => true, :tolerance => 1)
@settings.set_filetimeout_timer
end
it "should do nothing if the filetimeout setting is not greater than 0" do
@settings.expects(:value).with(:filetimeout).returns -2
EventLoop::Timer.expects(:new).never
@settings.set_filetimeout_timer
end
it "should create a timer with its interval set to the filetimeout, start? set to true, and a tolerance of 1" do
@settings.expects(:value).with(:filetimeout).returns 5
EventLoop::Timer.expects(:new).with(:interval => 5, :start? => true, :tolerance => 1)
@settings.set_filetimeout_timer
end
it "should reparse when the timer goes off" do
EventLoop::Timer.expects(:new).with(:interval => 5, :start? => true, :tolerance => 1).yields
@settings.expects(:reparse)
@settings.set_filetimeout_timer
end
end
describe "when determining if the service user is available" do
it "should return false if there is no user setting" do
Puppet::Util::Settings.new.should_not be_service_user_available
end
it "should return false if the user provider says the user is missing" do
settings = Puppet::Util::Settings.new
settings.setdefaults :main, :user => ["foo", "doc"]
user = mock 'user'
user.expects(:exists?).returns false
Puppet::Type.type(:user).expects(:new).with { |args| args[:name] == "foo" }.returns user
settings.should_not be_service_user_available
end
it "should return true if the user provider says the user is present" do
settings = Puppet::Util::Settings.new
settings.setdefaults :main, :user => ["foo", "doc"]
user = mock 'user'
user.expects(:exists?).returns true
Puppet::Type.type(:user).expects(:new).with { |args| args[:name] == "foo" }.returns user
settings.should be_service_user_available
end
it "should cache the result"
end
-
- describe "#without_noop" do
- before do
- @settings = Puppet::Util::Settings.new
- @settings.setdefaults :main, :noop => [true, ""]
- end
-
- it "should set noop to false for the duration of the block" do
- @settings.without_noop { @settings.value(:noop, :cli).should be_false }
- end
-
- it "should ensure that noop is returned to its previous value" do
- @settings.without_noop { raise } rescue nil
- @settings.value(:noop, :cli).should be_true
- end
-
- it "should work even if no 'noop' setting is available" do
- settings = Puppet::Util::Settings.new
- stuff = nil
- settings.without_noop { stuff = "yay" }
- stuff.should == "yay"
- end
- end
end