diff --git a/lib/puppet/indirector/resource/ral.rb b/lib/puppet/indirector/resource/ral.rb
index 1c2ab14ae..bc41d14ae 100644
--- a/lib/puppet/indirector/resource/ral.rb
+++ b/lib/puppet/indirector/resource/ral.rb
@@ -1,48 +1,53 @@
class Puppet::Resource::Ral < Puppet::Indirector::Code
def find( request )
# find by name
res = type(request).instances.find { |o| o.name == resource_name(request) }
res ||= type(request).new(:name => resource_name(request), :audit => type(request).properties.collect { |s| s.name })
res.to_resource
end
def search( request )
conditions = request.options.dup
conditions[:name] = resource_name(request) if resource_name(request)
type(request).instances.map do |res|
res.to_resource
end.find_all do |res|
conditions.all? {|property, value| res.to_resource[property].to_s == value.to_s}
end.sort do |a,b|
a.title <=> b.title
end
end
def save( request )
# In RAL-land, to "save" means to actually try to change machine state
res = request.instance
ral_res = res.to_ral
catalog = Puppet::Resource::Catalog.new
catalog.add_resource ral_res
catalog.apply
ral_res.to_resource
end
private
+ # {type,resource}_name: the resource name may contain slashes:
+ # File["/etc/hosts"]. To handle, assume the type name does
+ # _not_ have any slashes in it, and split only on the first.
+
def type_name( request )
- request.key.split('/')[0]
+ request.key.split('/', 2)[0]
end
def resource_name( request )
- request.key.split('/')[1]
+ name = request.key.split('/', 2)[1]
+ name unless name == ""
end
def type( request )
Puppet::Type.type(type_name(request)) or raise Puppet::Error, "Could not find type #{type}"
end
end
diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb
index c60e1d4fb..fdabd05c9 100644
--- a/lib/puppet/parser/compiler.rb
+++ b/lib/puppet/parser/compiler.rb
@@ -1,474 +1,489 @@
# Created by Luke A. Kanies on 2007-08-13.
# Copyright (c) 2007. All rights reserved.
require 'puppet/node'
require 'puppet/resource/catalog'
require 'puppet/util/errors'
require 'puppet/resource/type_collection_helper'
# Maintain a graph of scopes, along with a bunch of data
# about the individual catalog we're compiling.
class Puppet::Parser::Compiler
include Puppet::Util
include Puppet::Util::Errors
include Puppet::Resource::TypeCollectionHelper
def self.compile(node)
new(node).compile.to_resource
rescue => detail
puts detail.backtrace if Puppet[:trace]
raise Puppet::Error, "#{detail} on node #{node.name}"
ensure
# We get these from the environment and only cache them in a thread
# variable for the duration of the compilation.
Thread.current[:known_resource_types] = nil
Thread.current[:env_module_directories] = nil
end
attr_reader :node, :facts, :collections, :catalog, :node_scope, :resources, :relationships
# Add a collection to the global list.
def add_collection(coll)
@collections << coll
end
def add_relationship(dep)
@relationships << dep
end
# Store a resource override.
def add_override(override)
# If possible, merge the override in immediately.
if resource = @catalog.resource(override.ref)
resource.merge(override)
else
# Otherwise, store the override for later; these
# get evaluated in Resource#finish.
@resource_overrides[override.ref] << override
end
end
# Store a resource in our resource table.
def add_resource(scope, resource)
@resources << resource
# Note that this will fail if the resource is not unique.
@catalog.add_resource(resource)
# Add our container edge. If we're a class, then we get treated specially - we can
# control the stage that the class is applied in. Otherwise, we just
# get added to our parent container.
return if resource.type.to_s.downcase == "stage"
if resource.type.to_s.downcase != "class"
raise ArgumentError, "Only classes can set 'stage'; normal resources like #{resource} cannot change run stage" if resource[:stage]
return @catalog.add_edge(scope.resource, resource)
end
unless stage = @catalog.resource(:stage, resource[:stage] || (scope && scope.resource && scope.resource[:stage]) || :main)
raise ArgumentError, "Could not find stage #{resource[:stage] || :main} specified by #{resource}"
end
resource[:stage] ||= stage.title unless stage.title == :main
@catalog.add_edge(stage, resource)
end
# Do we use nodes found in the code, vs. the external node sources?
def ast_nodes?
known_resource_types.nodes?
end
# Store the fact that we've evaluated a class
def add_class(name)
@catalog.add_class(name) unless name == ""
end
# Return a list of all of the defined classes.
def classlist
@catalog.classes
end
# Compiler our catalog. This mostly revolves around finding and evaluating classes.
# This is the main entry into our catalog.
def compile
# Set the client's parameters into the top scope.
set_node_parameters
create_settings_scope
evaluate_main
evaluate_ast_node
evaluate_node_classes
evaluate_generators
finish
fail_on_unevaluated
@catalog
end
# LAK:FIXME There are no tests for this.
def delete_collection(coll)
@collections.delete(coll) if @collections.include?(coll)
end
# Return the node's environment.
def environment
unless defined?(@environment)
@environment = (node.environment and node.environment != "") ? node.environment : nil
end
Puppet::Node::Environment.current = @environment
@environment
end
# Evaluate all of the classes specified by the node.
def evaluate_node_classes
evaluate_classes(@node.classes, topscope)
end
# Evaluate each specified class in turn. If there are any classes we can't
# find, just tag the catalog and move on. This method really just
# creates resource objects that point back to the classes, and then the
# resources are themselves evaluated later in the process.
def evaluate_classes(classes, scope, lazy_evaluate = true)
raise Puppet::DevError, "No source for scope passed to evaluate_classes" unless scope.source
found = []
+ param_classes = nil
+ # if we are a param class, save the classes hash
+ # and transform classes to be the keys
+ if classes.class == Hash
+ param_classes = classes
+ classes = classes.keys
+ end
classes.each do |name|
# If we can find the class, then make a resource that will evaluate it.
if klass = scope.find_hostclass(name)
- found << name and next if scope.class_scope(klass)
- resource = klass.ensure_in_catalog(scope)
+ if param_classes
+ resource = klass.ensure_in_catalog(scope, param_classes[name] || {})
+ else
+ found << name and next if scope.class_scope(klass)
+ resource = klass.ensure_in_catalog(scope)
+ end
# If they've disabled lazy evaluation (which the :include function does),
# then evaluate our resource immediately.
resource.evaluate unless lazy_evaluate
found << name
else
Puppet.info "Could not find class #{name} for #{node.name}"
@catalog.tag(name)
end
end
found
end
def evaluate_relationships
@relationships.each { |rel| rel.evaluate(catalog) }
end
# Return a resource by either its ref or its type and title.
def findresource(*args)
@catalog.resource(*args)
end
def initialize(node, options = {})
@node = node
options.each do |param, value|
begin
send(param.to_s + "=", value)
rescue NoMethodError
raise ArgumentError, "Compiler objects do not accept #{param}"
end
end
initvars
end
# Create a new scope, with either a specified parent scope or
# using the top scope.
def newscope(parent, options = {})
parent ||= topscope
options[:compiler] = self
scope = Puppet::Parser::Scope.new(options)
scope.parent = parent
scope
end
# Return any overrides for the given resource.
def resource_overrides(resource)
@resource_overrides[resource.ref]
end
# The top scope is usually the top-level scope, but if we're using AST nodes,
# then it is instead the node's scope.
def topscope
node_scope || @topscope
end
private
# If ast nodes are enabled, then see if we can find and evaluate one.
def evaluate_ast_node
return unless ast_nodes?
# Now see if we can find the node.
astnode = nil
@node.names.each do |name|
break if astnode = known_resource_types.node(name.to_s.downcase)
end
unless (astnode ||= known_resource_types.node("default"))
raise Puppet::ParseError, "Could not find default node or by name with '#{node.names.join(", ")}'"
end
# Create a resource to model this node, and then add it to the list
# of resources.
resource = astnode.ensure_in_catalog(topscope)
resource.evaluate
# Now set the node scope appropriately, so that :topscope can
# behave differently.
@node_scope = topscope.class_scope(astnode)
end
# Evaluate our collections and return true if anything returned an object.
# The 'true' is used to continue a loop, so it's important.
def evaluate_collections
return false if @collections.empty?
found_something = false
exceptwrap do
# We have to iterate over a dup of the array because
# collections can delete themselves from the list, which
# changes its length and causes some collections to get missed.
@collections.dup.each do |collection|
found_something = true if collection.evaluate
end
end
found_something
end
# Make sure all of our resources have been evaluated into native resources.
# We return true if any resources have, so that we know to continue the
# evaluate_generators loop.
def evaluate_definitions
exceptwrap do
!unevaluated_resources.each { |resource| resource.evaluate }.empty?
end
end
# Iterate over collections and resources until we're sure that the whole
# compile is evaluated. This is necessary because both collections
# and defined resources can generate new resources, which themselves could
# be defined resources.
def evaluate_generators
count = 0
loop do
done = true
# Call collections first, then definitions.
done = false if evaluate_collections
done = false if evaluate_definitions
break if done
count += 1
if count > 1000
raise Puppet::ParseError, "Somehow looped more than 1000 times while evaluating host catalog"
end
end
end
# Find and evaluate our main object, if possible.
def evaluate_main
@main = known_resource_types.find_hostclass([""], "") || known_resource_types.add(Puppet::Resource::Type.new(:hostclass, ""))
@topscope.source = @main
@main_resource = Puppet::Parser::Resource.new("class", :main, :scope => @topscope, :source => @main)
@topscope.resource = @main_resource
add_resource(@topscope, @main_resource)
@main_resource.evaluate
end
# Make sure the entire catalog is evaluated.
def fail_on_unevaluated
fail_on_unevaluated_overrides
fail_on_unevaluated_resource_collections
end
# If there are any resource overrides remaining, then we could
# not find the resource they were supposed to override, so we
# want to throw an exception.
def fail_on_unevaluated_overrides
remaining = []
@resource_overrides.each do |name, overrides|
remaining += overrides
end
unless remaining.empty?
fail Puppet::ParseError,
"Could not find resource(s) %s for overriding" % remaining.collect { |o|
o.ref
}.join(", ")
end
end
# Make sure we don't have any remaining collections that specifically
# look for resources, because we want to consider those to be
# parse errors.
def fail_on_unevaluated_resource_collections
remaining = []
@collections.each do |coll|
# We're only interested in the 'resource' collections,
# which result from direct calls of 'realize'. Anything
# else is allowed not to return resources.
# Collect all of them, so we have a useful error.
if r = coll.resources
if r.is_a?(Array)
remaining += r
else
remaining << r
end
end
end
raise Puppet::ParseError, "Failed to realize virtual resources #{remaining.join(', ')}" unless remaining.empty?
end
# Make sure all of our resources and such have done any last work
# necessary.
def finish
evaluate_relationships
resources.each do |resource|
# Add in any resource overrides.
if overrides = resource_overrides(resource)
overrides.each do |over|
resource.merge(over)
end
# Remove the overrides, so that the configuration knows there
# are none left.
overrides.clear
end
resource.finish if resource.respond_to?(:finish)
end
add_resource_metaparams
end
def add_resource_metaparams
unless main = catalog.resource(:class, :main)
raise "Couldn't find main"
end
names = []
Puppet::Type.eachmetaparam do |name|
next if Puppet::Parser::Resource.relationship_parameter?(name)
names << name
end
data = {}
catalog.walk(main, :out) do |source, target|
if source_data = data[source] || metaparams_as_data(source, names)
# only store anything in the data hash if we've actually got
# data
data[source] ||= source_data
source_data.each do |param, value|
target[param] = value if target[param].nil?
end
data[target] = source_data.merge(metaparams_as_data(target, names))
end
target.tag(*(source.tags))
end
end
def metaparams_as_data(resource, params)
data = nil
params.each do |param|
unless resource[param].nil?
# Because we could be creating a hash for every resource,
# and we actually probably don't often have any data here at all,
# we're optimizing a bit by only creating a hash if there's
# any data to put in it.
data ||= {}
data[param] = resource[param]
end
end
data
end
# Set up all of our internal variables.
def initvars
# The list of objects that will available for export.
@exported_resources = {}
# The list of overrides. This is used to cache overrides on objects
# that don't exist yet. We store an array of each override.
@resource_overrides = Hash.new do |overs, ref|
overs[ref] = []
end
# The list of collections that have been created. This is a global list,
# but they each refer back to the scope that created them.
@collections = []
# The list of relationships to evaluate.
@relationships = []
# For maintaining the relationship between scopes and their resources.
@catalog = Puppet::Resource::Catalog.new(@node.name)
@catalog.version = known_resource_types.version
# Create our initial scope and a resource that will evaluate main.
@topscope = Puppet::Parser::Scope.new(:compiler => self)
@main_stage_resource = Puppet::Parser::Resource.new("stage", :main, :scope => @topscope)
@catalog.add_resource(@main_stage_resource)
# local resource array to maintain resource ordering
@resources = []
# Make sure any external node classes are in our class list
- @catalog.add_class(*@node.classes)
+ if @node.classes.class == Hash
+ @catalog.add_class(*@node.classes.keys)
+ else
+ @catalog.add_class(*@node.classes)
+ end
end
# Set the node's parameters into the top-scope as variables.
def set_node_parameters
node.parameters.each do |param, value|
@topscope.setvar(param, value)
end
# These might be nil.
catalog.client_version = node.parameters["clientversion"]
catalog.server_version = node.parameters["serverversion"]
end
def create_settings_scope
unless settings_type = environment.known_resource_types.hostclass("settings")
settings_type = Puppet::Resource::Type.new :hostclass, "settings"
environment.known_resource_types.add(settings_type)
end
settings_resource = Puppet::Parser::Resource.new("class", "settings", :scope => @topscope)
settings_type.evaluate_code(settings_resource)
@catalog.add_resource(settings_resource)
scope = @topscope.class_scope(settings_type)
Puppet.settings.each do |name, setting|
next if name.to_s == "name"
scope.setvar name.to_s, environment[name]
end
end
# Return an array of all of the unevaluated resources. These will be definitions,
# which need to get evaluated into native resources.
def unevaluated_resources
# The order of these is significant for speed due to short-circuting
resources.reject { |resource| resource.evaluated? or resource.virtual? or resource.builtin_type? }
end
end
diff --git a/lib/puppet/parser/functions/defined.rb b/lib/puppet/parser/functions/defined.rb
index 90632af2f..2aeaa9ba0 100644
--- a/lib/puppet/parser/functions/defined.rb
+++ b/lib/puppet/parser/functions/defined.rb
@@ -1,27 +1,49 @@
# Test whether a given class or definition is defined
-Puppet::Parser::Functions::newfunction(:defined, :type => :rvalue, :doc => "Determine whether a given
- type is defined, either as a native type or a defined type, or whether a class is defined.
- This is useful for checking whether a class is defined and only including it if it is.
- This function can also test whether a resource has been defined, using resource references
- (e.g., `if defined(File['/tmp/myfile']) { ... }`). This function is unfortunately
- dependent on the parse order of the configuration when testing whether a resource is defined.") do |vals|
+Puppet::Parser::Functions::newfunction(:defined, :type => :rvalue, :doc => "Determine whether
+ a given class or resource type is defined. This function can also determine whether a
+ specific resource has been declared. Returns true or false. Accepts class names,
+ type names, and resource references.
+
+ The `defined` function checks both native and defined types, including types
+ provided as plugins via modules. Types and classes are both checked using their names:
+
+ defined(\"file\")
+ defined(\"customtype\")
+ defined(\"foo\")
+ defined(\"foo::bar\")
+
+ Resource declarations are checked using resource references, e.g.
+ `defined( File['/tmp/myfile'] )`. Checking whether a given resource
+ has been declared is, unfortunately, dependent on the parse order of
+ the configuration, and the following code will not work:
+
+ if defined(File['/tmp/foo']) {
+ notify(\"This configuration includes the /tmp/foo file.\")
+ }
+ file {\"/tmp/foo\":
+ ensure => present,
+ }
+
+ However, this order requirement refers to parse order only, and ordering of
+ resources in the configuration graph (e.g. with `before` or `require`) does not
+ affect the behavior of `defined`.") do |vals|
result = false
vals = [vals] unless vals.is_a?(Array)
vals.each do |val|
case val
when String
if Puppet::Type.type(val) or find_definition(val) or find_hostclass(val)
result = true
break
end
when Puppet::Resource
if findresource(val.to_s)
result = true
break
end
else
raise ArgumentError, "Invalid argument of type '#{val.class}' to 'defined'"
end
end
result
end
diff --git a/lib/puppet/property.rb b/lib/puppet/property.rb
index 84e1a0360..12f496a6e 100644
--- a/lib/puppet/property.rb
+++ b/lib/puppet/property.rb
@@ -1,324 +1,339 @@
# The virtual base class for properties, which are the self-contained building
# blocks for actually doing work on the system.
require 'puppet'
require 'puppet/parameter'
class Puppet::Property < Puppet::Parameter
require 'puppet/property/ensure'
# Because 'should' uses an array, we have a special method for handling
# it. We also want to keep copies of the original values, so that
# they can be retrieved and compared later when merging.
attr_reader :shouldorig
attr_writer :noop
class << self
attr_accessor :unmanaged
attr_reader :name
# Return array matching info, defaulting to just matching
# the first value.
def array_matching
@array_matching ||= :first
end
# Set whether properties should match all values or just the first one.
def array_matching=(value)
value = value.intern if value.is_a?(String)
raise ArgumentError, "Supported values for Property#array_matching are 'first' and 'all'" unless [:first, :all].include?(value)
@array_matching = value
end
end
# Look up a value's name, so we can find options and such.
def self.value_name(name)
if value = value_collection.match?(name)
value.name
end
end
# Retrieve an option set when a value was defined.
def self.value_option(name, option)
if value = value_collection.value(name)
value.send(option)
end
end
# Define a new valid value for a property. You must provide the value itself,
# usually as a symbol, or a regex to match the value.
#
# The first argument to the method is either the value itself or a regex.
# The second argument is an option hash; valid options are:
# * :method: The name of the method to define. Defaults to 'set_'.
# * :required_features: A list of features this value requires.
# * :event: The event that should be returned when this value is set.
# * :call: When to call any associated block. The default value
# is `instead`, which means to call the value instead of calling the
# provider. You can also specify `before` or `after`, which will
# call both the block and the provider, according to the order you specify
# (the `first` refers to when the block is called, not the provider).
def self.newvalue(name, options = {}, &block)
value = value_collection.newvalue(name, options, &block)
define_method(value.method, &value.block) if value.method and value.block
value
end
# Call the provider method.
def call_provider(value)
provider.send(self.class.name.to_s + "=", value)
rescue NoMethodError
self.fail "The #{provider.class.name} provider can not handle attribute #{self.class.name}"
end
# Call the dynamically-created method associated with our value, if
# there is one.
def call_valuemethod(name, value)
if method = self.class.value_option(name, :method) and self.respond_to?(method)
begin
event = self.send(method)
rescue Puppet::Error
raise
rescue => detail
puts detail.backtrace if Puppet[:trace]
error = Puppet::Error.new("Could not set '#{value} on #{self.class.name}: #{detail}", @resource.line, @resource.file)
error.set_backtrace detail.backtrace
raise error
end
elsif block = self.class.value_option(name, :block)
# FIXME It'd be better here to define a method, so that
# the blocks could return values.
self.instance_eval(&block)
else
devfail "Could not find method for value '#{name}'"
end
end
# How should a property change be printed as a string?
def change_to_s(current_value, newvalue)
begin
if current_value == :absent
return "defined '#{name}' as '#{should_to_s(newvalue)}'"
elsif newvalue == :absent or newvalue == [:absent]
return "undefined '#{name}' from '#{is_to_s(current_value)}'"
else
return "#{name} changed '#{is_to_s(current_value)}' to '#{should_to_s(newvalue)}'"
end
rescue Puppet::Error, Puppet::DevError
raise
rescue => detail
puts detail.backtrace if Puppet[:trace]
raise Puppet::DevError, "Could not convert change '#{name}' to string: #{detail}"
end
end
# Figure out which event to return.
def event_name
value = self.should
event_name = self.class.value_option(value, :event) and return event_name
name == :ensure or return (name.to_s + "_changed").to_sym
return (resource.type.to_s + case value
when :present; "_created"
when :absent; "_removed"
else
"_changed"
end).to_sym
end
# Return a modified form of the resource event.
def event
resource.event :name => event_name, :desired_value => should, :property => self, :source_description => path
end
attr_reader :shadow
# initialize our property
def initialize(hash = {})
super
if ! self.metaparam? and klass = Puppet::Type.metaparamclass(self.class.name)
setup_shadow(klass)
end
end
# Determine whether the property is in-sync or not. If @should is
# not defined or is set to a non-true value, then we do not have
# a valid value for it and thus consider the property to be in-sync
# since we cannot fix it. Otherwise, we expect our should value
# to be an array, and if @is matches any of those values, then
# we consider it to be in-sync.
- def insync?(is)
+ #
+ # Don't override this method.
+ def safe_insync?(is)
+ # If there is no @should value, consider the property to be in sync.
return true unless @should
+ # Otherwise delegate to the (possibly derived) insync? method.
+ insync?(is)
+ end
+
+ def self.method_added(sym)
+ raise "Puppet::Property#safe_insync? shouldn't be overridden; please override insync? instead" if sym == :safe_insync?
+ end
+
+ # This method should be overridden by derived classes if necessary
+ # to provide extra logic to determine whether the property is in
+ # sync.
+ def insync?(is)
self.devfail "#{self.class.name}'s should is not array" unless @should.is_a?(Array)
# an empty array is analogous to no should values
return true if @should.empty?
# Look for a matching value
return (is == @should or is == @should.collect { |v| v.to_s }) if match_all?
@should.each { |val| return true if is == val or is == val.to_s }
# otherwise, return false
false
end
# because the @should and @is vars might be in weird formats,
# we need to set up a mechanism for pretty printing of the values
# default to just the values, but this way individual properties can
# override these methods
def is_to_s(currentvalue)
currentvalue
end
# Send a log message.
def log(msg)
Puppet::Util::Log.create(
:level => resource[:loglevel],
:message => msg,
:source => self
)
end
# Should we match all values, or just the first?
def match_all?
self.class.array_matching == :all
end
# Execute our shadow's munge code, too, if we have one.
def munge(value)
self.shadow.munge(value) if self.shadow
super
end
# each property class must define the name method, and property instances
# do not change that name
# this implicitly means that a given object can only have one property
# instance of a given property class
def name
self.class.name
end
# for testing whether we should actually do anything
def noop
# This is only here to make testing easier.
if @resource.respond_to?(:noop?)
@resource.noop?
else
if defined?(@noop)
@noop
else
Puppet[:noop]
end
end
end
# By default, call the method associated with the property name on our
# provider. In other words, if the property name is 'gid', we'll call
# 'provider.gid' to retrieve the current value.
def retrieve
provider.send(self.class.name)
end
# Set our value, using the provider, an associated block, or both.
def set(value)
# Set a name for looking up associated options like the event.
name = self.class.value_name(value)
call = self.class.value_option(name, :call) || :none
if call == :instead
call_valuemethod(name, value)
elsif call == :none
# They haven't provided a block, and our parent does not have
# a provider, so we have no idea how to handle this.
self.fail "#{self.class.name} cannot handle values of type #{value.inspect}" unless @resource.provider
call_provider(value)
else
# LAK:NOTE 20081031 This is a change in behaviour -- you could
# previously specify :call => [;before|:after], which would call
# the setter *in addition to* the block. I'm convinced this
# was never used, and it makes things unecessarily complicated.
# If you want to specify a block and still call the setter, then
# do so in the block.
devfail "Cannot use obsolete :call value '#{call}' for property '#{self.class.name}'"
end
end
# If there's a shadowing metaparam, instantiate it now.
# This allows us to create a property or parameter with the
# same name as a metaparameter, and the metaparam will only be
# stored as a shadow.
def setup_shadow(klass)
@shadow = klass.new(:resource => self.resource)
end
# Only return the first value
def should
return nil unless defined?(@should)
self.devfail "should for #{self.class.name} on #{resource.name} is not an array" unless @should.is_a?(Array)
if match_all?
return @should.collect { |val| self.unmunge(val) }
else
return self.unmunge(@should[0])
end
end
# Set the should value.
def should=(values)
values = [values] unless values.is_a?(Array)
@shouldorig = values
values.each { |val| validate(val) }
@should = values.collect { |val| self.munge(val) }
end
def should_to_s(newvalue)
[newvalue].flatten.join(" ")
end
def sync
devfail "Got a nil value for should" unless should
set(should)
end
# Verify that the passed value is valid.
# If the developer uses a 'validate' hook, this method will get overridden.
def unsafe_validate(value)
super
validate_features_per_value(value)
end
# Make sure that we've got all of the required features for a given value.
def validate_features_per_value(value)
if features = self.class.value_option(self.class.value_name(value), :required_features)
features = Array(features)
needed_features = features.collect { |f| f.to_s }.join(", ")
raise ArgumentError, "Provider must have features '#{needed_features}' to set '#{self.class.name}' to '#{value}'" unless provider.satisfies?(features)
end
end
# Just return any should value we might have.
def value
self.should
end
# Match the Parameter interface, but we really just use 'should' internally.
# Note that the should= method does all of the validation and such.
def value=(value)
self.should = value
end
end
diff --git a/lib/puppet/property/keyvalue.rb b/lib/puppet/property/keyvalue.rb
index 0181708f9..57d0ea2d9 100644
--- a/lib/puppet/property/keyvalue.rb
+++ b/lib/puppet/property/keyvalue.rb
@@ -1,88 +1,86 @@
#This subclass of property manages string key value pairs.
#In order to use this property:
# - the @should value must be an array of keyvalue pairs separated by the 'separator'
# - the retrieve method should return a hash with the keys as symbols
# IMPORTANT NOTE: In order for this property to work there must also be a 'membership' parameter
# The class that inherits from property should override that method with the symbol for the membership
require 'puppet/property'
module Puppet
class Property
class KeyValue < Property
def hash_to_key_value_s(hash)
hash.select { |k,v| true }.map { |pair| pair.join(separator) }.join(delimiter)
end
def should_to_s(should_value)
hash_to_key_value_s(should_value)
end
def is_to_s(current_value)
hash_to_key_value_s(current_value)
end
def membership
:key_value_membership
end
def inclusive?
@resource[membership] == :inclusive
end
def hashify(key_value_array)
#turns string array into a hash
key_value_array.inject({}) do |hash, key_value|
tmp = key_value.split(separator)
hash[tmp[0].intern] = tmp[1]
hash
end
end
def process_current_hash(current)
return {} if current == :absent
#inclusive means we are managing everything so if it isn't in should, its gone
current.each_key { |key| current[key] = nil } if inclusive?
current
end
def should
return nil unless @should
members = hashify(@should)
current = process_current_hash(retrieve)
#shared keys will get overwritten by members
current.merge(members)
end
def separator
"="
end
def delimiter
";"
end
def retrieve
#ok, some 'convention' if the keyvalue property is named properties, provider should implement a properties method
if key_hash = provider.send(name) and key_hash != :absent
return key_hash
else
return :absent
end
end
def insync?(is)
- return true unless @should
-
return true unless is
(is == self.should)
end
end
end
end
diff --git a/lib/puppet/property/list.rb b/lib/puppet/property/list.rb
index dcee85db7..b86dc87f2 100644
--- a/lib/puppet/property/list.rb
+++ b/lib/puppet/property/list.rb
@@ -1,77 +1,75 @@
require 'puppet/property'
module Puppet
class Property
class List < Property
def should_to_s(should_value)
#just return the should value
should_value
end
def is_to_s(currentvalue)
if currentvalue == :absent
return "absent"
else
return currentvalue.join(delimiter)
end
end
def membership
:membership
end
def add_should_with_current(should, current)
should += current if current.is_a?(Array)
should.uniq
end
def inclusive?
@resource[membership] == :inclusive
end
#dearrayify was motivated because to simplify the implementation of the OrderedList property
def dearrayify(array)
array.sort.join(delimiter)
end
def should
return nil unless @should
members = @should
#inclusive means we are managing everything so if it isn't in should, its gone
members = add_should_with_current(members, retrieve) if ! inclusive?
dearrayify(members)
end
def delimiter
","
end
def retrieve
#ok, some 'convention' if the list property is named groups, provider should implement a groups method
if tmp = provider.send(name) and tmp != :absent
return tmp.split(delimiter)
else
return :absent
end
end
def prepare_is_for_comparison(is)
if is == :absent
is = []
end
dearrayify(is)
end
def insync?(is)
- return true unless @should
-
return true unless is
(prepare_is_for_comparison(is) == self.should)
end
end
end
end
diff --git a/lib/puppet/provider/file/posix.rb b/lib/puppet/provider/file/posix.rb
index 6cbf98e9a..f7b8c9797 100644
--- a/lib/puppet/provider/file/posix.rb
+++ b/lib/puppet/provider/file/posix.rb
@@ -1,99 +1,97 @@
Puppet::Type.type(:file).provide :posix do
desc "Uses POSIX functionality to manage file's users and rights."
confine :feature => :posix
include Puppet::Util::POSIX
include Puppet::Util::Warnings
require 'etc'
def id2name(id)
return id.to_s if id.is_a?(Symbol)
return nil if id > Puppet[:maximum_uid].to_i
begin
user = Etc.getpwuid(id)
rescue TypeError
return nil
rescue ArgumentError
return nil
end
if user.uid == ""
return nil
else
return user.name
end
end
- def insync?(current, should)
- return true unless should
-
+ def is_owner_insync?(current, should)
should.each do |value|
if value =~ /^\d+$/
uid = Integer(value)
elsif value.is_a?(String)
fail "Could not find user #{value}" unless uid = uid(value)
else
uid = value
end
return true if uid == current
end
unless Puppet.features.root?
warnonce "Cannot manage ownership unless running as root"
return true
end
false
end
# Determine if the user is valid, and if so, return the UID
def validuser?(value)
Integer(value) rescue uid(value) || false
end
def retrieve(resource)
unless stat = resource.stat(false)
return :absent
end
currentvalue = stat.uid
# On OS X, files that are owned by -2 get returned as really
# large UIDs instead of negative ones. This isn't a Ruby bug,
# it's an OS X bug, since it shows up in perl, too.
if currentvalue > Puppet[:maximum_uid].to_i
self.warning "Apparently using negative UID (#{currentvalue}) on a platform that does not consistently handle them"
currentvalue = :silly
end
currentvalue
end
def sync(path, links, should)
# Set our method appropriately, depending on links.
if links == :manage
method = :lchown
else
method = :chown
end
uid = nil
should.each do |user|
break if uid = validuser?(user)
end
raise Puppet::Error, "Could not find user(s) #{should.join(",")}" unless uid
begin
File.send(method, uid, nil, path)
rescue => detail
raise Puppet::Error, "Failed to set owner to '#{uid}': #{detail}"
end
:file_changed
end
end
diff --git a/lib/puppet/provider/file/win32.rb b/lib/puppet/provider/file/win32.rb
index 8ead69a89..21e7ca974 100644
--- a/lib/puppet/provider/file/win32.rb
+++ b/lib/puppet/provider/file/win32.rb
@@ -1,74 +1,72 @@
Puppet::Type.type(:file).provide :microsoft_windows do
desc "Uses Microsoft Windows functionality to manage file's users and rights."
confine :feature => :microsoft_windows
include Puppet::Util::Warnings
require 'sys/admin' if Puppet.features.microsoft_windows?
def id2name(id)
return id.to_s if id.is_a?(Symbol)
return nil if id > Puppet[:maximum_uid].to_i
# should translate ID numbers to usernames
id
end
- def insync?(current, should)
- return true unless should
-
+ def is_owner_insync?(current, should)
should.each do |value|
if value =~ /^\d+$/
uid = Integer(value)
elsif value.is_a?(String)
fail "Could not find user #{value}" unless uid = uid(value)
else
uid = value
end
return true if uid == current
end
unless Puppet.features.root?
warnonce "Cannot manage ownership unless running as root"
return true
end
false
end
# Determine if the user is valid, and if so, return the UID
def validuser?(value)
info "Is '#{value}' a valid user?"
return 0
begin
number = Integer(value)
return number
rescue ArgumentError
number = nil
end
(number = uid(value)) && number
end
def retrieve(resource)
unless stat = resource.stat(false)
return :absent
end
currentvalue = stat.uid
# On OS X, files that are owned by -2 get returned as really
# large UIDs instead of negative ones. This isn't a Ruby bug,
# it's an OS X bug, since it shows up in perl, too.
if currentvalue > Puppet[:maximum_uid].to_i
self.warning "Apparently using negative UID (#{currentvalue}) on a platform that does not consistently handle them"
currentvalue = :silly
end
currentvalue
end
def sync(path, links, should)
info("should set '%s'%%owner to '%s'" % [path, should])
end
end
diff --git a/lib/puppet/provider/mount.rb b/lib/puppet/provider/mount.rb
index 8c7b24bd4..c979f742f 100644
--- a/lib/puppet/provider/mount.rb
+++ b/lib/puppet/provider/mount.rb
@@ -1,51 +1,53 @@
# Created by Luke Kanies on 2006-11-12.
# Copyright (c) 2006. All rights reserved.
require 'puppet'
# A module just to store the mount/unmount methods. Individual providers
# still need to add the mount commands manually.
module Puppet::Provider::Mount
# This only works when the mount point is synced to the fstab.
def mount
# Manually pass the mount options in, since some OSes *cough*OS X*cough* don't
# read from /etc/fstab but still want to use this type.
args = []
args << "-o" << self.options if self.options and self.options != :absent
args << resource[:name]
flush if respond_to?(:flush)
mountcmd(*args)
end
def remount
info "Remounting"
if resource[:remounts] == :true
mountcmd "-o", "remount", resource[:name]
else
unmount
mount
end
end
# This only works when the mount point is synced to the fstab.
def unmount
umount resource[:name]
end
# Is the mount currently mounted?
def mounted?
platform = Facter.value("operatingsystem")
name = resource[:name]
mounts = mountcmd.split("\n").find do |line|
case platform
when "Darwin"
line =~ / on #{name} / or line =~ %r{ on /private/var/automount#{name}}
when "Solaris", "HP-UX"
line =~ /^#{name} on /
+ when "AIX"
+ line.split(/\s+/)[1] == name
else
line =~ / on #{name} /
end
end
end
end
diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb
index 965a2aa60..b01880360 100644
--- a/lib/puppet/provider/nameservice/directoryservice.rb
+++ b/lib/puppet/provider/nameservice/directoryservice.rb
@@ -1,516 +1,516 @@
# Created by Jeff McCune on 2007-07-22
# Copyright (c) 2007. All rights reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation (version 2 of the License)
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston MA 02110-1301 USA
require 'puppet'
require 'puppet/provider/nameservice'
require 'facter/util/plist'
require 'cgi'
class Puppet::Provider::NameService
class DirectoryService < Puppet::Provider::NameService
# JJM: Dive into the singleton_class
class << self
# JJM: This allows us to pass information when calling
# Puppet::Type.type
# e.g. Puppet::Type.type(:user).provide :directoryservice, :ds_path => "Users"
# This is referenced in the get_ds_path class method
attr_writer :ds_path
attr_writer :macosx_version_major
end
initvars
commands :dscl => "/usr/bin/dscl"
commands :dseditgroup => "/usr/sbin/dseditgroup"
commands :sw_vers => "/usr/bin/sw_vers"
confine :operatingsystem => :darwin
defaultfor :operatingsystem => :darwin
# JJM 2007-07-25: This map is used to map NameService attributes to their
# corresponding DirectoryService attribute names.
# See: http://images.apple.com/server/docs.Open_Directory_v10.4.pdf
# JJM: Note, this is de-coupled from the Puppet::Type, and must
# be actively maintained. There may also be collisions with different
# types (Users, Groups, Mounts, Hosts, etc...)
@@ds_to_ns_attribute_map = {
'RecordName' => :name,
'PrimaryGroupID' => :gid,
'NFSHomeDirectory' => :home,
'UserShell' => :shell,
'UniqueID' => :uid,
'RealName' => :comment,
'Password' => :password,
'GeneratedUID' => :guid,
'IPAddress' => :ip_address,
'ENetAddress' => :en_address,
'GroupMembership' => :members,
}
# JJM The same table as above, inverted.
@@ns_to_ds_attribute_map = {
:name => 'RecordName',
:gid => 'PrimaryGroupID',
:home => 'NFSHomeDirectory',
:shell => 'UserShell',
:uid => 'UniqueID',
:comment => 'RealName',
:password => 'Password',
:guid => 'GeneratedUID',
:en_address => 'ENetAddress',
:ip_address => 'IPAddress',
:members => 'GroupMembership',
}
@@password_hash_dir = "/var/db/shadow/hash"
def self.instances
# JJM Class method that provides an array of instance objects of this
# type.
# JJM: Properties are dependent on the Puppet::Type we're managine.
type_property_array = [:name] + @resource_type.validproperties
# Create a new instance of this Puppet::Type for each object present
# on the system.
list_all_present.collect do |name_string|
self.new(single_report(name_string, *type_property_array))
end
end
def self.get_ds_path
# JJM: 2007-07-24 This method dynamically returns the DS path we're concerned with.
# For example, if we're working with an user type, this will be /Users
# with a group type, this will be /Groups.
# @ds_path is an attribute of the class itself.
return @ds_path if defined?(@ds_path)
# JJM: "Users" or "Groups" etc ... (Based on the Puppet::Type)
# Remember this is a class method, so self.class is Class
# Also, @resource_type seems to be the reference to the
# Puppet::Type this class object is providing for.
@resource_type.name.to_s.capitalize + "s"
end
def self.get_macosx_version_major
return @macosx_version_major if defined?(@macosx_version_major)
begin
# Make sure we've loaded all of the facts
Facter.loadfacts
if Facter.value(:macosx_productversion_major)
product_version_major = Facter.value(:macosx_productversion_major)
else
# TODO: remove this code chunk once we require Facter 1.5.5 or higher.
Puppet.warning("DEPRECATION WARNING: Future versions of the directoryservice provider will require Facter 1.5.5 or newer.")
product_version = Facter.value(:macosx_productversion)
fail("Could not determine OS X version from Facter") if product_version.nil?
product_version_major = product_version.scan(/(\d+)\.(\d+)./).join(".")
end
fail("#{product_version_major} is not supported by the directoryservice provider") if %w{10.0 10.1 10.2 10.3}.include?(product_version_major)
@macosx_version_major = product_version_major
return @macosx_version_major
rescue Puppet::ExecutionFailure => detail
fail("Could not determine OS X version: #{detail}")
end
end
def self.list_all_present
# JJM: List all objects of this Puppet::Type already present on the system.
begin
dscl_output = execute(get_exec_preamble("-list"))
rescue Puppet::ExecutionFailure => detail
fail("Could not get #{@resource_type.name} list from DirectoryService")
end
dscl_output.split("\n")
end
def self.parse_dscl_url_data(dscl_output)
# we need to construct a Hash from the dscl -url output to match
# that returned by the dscl -plist output for 10.5+ clients.
#
# Nasty assumptions:
# a) no values *end* in a colon ':', only keys
# b) if a line ends in a colon and the next line does start with
# a space, then the second line is a value of the first.
# c) (implied by (b)) keys don't start with spaces.
dscl_plist = {}
dscl_output.split("\n").inject([]) do |array, line|
if line =~ /^\s+/ # it's a value
array[-1] << line # add the value to the previous key
else
array << line
end
array
end.compact
dscl_output.each do |line|
# This should be a 'normal' entry. key and value on one line.
# We split on ': ' to deal with keys/values with a colon in them.
split_array = line.split(/:\s+/)
key = split_array.first
value = CGI::unescape(split_array.last.strip.chomp)
# We need to treat GroupMembership separately as it is currently
# the only attribute we care about multiple values for, and
# the values can never contain spaces (shortnames)
# We also make every value an array to be consistent with the
# output of dscl -plist under 10.5
if key == "GroupMembership"
dscl_plist[key] = value.split(/\s/)
else
dscl_plist[key] = [value]
end
end
dscl_plist
end
def self.parse_dscl_plist_data(dscl_output)
Plist.parse_xml(dscl_output)
end
def self.generate_attribute_hash(input_hash, *type_properties)
attribute_hash = {}
input_hash.keys.each do |key|
ds_attribute = key.sub("dsAttrTypeStandard:", "")
next unless (@@ds_to_ns_attribute_map.keys.include?(ds_attribute) and type_properties.include? @@ds_to_ns_attribute_map[ds_attribute])
ds_value = input_hash[key]
case @@ds_to_ns_attribute_map[ds_attribute]
when :members
ds_value = ds_value # only members uses arrays so far
when :gid, :uid
# OS X stores objects like uid/gid as strings.
# Try casting to an integer for these cases to be
# consistent with the other providers and the group type
# validation
begin
ds_value = Integer(ds_value[0])
rescue ArgumentError
ds_value = ds_value[0]
end
else ds_value = ds_value[0]
end
attribute_hash[@@ds_to_ns_attribute_map[ds_attribute]] = ds_value
end
# NBK: need to read the existing password here as it's not actually
# stored in the user record. It is stored at a path that involves the
# UUID of the user record for non-Mobile local acccounts.
# Mobile Accounts are out of scope for this provider for now
attribute_hash[:password] = self.get_password(attribute_hash[:guid]) if @resource_type.validproperties.include?(:password) and Puppet.features.root?
attribute_hash
end
def self.single_report(resource_name, *type_properties)
# JJM 2007-07-24:
# Given a the name of an object and a list of properties of that
# object, return all property values in a hash.
#
# This class method returns nil if the object doesn't exist
# Otherwise, it returns a hash of the object properties.
all_present_str_array = list_all_present
# NBK: shortcut the process if the resource is missing
return nil unless all_present_str_array.include? resource_name
dscl_vector = get_exec_preamble("-read", resource_name)
begin
dscl_output = execute(dscl_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not get report. command execution failed.")
end
# Two code paths is ugly, but until we can drop 10.4 support we don't
# have a lot of choice. Ultimately this should all be done using Ruby
# to access the DirectoryService APIs directly, but that's simply not
# feasible for a while yet.
case self.get_macosx_version_major
when "10.4"
dscl_plist = self.parse_dscl_url_data(dscl_output)
when "10.5", "10.6"
dscl_plist = self.parse_dscl_plist_data(dscl_output)
end
self.generate_attribute_hash(dscl_plist, *type_properties)
end
def self.get_exec_preamble(ds_action, resource_name = nil)
# JJM 2007-07-24
# DSCL commands are often repetitive and contain the same positional
# arguments over and over. See http://developer.apple.com/documentation/Porting/Conceptual/PortingUnix/additionalfeatures/chapter_10_section_9.html
# for an example of what I mean.
# This method spits out proper DSCL commands for us.
# We EXPECT name to be @resource[:name] when called from an instance object.
# 10.4 doesn't support the -plist option for dscl, and 10.5 has a
# different format for the -url output with objects with spaces in
# their values. *sigh*. Use -url for 10.4 in the hope this can be
# deprecated one day, and use -plist for 10.5 and higher.
case self.get_macosx_version_major
when "10.4"
command_vector = [ command(:dscl), "-url", "." ]
when "10.5", "10.6"
command_vector = [ command(:dscl), "-plist", "." ]
end
# JJM: The actual action to perform. See "man dscl"
# Common actiosn: -create, -delete, -merge, -append, -passwd
command_vector << ds_action
# JJM: get_ds_path will spit back "Users" or "Groups",
# etc... Depending on the Puppet::Type of our self.
if resource_name
command_vector << "/#{get_ds_path}/#{resource_name}"
else
command_vector << "/#{get_ds_path}"
end
# JJM: This returns most of the preamble of the command.
# e.g. 'dscl / -create /Users/mccune'
command_vector
end
def self.set_password(resource_name, guid, password_hash)
password_hash_file = "#{@@password_hash_dir}/#{guid}"
begin
File.open(password_hash_file, 'w') { |f| f.write(password_hash)}
rescue Errno::EACCES => detail
fail("Could not write to password hash file: #{detail}")
end
# NBK: For shadow hashes, the user AuthenticationAuthority must contain a value of
# ";ShadowHash;". The LKDC in 10.5 makes this more interesting though as it
# will dynamically generate ;Kerberosv5;;username@LKDC:SHA1 attributes if
# missing. Thus we make sure we only set ;ShadowHash; if it is missing, and
# we can do this with the merge command. This allows people to continue to
# use other custom AuthenticationAuthority attributes without stomping on them.
#
# There is a potential problem here in that we're only doing this when setting
# the password, and the attribute could get modified at other times while the
# hash doesn't change and so this doesn't get called at all... but
# without switching all the other attributes to merge instead of create I can't
# see a simple enough solution for this that doesn't modify the user record
# every single time. This should be a rather rare edge case. (famous last words)
dscl_vector = self.get_exec_preamble("-merge", resource_name)
dscl_vector << "AuthenticationAuthority" << ";ShadowHash;"
begin
dscl_output = execute(dscl_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not set AuthenticationAuthority.")
end
end
def self.get_password(guid)
password_hash = nil
password_hash_file = "#{@@password_hash_dir}/#{guid}"
if File.exists?(password_hash_file) and File.file?(password_hash_file)
fail("Could not read password hash file at #{password_hash_file}") if not File.readable?(password_hash_file)
f = File.new(password_hash_file)
password_hash = f.read
f.close
end
password_hash
end
def ensure=(ensure_value)
super
# We need to loop over all valid properties for the type we're
# managing and call the method which sets that property value
# dscl can't create everything at once unfortunately.
if ensure_value == :present
@resource.class.validproperties.each do |name|
next if name == :ensure
# LAK: We use property.sync here rather than directly calling
# the settor method because the properties might do some kind
# of conversion. In particular, the user gid property might
# have a string and need to convert it to a number
if @resource.should(name)
@resource.property(name).sync
elsif value = autogen(name)
self.send(name.to_s + "=", value)
else
next
end
end
end
end
def password=(passphrase)
exec_arg_vector = self.class.get_exec_preamble("-read", @resource.name)
exec_arg_vector << @@ns_to_ds_attribute_map[:guid]
begin
guid_output = execute(exec_arg_vector)
guid_plist = Plist.parse_xml(guid_output)
# Although GeneratedUID like all DirectoryService values can be multi-valued
# according to the schema, in practice user accounts cannot have multiple UUIDs
# otherwise Bad Things Happen, so we just deal with the first value.
guid = guid_plist["dsAttrTypeStandard:#{@@ns_to_ds_attribute_map[:guid]}"][0]
self.class.set_password(@resource.name, guid, passphrase)
rescue Puppet::ExecutionFailure => detail
fail("Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}")
end
end
# NBK: we override @parent.set as we need to execute a series of commands
# to deal with array values, rather than the single command nameservice.rb
# expects to be returned by modifycmd. Thus we don't bother defining modifycmd.
def set(param, value)
self.class.validate(param, value)
current_members = @property_value_cache_hash[:members]
if param == :members
# If we are meant to be authoritative for the group membership
# then remove all existing members who haven't been specified
# in the manifest.
remove_unwanted_members(current_members, value) if @resource[:auth_membership] and not current_members.nil?
# if they're not a member, make them one.
add_members(current_members, value)
else
exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
# JJM: The following line just maps the NS name to the DS name
# e.g. { :uid => 'UniqueID' }
exec_arg_vector << @@ns_to_ds_attribute_map[symbolize(param)]
# JJM: The following line sends the actual value to set the property to
exec_arg_vector << value.to_s
begin
execute(exec_arg_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}")
end
end
end
# NBK: we override @parent.create as we need to execute a series of commands
# to create objects with dscl, rather than the single command nameservice.rb
# expects to be returned by addcmd. Thus we don't bother defining addcmd.
def create
if exists?
info "already exists"
return nil
end
# NBK: First we create the object with a known guid so we can set the contents
# of the password hash if required
# Shelling out sucks, but for a single use case it doesn't seem worth
# requiring people install a UUID library that doesn't come with the system.
# This should be revisited if Puppet starts managing UUIDs for other platform
# user records.
guid = %x{/usr/bin/uuidgen}.chomp
exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
exec_arg_vector << @@ns_to_ds_attribute_map[:guid] << guid
begin
execute(exec_arg_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not set GeneratedUID for #{@resource.class.name} #{@resource.name}: #{detail}")
end
if value = @resource.should(:password) and value != ""
self.class.set_password(@resource[:name], guid, value)
end
# Now we create all the standard properties
Puppet::Type.type(@resource.class.name).validproperties.each do |property|
next if property == :ensure
if value = @resource.should(property) and value != ""
if property == :members
add_members(nil, value)
else
exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
exec_arg_vector << @@ns_to_ds_attribute_map[symbolize(property)]
next if property == :password # skip setting the password here
exec_arg_vector << value.to_s
begin
execute(exec_arg_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not create #{@resource.class.name} #{@resource.name}: #{detail}")
end
end
end
end
end
def remove_unwanted_members(current_members, new_members)
current_members.each do |member|
- if not new_members.include?(member)
+ if not new_members.flatten.include?(member)
cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-d", member, @resource[:name]]
begin
execute(cmd)
rescue Puppet::ExecutionFailure => detail
fail("Could not remove #{member} from group: #{@resource.name}, #{detail}")
end
end
end
end
def add_members(current_members, new_members)
- new_members.each do |new_member|
+ new_members.flatten.each do |new_member|
if current_members.nil? or not current_members.include?(new_member)
cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-a", new_member, @resource[:name]]
begin
execute(cmd)
rescue Puppet::ExecutionFailure => detail
fail("Could not add #{new_member} to group: #{@resource.name}, #{detail}")
end
end
end
end
def deletecmd
# JJM: Like addcmd, only called when deleting the object itself
# Note, this isn't used to delete properties of the object,
# at least that's how I understand it...
self.class.get_exec_preamble("-delete", @resource[:name])
end
def getinfo(refresh = false)
# JJM 2007-07-24:
# Override the getinfo method, which is also defined in nameservice.rb
# This method returns and sets @infohash
# I'm not re-factoring the name "getinfo" because this method will be
# most likely called by nameservice.rb, which I didn't write.
if refresh or (! defined?(@property_value_cache_hash) or ! @property_value_cache_hash)
# JJM 2007-07-24: OK, there's a bit of magic that's about to
# happen... Let's see how strong my grip has become... =)
#
# self is a provider instance of some Puppet::Type, like
# Puppet::Type::User::ProviderDirectoryservice for the case of the
# user type and this provider.
#
# self.class looks like "user provider directoryservice", if that
# helps you ...
#
# self.class.resource_type is a reference to the Puppet::Type class,
# probably Puppet::Type::User or Puppet::Type::Group, etc...
#
# self.class.resource_type.validproperties is a class method,
# returning an Array of the valid properties of that specific
# Puppet::Type.
#
# So... something like [:comment, :home, :password, :shell, :uid,
# :groups, :ensure, :gid]
#
# Ultimately, we add :name to the list, delete :ensure from the
# list, then report on the remaining list. Pretty whacky, ehh?
type_properties = [:name] + self.class.resource_type.validproperties
type_properties.delete(:ensure) if type_properties.include? :ensure
type_properties << :guid # append GeneratedUID so we just get the report here
@property_value_cache_hash = self.class.single_report(@resource[:name], *type_properties)
[:uid, :gid].each do |param|
@property_value_cache_hash[param] = @property_value_cache_hash[param].to_i if @property_value_cache_hash and @property_value_cache_hash.include?(param)
end
end
@property_value_cache_hash
end
end
end
diff --git a/lib/puppet/provider/package/freebsd.rb b/lib/puppet/provider/package/freebsd.rb
index 2f012a4ed..e10a20b04 100755
--- a/lib/puppet/provider/package/freebsd.rb
+++ b/lib/puppet/provider/package/freebsd.rb
@@ -1,50 +1,50 @@
Puppet::Type.type(:package).provide :freebsd, :parent => :openbsd do
desc "The specific form of package management on FreeBSD. This is an
extremely quirky packaging system, in that it freely mixes between
ports and packages. Apparently all of the tools are written in Ruby,
so there are plans to rewrite this support to directly use those
libraries."
commands :pkginfo => "/usr/sbin/pkg_info",
:pkgadd => "/usr/sbin/pkg_add",
:pkgdelete => "/usr/sbin/pkg_delete"
confine :operatingsystem => :freebsd
def self.listcmd
command(:pkginfo)
end
def install
should = @resource.should(:ensure)
if @resource[:source] =~ /\/$/
if @resource[:source] =~ /^(ftp|https?):/
- withenv :PACKAGESITE => @resource[:source] do
+ Puppet::Util::Execution::withenv :PACKAGESITE => @resource[:source] do
pkgadd "-r", @resource[:name]
end
else
- withenv :PKG_PATH => @resource[:source] do
+ Puppet::Util::Execution::withenv :PKG_PATH => @resource[:source] do
pkgadd @resource[:name]
end
end
else
Puppet.warning "source is defined but does not have trailing slash, ignoring #{@resource[:source]}" if @resource[:source]
pkgadd "-r", @resource[:name]
end
end
def query
self.class.instances.each do |provider|
if provider.name == @resource.name
return provider.properties
end
end
nil
end
def uninstall
pkgdelete "#{@resource[:name]}-#{@resource.should(:ensure)}"
end
end
diff --git a/lib/puppet/provider/zone/solaris.rb b/lib/puppet/provider/zone/solaris.rb
index c11444993..f46337b14 100644
--- a/lib/puppet/provider/zone/solaris.rb
+++ b/lib/puppet/provider/zone/solaris.rb
@@ -1,257 +1,257 @@
Puppet::Type.type(:zone).provide(:solaris) do
desc "Provider for Solaris Zones."
commands :adm => "/usr/sbin/zoneadm", :cfg => "/usr/sbin/zonecfg"
defaultfor :operatingsystem => :solaris
mk_resource_methods
# Convert the output of a list into a hash
def self.line2hash(line)
fields = [:id, :name, :ensure, :path]
properties = {}
line.split(":").each_with_index { |value, index|
next unless fields[index]
properties[fields[index]] = value
}
# Configured but not installed zones do not have IDs
properties.delete(:id) if properties[:id] == "-"
properties[:ensure] = symbolize(properties[:ensure])
properties
end
def self.instances
# LAK:NOTE See http://snurl.com/21zf8 [groups_google_com]
x = adm(:list, "-cp").split("\n").collect do |line|
new(line2hash(line))
end
end
# Perform all of our configuration steps.
def configure
# If the thing is entirely absent, then we need to create the config.
# Is there someway to get this on one line?
str = "create -b #{@resource[:create_args]}\nset zonepath=#{@resource[:path]}\n"
# Then perform all of our configuration steps. It's annoying
# that we need this much internal info on the resource.
@resource.send(:properties).each do |property|
- str += property.configtext + "\n" if property.is_a? ZoneConfigProperty and ! property.insync?(properties[property.name])
+ str += property.configtext + "\n" if property.is_a? ZoneConfigProperty and ! property.safe_insync?(properties[property.name])
end
str += "commit\n"
setconfig(str)
end
def destroy
zonecfg :delete, "-F"
end
def exists?
properties[:ensure] != :absent
end
# Clear out the cached values.
def flush
@property_hash.clear
end
def install(dummy_argument=:work_arround_for_ruby_GC_bug)
if @resource[:clone] # TODO: add support for "-s snapshot"
zoneadm :clone, @resource[:clone]
elsif @resource[:install_args]
zoneadm :install, @resource[:install_args].split(" ")
else
zoneadm :install
end
end
# Look up the current status.
def properties
if @property_hash.empty?
@property_hash = status || {}
if @property_hash.empty?
@property_hash[:ensure] = :absent
else
@resource.class.validproperties.each do |name|
@property_hash[name] ||= :absent
end
end
end
@property_hash.dup
end
# We need a way to test whether a zone is in process. Our 'ensure'
# property models the static states, but we need to handle the temporary ones.
def processing?
if hash = status
case hash[:ensure]
when "incomplete", "ready", "shutting_down"
true
else
false
end
else
false
end
end
# Collect the configuration of the zone.
def getconfig
output = zonecfg :info
name = nil
current = nil
hash = {}
output.split("\n").each do |line|
case line
when /^(\S+):\s*$/
name = $1
current = nil # reset it
when /^(\S+):\s*(.+)$/
hash[$1.intern] = $2
when /^\s+(\S+):\s*(.+)$/
if name
hash[name] = [] unless hash.include? name
unless current
current = {}
hash[name] << current
end
current[$1.intern] = $2
else
err "Ignoring '#{line}'"
end
else
debug "Ignoring zone output '#{line}'"
end
end
hash
end
# Execute a configuration string. Can't be private because it's called
# by the properties.
def setconfig(str)
command = "#{command(:cfg)} -z #{@resource[:name]} -f -"
debug "Executing '#{command}' in zone #{@resource[:name]} with '#{str}'"
IO.popen(command, "w") do |pipe|
pipe.puts str
end
unless $CHILD_STATUS == 0
raise ArgumentError, "Failed to apply configuration"
end
end
def start
# Check the sysidcfg stuff
if cfg = @resource[:sysidcfg]
zoneetc = File.join(@resource[:path], "root", "etc")
sysidcfg = File.join(zoneetc, "sysidcfg")
# if the zone root isn't present "ready" the zone
# which makes zoneadmd mount the zone root
zoneadm :ready unless File.directory?(zoneetc)
unless File.exists?(sysidcfg)
begin
File.open(sysidcfg, "w", 0600) do |f|
f.puts cfg
end
rescue => detail
puts detail.stacktrace if Puppet[:debug]
raise Puppet::Error, "Could not create sysidcfg: #{detail}"
end
end
end
zoneadm :boot
end
# Return a hash of the current status of this zone.
def status
begin
output = adm "-z", @resource[:name], :list, "-p"
rescue Puppet::ExecutionFailure
return nil
end
main = self.class.line2hash(output.chomp)
# Now add in the configuration information
config_status.each do |name, value|
main[name] = value
end
main
end
def ready
zoneadm :ready
end
def stop
zoneadm :halt
end
def unconfigure
zonecfg :delete, "-F"
end
def uninstall
zoneadm :uninstall, "-F"
end
private
# Turn the results of getconfig into status information.
def config_status
config = getconfig
result = {}
result[:autoboot] = config[:autoboot] ? config[:autoboot].intern : :absent
result[:pool] = config[:pool]
result[:shares] = config[:shares]
if dir = config["inherit-pkg-dir"]
result[:inherit] = dir.collect { |dirs| dirs[:dir] }
end
result[:iptype] = config[:"ip-type"]
if net = config["net"]
result[:ip] = net.collect do |params|
if params[:defrouter]
"#{params[:physical]}:#{params[:address]}:#{params[:defrouter]}"
elsif params[:address]
"#{params[:physical]}:#{params[:address]}"
else
params[:physical]
end
end
end
result
end
def zoneadm(*cmd)
adm("-z", @resource[:name], *cmd)
rescue Puppet::ExecutionFailure => detail
self.fail "Could not #{cmd[0]} zone: #{detail}"
end
def zonecfg(*cmd)
# You apparently can't get the configuration of the global zone
return "" if self.name == "global"
begin
cfg("-z", self.name, *cmd)
rescue Puppet::ExecutionFailure => detail
self.fail "Could not #{cmd[0]} zone: #{detail}"
end
end
end
diff --git a/lib/puppet/reference/configuration.rb b/lib/puppet/reference/configuration.rb
index e6a8dc20f..c8ff145ba 100644
--- a/lib/puppet/reference/configuration.rb
+++ b/lib/puppet/reference/configuration.rb
@@ -1,144 +1,144 @@
config = Puppet::Util::Reference.newreference(:configuration, :depth => 1, :doc => "A reference for all configuration parameters") do
docs = {}
Puppet.settings.each do |name, object|
docs[name] = object
end
str = ""
docs.sort { |a, b|
a[0].to_s <=> b[0].to_s
}.each do |name, object|
# Make each name an anchor
header = name.to_s
str += h(header, 3)
# Print the doc string itself
begin
str += object.desc.gsub(/\n/, " ")
rescue => detail
puts detail.backtrace
puts detail
end
str += "\n\n"
# Now print the data about the item.
str += ""
val = object.default
if name.to_s == "vardir"
val = "/var/lib/puppet"
elsif name.to_s == "confdir"
val = "/etc/puppet"
end
# Leave out the section information; it was apparently confusing people.
#str += "- **Section**: #{object.section}\n"
unless val == ""
str += "- *Default*: #{val}\n"
end
str += "\n"
end
return str
end
config.header = "
## Specifying Configuration Parameters
### On The Command-Line
Every Puppet executable (with the exception of `puppetdoc`) accepts all of
the parameters below, but not all of the arguments make sense for every executable.
I have tried to be as thorough as possible in the descriptions of the
arguments, so it should be obvious whether an argument is appropriate or not.
These parameters can be supplied to the executables either as command-line
options or in the configuration file. For instance, the command-line
invocation below would set the configuration directory to `/private/puppet`:
$ puppet agent --confdir=/private/puppet
Note that boolean options are turned on and off with a slightly different
syntax on the command line:
$ puppet agent --storeconfigs
$ puppet agent --no-storeconfigs
The invocations above will enable and disable, respectively, the storage of
the client configuration.
### Configuration Files
As mentioned above, the configuration parameters can also be stored in a
configuration file, located in the configuration directory. As root, the
default configuration directory is `/etc/puppet`, and as a regular user, the
default configuration directory is `~user/.puppet`. As of 0.23.0, all
executables look for `puppet.conf` in their configuration directory
(although they previously looked for separate files). For example,
`puppet.conf` is located at `/etc/puppet/puppet.conf` as `root` and
`~user/.puppet/puppet.conf` as a regular user by default.
All executables will set any parameters set within the `[main]` section,
and each executable will also use one of the `[master]`, `[agent]`.
#### File Format
The file follows INI-style formatting. Here is an example of a very simple
`puppet.conf` file:
[main]
confdir = /private/puppet
storeconfigs = true
Note that boolean parameters must be explicitly specified as `true` or
`false` as seen above.
-If you need to change file parameters (e.g., reset the mode or owner), do
+If you need to change file or directory parameters (e.g., reset the mode or owner), do
so within curly braces on the same line:
[main]
- myfile = /tmp/whatever {owner = root, mode = 644}
+ vardir = /new/vardir {owner = root, mode = 644}
If you're starting out with a fresh configuration, you may wish to let
the executable generate a template configuration file for you by invoking
the executable in question with the `--genconfig` command. The executable
will print a template configuration to standard output, which can be
redirected to a file like so:
$ puppet agent --genconfig > /etc/puppet/puppet.conf
Note that this invocation will replace the contents of any pre-existing
`puppet.conf` file, so make a backup of your present config if it contains
valuable information.
Like the `--genconfig` argument, the executables also accept a `--genmanifest`
argument, which will generate a manifest that can be used to manage all of
Puppet's directories and files and prints it to standard output. This can
likewise be redirected to a file:
$ puppet agent --genmanifest > /etc/puppet/manifests/site.pp
Puppet can also create user and group accounts for itself (one `puppet` group
and one `puppet` user) if it is invoked as `root` with the `--mkusers` argument:
$ puppet agent --mkusers
## Signals
The `puppet agent` and `puppet master` executables catch some signals for special
handling. Both daemons catch (`SIGHUP`), which forces the server to restart
tself. Predictably, interrupt and terminate (`SIGINT` and `SIGTERM`) will shut
down the server, whether it be an instance of `puppet agent` or `puppet master`.
Sending the `SIGUSR1` signal to an instance of `puppet agent` will cause it to
immediately begin a new configuration transaction with the server. This
signal has no effect on `puppet master`.
## Configuration Parameter Reference
Below is a list of all documented parameters. Not all of them are valid with all
Puppet executables, but the executables will ignore any inappropriate values.
"
diff --git a/lib/puppet/resource/type.rb b/lib/puppet/resource/type.rb
index d40adc145..34fddf135 100644
--- a/lib/puppet/resource/type.rb
+++ b/lib/puppet/resource/type.rb
@@ -1,339 +1,347 @@
require 'puppet/parser/parser'
require 'puppet/util/warnings'
require 'puppet/util/errors'
require 'puppet/util/inline_docs'
require 'puppet/parser/ast/leaf'
require 'puppet/dsl'
class Puppet::Resource::Type
Puppet::ResourceType = self
include Puppet::Util::InlineDocs
include Puppet::Util::Warnings
include Puppet::Util::Errors
RESOURCE_SUPERTYPES = [:hostclass, :node, :definition]
attr_accessor :file, :line, :doc, :code, :ruby_code, :parent, :resource_type_collection, :module_name
attr_reader :type, :namespace, :arguments, :behaves_like
RESOURCE_SUPERTYPES.each do |t|
define_method("#{t}?") { self.type == t }
end
require 'puppet/indirector'
extend Puppet::Indirector
indirects :resource_type, :terminus_class => :parser
def self.from_pson(data)
name = data.delete('name') or raise ArgumentError, "Resource Type names must be specified"
type = data.delete('type') || "definition"
data = data.inject({}) { |result, ary| result[ary[0].intern] = ary[1]; result }
new(type, name, data)
end
def to_pson_data_hash
data = [:code, :doc, :line, :file, :parent].inject({}) do |hash, param|
next hash unless value = self.send(param)
hash[param.to_s] = value
hash
end
data['arguments'] = arguments.dup
data['name'] = name
data['type'] = type
data
end
def to_pson(*args)
to_pson_data_hash.to_pson(*args)
end
# Are we a child of the passed class? Do a recursive search up our
# parentage tree to figure it out.
def child_of?(klass)
return false unless parent
return(klass == parent_type ? true : parent_type.child_of?(klass))
end
# Now evaluate the code associated with this class or definition.
def evaluate_code(resource)
scope = resource.scope
if tmp = evaluate_parent_type(resource)
scope = tmp
end
scope = subscope(scope, resource) unless resource.title == :main
scope.compiler.add_class(name) unless definition?
set_resource_parameters(resource, scope)
code.safeevaluate(scope) if code
evaluate_ruby_code(resource, scope) if ruby_code
end
def initialize(type, name, options = {})
@type = type.to_s.downcase.to_sym
raise ArgumentError, "Invalid resource supertype '#{type}'" unless RESOURCE_SUPERTYPES.include?(@type)
name = convert_from_ast(name) if name.is_a?(Puppet::Parser::AST::HostName)
set_name_and_namespace(name)
[:code, :doc, :line, :file, :parent].each do |param|
next unless value = options[param]
send(param.to_s + "=", value)
end
set_arguments(options[:arguments])
end
# This is only used for node names, and really only when the node name
# is a regexp.
def match(string)
return string.to_s.downcase == name unless name_is_regex?
@name =~ string
end
# Add code from a new instance to our code.
def merge(other)
fail "#{name} is not a class; cannot add code to it" unless type == :hostclass
fail "#{other.name} is not a class; cannot add code from it" unless other.type == :hostclass
fail "Cannot have code outside of a class/node/define because 'freeze_main' is enabled" if name == "" and Puppet.settings[:freeze_main]
if parent and other.parent and parent != other.parent
fail "Cannot merge classes with different parent classes (#{name} => #{parent} vs. #{other.name} => #{other.parent})"
end
# We know they're either equal or only one is set, so keep whichever parent is specified.
self.parent ||= other.parent
if other.doc
self.doc ||= ""
self.doc += other.doc
end
# This might just be an empty, stub class.
return unless other.code
unless self.code
self.code = other.code
return
end
array_class = Puppet::Parser::AST::ASTArray
self.code = array_class.new(:children => [self.code]) unless self.code.is_a?(array_class)
if other.code.is_a?(array_class)
code.children += other.code.children
else
code.children << other.code
end
end
# Make an instance of the resource type, and place it in the catalog
# if it isn't in the catalog already. This is only possible for
# classes and nodes. No parameters are be supplied--if this is a
# parameterized class, then all parameters take on their default
# values.
- def ensure_in_catalog(scope)
+ def ensure_in_catalog(scope, parameters=nil)
type == :definition and raise ArgumentError, "Cannot create resources for defined resource types"
resource_type = type == :hostclass ? :class : :node
# Do nothing if the resource already exists; this makes sure we don't
# get multiple copies of the class resource, which helps provide the
# singleton nature of classes.
- if resource = scope.catalog.resource(resource_type, name)
+ # we should not do this for classes with parameters
+ # if parameters are passed, we should still try to create the resource
+ # even if it exists so that we can fail
+ # this prevents us from being able to combine param classes with include
+ if resource = scope.catalog.resource(resource_type, name) and !parameters
return resource
end
-
resource = Puppet::Parser::Resource.new(resource_type, name, :scope => scope, :source => self)
+ if parameters
+ parameters.each do |k,v|
+ resource.set_parameter(k,v)
+ end
+ end
instantiate_resource(scope, resource)
scope.compiler.add_resource(scope, resource)
resource
end
def instantiate_resource(scope, resource)
# Make sure our parent class has been evaluated, if we have one.
if parent && !scope.catalog.resource(resource.type, parent)
parent_type(scope).ensure_in_catalog(scope)
end
if ['Class', 'Node'].include? resource.type
scope.catalog.tag(*resource.tags)
end
end
def name
return @name unless @name.is_a?(Regexp)
@name.source.downcase.gsub(/[^-\w:.]/,'').sub(/^\.+/,'')
end
def name_is_regex?
@name.is_a?(Regexp)
end
# MQR TODO:
#
# The change(s) introduced by the fix for #4270 are mostly silly & should be
# removed, though we didn't realize it at the time. If it can be established/
# ensured that nodes never call parent_type and that resource_types are always
# (as they should be) members of exactly one resource_type_collection the
# following method could / should be replaced with:
#
# def parent_type
# @parent_type ||= parent && (
# resource_type_collection.find_or_load([name],parent,type.to_sym) ||
# fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{resource_type_collection.environment}"
# )
# end
#
# ...and then the rest of the changes around passing in scope reverted.
#
def parent_type(scope = nil)
return nil unless parent
unless @parent_type
raise "Must pass scope to parent_type when called first time" unless scope
unless @parent_type = scope.environment.known_resource_types.send("find_#{type}", [name], parent)
fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{scope.environment}"
end
end
@parent_type
end
# Set any arguments passed by the resource as variables in the scope.
def set_resource_parameters(resource, scope)
set = {}
resource.to_hash.each do |param, value|
param = param.to_sym
fail Puppet::ParseError, "#{resource.ref} does not accept attribute #{param}" unless valid_parameter?(param)
exceptwrap { scope.setvar(param.to_s, value) }
set[param] = true
end
+ if @type == :hostclass
+ scope.setvar("title", resource.title.to_s.downcase) unless set.include? :title
+ scope.setvar("name", resource.name.to_s.downcase ) unless set.include? :name
+ else
+ scope.setvar("title", resource.title ) unless set.include? :title
+ scope.setvar("name", resource.name ) unless set.include? :name
+ end
+ scope.setvar("module_name", module_name) if module_name and ! set.include? :module_name
+
+ if caller_name = scope.parent_module_name and ! set.include?(:caller_module_name)
+ scope.setvar("caller_module_name", caller_name)
+ end
+ scope.class_set(self.name,scope) if hostclass? or node?
# Verify that all required arguments are either present or
# have been provided with defaults.
arguments.each do |param, default|
param = param.to_sym
next if set.include?(param)
# Even if 'default' is a false value, it's an AST value, so this works fine
fail Puppet::ParseError, "Must pass #{param} to #{resource.ref}" unless default
value = default.safeevaluate(scope)
scope.setvar(param.to_s, value)
# Set it in the resource, too, so the value makes it to the client.
resource[param] = value
end
- if @type == :hostclass
- scope.setvar("title", resource.title.to_s.downcase) unless set.include? :title
- scope.setvar("name", resource.name.to_s.downcase ) unless set.include? :name
- else
- scope.setvar("title", resource.title ) unless set.include? :title
- scope.setvar("name", resource.name ) unless set.include? :name
- end
- scope.setvar("module_name", module_name) if module_name and ! set.include? :module_name
-
- if caller_name = scope.parent_module_name and ! set.include?(:caller_module_name)
- scope.setvar("caller_module_name", caller_name)
- end
- scope.class_set(self.name,scope) if hostclass? or node?
end
# Create a new subscope in which to evaluate our code.
def subscope(scope, resource)
scope.newscope :resource => resource, :namespace => self.namespace, :source => self
end
# Check whether a given argument is valid.
def valid_parameter?(param)
param = param.to_s
return true if param == "name"
return true if Puppet::Type.metaparam?(param)
return false unless defined?(@arguments)
return(arguments.include?(param) ? true : false)
end
def set_arguments(arguments)
@arguments = {}
return if arguments.nil?
arguments.each do |arg, default|
arg = arg.to_s
warn_if_metaparam(arg, default)
@arguments[arg] = default
end
end
private
def convert_from_ast(name)
value = name.value
if value.is_a?(Puppet::Parser::AST::Regex)
name = value.value
else
name = value
end
end
def evaluate_parent_type(resource)
return unless klass = parent_type(resource.scope) and parent_resource = resource.scope.compiler.catalog.resource(:class, klass.name) || resource.scope.compiler.catalog.resource(:node, klass.name)
parent_resource.evaluate unless parent_resource.evaluated?
parent_scope(resource.scope, klass)
end
def evaluate_ruby_code(resource, scope)
Puppet::DSL::ResourceAPI.new(resource, scope, ruby_code).evaluate
end
# Split an fq name into a namespace and name
def namesplit(fullname)
ary = fullname.split("::")
n = ary.pop || ""
ns = ary.join("::")
return ns, n
end
def parent_scope(scope, klass)
scope.class_scope(klass) || raise(Puppet::DevError, "Could not find scope for #{klass.name}")
end
def set_name_and_namespace(name)
if name.is_a?(Regexp)
@name = name
@namespace = ""
else
@name = name.to_s.downcase
# Note we're doing something somewhat weird here -- we're setting
# the class's namespace to its fully qualified name. This means
# anything inside that class starts looking in that namespace first.
@namespace, ignored_shortname = @type == :hostclass ? [@name, ''] : namesplit(@name)
end
end
def warn_if_metaparam(param, default)
return unless Puppet::Type.metaparamclass(param)
if default
warnonce "#{param} is a metaparam; this value will inherit to all contained resources"
else
raise Puppet::ParseError, "#{param} is a metaparameter; please choose another parameter name in the #{self.name} definition"
end
end
end
diff --git a/lib/puppet/transaction/resource_harness.rb b/lib/puppet/transaction/resource_harness.rb
index c1b980632..4a3d35e0d 100644
--- a/lib/puppet/transaction/resource_harness.rb
+++ b/lib/puppet/transaction/resource_harness.rb
@@ -1,196 +1,178 @@
require 'puppet/resource/status'
class Puppet::Transaction::ResourceHarness
extend Forwardable
def_delegators :@transaction, :relationship_graph
attr_reader :transaction
def allow_changes?(resource)
if resource.purging? and resource.deleting? and deps = relationship_graph.dependents(resource) \
and ! deps.empty? and deps.detect { |d| ! d.deleting? }
deplabel = deps.collect { |r| r.ref }.join(",")
plurality = deps.length > 1 ? "":"s"
resource.warning "#{deplabel} still depend#{plurality} on me -- not purging"
false
else
true
end
end
# Used mostly for scheduling and auditing at this point.
def cached(resource, name)
Puppet::Util::Storage.cache(resource)[name]
end
# Used mostly for scheduling and auditing at this point.
def cache(resource, name, value)
Puppet::Util::Storage.cache(resource)[name] = value
end
def perform_changes(resource)
current = resource.retrieve_resource
cache resource, :checked, Time.now
return [] if ! allow_changes?(resource)
current_values = current.to_hash
historical_values = Puppet::Util::Storage.cache(resource).dup
desired_values = {}
resource.properties.each do |property|
desired_values[property.name] = property.should
end
audited_params = (resource[:audit] || []).map { |p| p.to_sym }
synced_params = []
# Record the current state in state.yml.
audited_params.each do |param|
cache(resource, param, current_values[param])
end
# Update the machine state & create logs/events
events = []
ensure_param = resource.parameter(:ensure)
- if desired_values[:ensure] && !ensure_param.insync?(current_values[:ensure])
+ if desired_values[:ensure] && !ensure_param.safe_insync?(current_values[:ensure])
events << apply_parameter(ensure_param, current_values[:ensure], audited_params.include?(:ensure), historical_values[:ensure])
synced_params << :ensure
elsif current_values[:ensure] != :absent
work_order = resource.properties # Note: only the resource knows what order to apply changes in
work_order.each do |param|
- if desired_values[param.name] && !param.insync?(current_values[param.name])
+ if desired_values[param.name] && !param.safe_insync?(current_values[param.name])
events << apply_parameter(param, current_values[param.name], audited_params.include?(param.name), historical_values[param.name])
synced_params << param.name
end
end
end
# Add more events to capture audit results
audited_params.each do |param_name|
if historical_values.include?(param_name)
if historical_values[param_name] != current_values[param_name] && !synced_params.include?(param_name)
event = create_change_event(resource.parameter(param_name), current_values[param_name], true, historical_values[param_name])
event.send_log
events << event
end
else
resource.property(param_name).notice "audit change: newly-recorded value #{current_values[param_name]}"
end
end
events
end
def create_change_event(property, current_value, do_audit, historical_value)
event = property.event
event.previous_value = current_value
event.desired_value = property.should
event.historical_value = historical_value
if do_audit
event.audited = true
event.status = "audit"
if historical_value != current_value
event.message = "audit change: previously recorded value #{property.is_to_s(historical_value)} has been changed to #{property.is_to_s(current_value)}"
end
end
event
end
def apply_parameter(property, current_value, do_audit, historical_value)
event = create_change_event(property, current_value, do_audit, historical_value)
if do_audit && historical_value && historical_value != current_value
brief_audit_message = " (previously recorded value was #{property.is_to_s(historical_value)})"
else
brief_audit_message = ""
end
if property.noop
event.message = "current_value #{property.is_to_s(current_value)}, should be #{property.should_to_s(property.should)} (noop)#{brief_audit_message}"
event.status = "noop"
else
property.sync
event.message = [ property.change_to_s(current_value, property.should), brief_audit_message ].join
event.status = "success"
end
event
rescue => detail
puts detail.backtrace if Puppet[:trace]
event.status = "failure"
event.message = "change from #{property.is_to_s(current_value)} to #{property.should_to_s(property.should)} failed: #{detail}"
event
ensure
event.send_log
end
def evaluate(resource)
start = Time.now
status = Puppet::Resource::Status.new(resource)
perform_changes(resource).each do |event|
status << event
end
if status.changed? && ! resource.noop?
cache(resource, :synced, Time.now)
resource.flush if resource.respond_to?(:flush)
end
return status
rescue => detail
resource.fail "Could not create resource status: #{detail}" unless status
puts detail.backtrace if Puppet[:trace]
resource.err "Could not evaluate: #{detail}"
status.failed = true
return status
ensure
(status.evaluation_time = Time.now - start) if status
end
def initialize(transaction)
@transaction = transaction
end
def scheduled?(status, resource)
return true if Puppet[:ignoreschedules]
return true unless schedule = schedule(resource)
# We use 'checked' here instead of 'synced' because otherwise we'll
# end up checking most resources most times, because they will generally
# have been synced a long time ago (e.g., a file only gets updated
# once a month on the server and its schedule is daily; the last sync time
# will have been a month ago, so we'd end up checking every run).
schedule.match?(cached(resource, :checked).to_i)
end
def schedule(resource)
unless resource.catalog
resource.warning "Cannot schedule without a schedule-containing catalog"
return nil
end
return nil unless name = resource[:schedule]
resource.catalog.resource(:schedule, name) || resource.fail("Could not find schedule #{name}")
end
-
- private
-
- def absent_and_not_being_created?(current, param)
- current[:ensure] == :absent and param.should.nil?
- end
-
- def ensure_is_insync?(current, param)
- param.insync?(current[:ensure])
- end
-
- def ensure_should_be_absent?(current, param)
- param.should == :absent
- end
-
- def param_is_insync?(current, param)
- param.insync?(current[param.name])
- end
end
diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb
index ea3944b4e..e03650b54 100644
--- a/lib/puppet/type.rb
+++ b/lib/puppet/type.rb
@@ -1,1897 +1,1897 @@
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)
if property
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
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}.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
if provider and ! provider.class.supports_parameter?(klass)
missing = klass.required_features.find_all { |f| ! provider.class.feature?(f) }
info "Provider %s does not support features %s; not managing attribute %s" % [provider.class.name, missing.join(", "), name]
return nil
end
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
+ if property.safe_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)
+ unless property.safe_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).collect {|p| p.to_sym}
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.kind_of?(Array) && list.to_sym == :all
list = all_properties
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 [Language Tutorial](http://docs.puppetlabs.com/guides/language_tutorial.html) 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/type/cron.rb b/lib/puppet/type/cron.rb
index 76399d693..4b18e71f9 100755
--- a/lib/puppet/type/cron.rb
+++ b/lib/puppet/type/cron.rb
@@ -1,414 +1,410 @@
require 'etc'
require 'facter'
require 'puppet/util/filetype'
Puppet::Type.newtype(:cron) do
@doc = "Installs and manages cron jobs. All fields except the command
and the user are optional, although specifying no periodic
fields would result in the command being executed every
minute. While the name of the cron job is not part of the actual
job, it is used by Puppet to store and retrieve it.
If you specify a cron job that matches an existing job in every way
except name, then the jobs will be considered equivalent and the
new name will be permanently associated with that job. Once this
association is made and synced to disk, you can then manage the job
normally (e.g., change the schedule of the job).
Example:
cron { logrotate:
command => \"/usr/sbin/logrotate\",
user => root,
hour => 2,
minute => 0
}
Note that all cron values can be specified as an array of values:
cron { logrotate:
command => \"/usr/sbin/logrotate\",
user => root,
hour => [2, 4]
}
Or using ranges, or the step syntax `*/2` (although there's no guarantee that
your `cron` daemon supports it):
cron { logrotate:
command => \"/usr/sbin/logrotate\",
user => root,
hour => ['2-4'],
minute => '*/10'
}
"
ensurable
# A base class for all of the Cron parameters, since they all have
# similar argument checking going on.
class CronParam < Puppet::Property
class << self
attr_accessor :boundaries, :default
end
# We have to override the parent method, because we consume the entire
# "should" array
def insync?(is)
- if @should
- self.is_to_s(is) == self.should_to_s
- else
- true
- end
+ self.is_to_s(is) == self.should_to_s
end
# A method used to do parameter input handling. Converts integers
# in string form to actual integers, and returns the value if it's
# an integer or false if it's just a normal string.
def numfix(num)
if num =~ /^\d+$/
return num.to_i
elsif num.is_a?(Integer)
return num
else
return false
end
end
# Verify that a number is within the specified limits. Return the
# number if it is, or false if it is not.
def limitcheck(num, lower, upper)
(num >= lower and num <= upper) && num
end
# Verify that a value falls within the specified array. Does case
# insensitive matching, and supports matching either the entire word
# or the first three letters of the word.
def alphacheck(value, ary)
tmp = value.downcase
# If they specified a shortened version of the name, then see
# if we can lengthen it (e.g., mon => monday).
if tmp.length == 3
ary.each_with_index { |name, index|
if name =~ /#{tmp}/i
return index
end
}
else
return ary.index(tmp) if ary.include?(tmp)
end
false
end
def should_to_s(newvalue = @should)
if newvalue
newvalue = [newvalue] unless newvalue.is_a?(Array)
if self.name == :command or newvalue[0].is_a? Symbol
newvalue[0]
else
newvalue.join(",")
end
else
nil
end
end
def is_to_s(currentvalue = @is)
if currentvalue
return currentvalue unless currentvalue.is_a?(Array)
if self.name == :command or currentvalue[0].is_a? Symbol
currentvalue[0]
else
currentvalue.join(",")
end
else
nil
end
end
def should
if @should and @should[0] == :absent
:absent
else
@should
end
end
def should=(ary)
super
@should.flatten!
end
# The method that does all of the actual parameter value
# checking; called by all of the +param=+ methods.
# Requires the value, type, and bounds, and optionally supports
# a boolean of whether to do alpha checking, and if so requires
# the ary against which to do the checking.
munge do |value|
# Support 'absent' as a value, so that they can remove
# a value
if value == "absent" or value == :absent
return :absent
end
# Allow the */2 syntax
if value =~ /^\*\/[0-9]+$/
return value
end
# Allow ranges
if value =~ /^[0-9]+-[0-9]+$/
return value
end
# Allow ranges + */2
if value =~ /^[0-9]+-[0-9]+\/[0-9]+$/
return value
end
if value == "*"
return :absent
end
return value unless self.class.boundaries
lower, upper = self.class.boundaries
retval = nil
if num = numfix(value)
retval = limitcheck(num, lower, upper)
elsif respond_to?(:alpha)
# If it has an alpha method defined, then we check
# to see if our value is in that list and if so we turn
# it into a number
retval = alphacheck(value, alpha)
end
if retval
return retval.to_s
else
self.fail "#{value} is not a valid #{self.class.name}"
end
end
end
# Somewhat uniquely, this property does not actually change anything -- it
# just calls +@resource.sync+, which writes out the whole cron tab for
# the user in question. There is no real way to change individual cron
# jobs without rewriting the entire cron file.
#
# Note that this means that managing many cron jobs for a given user
# could currently result in multiple write sessions for that user.
newproperty(:command, :parent => CronParam) do
desc "The command to execute in the cron job. The environment
provided to the command varies by local system rules, and it is
best to always provide a fully qualified command. The user's
profile is not sourced when the command is run, so if the
user's environment is desired it should be sourced manually.
All cron parameters support `absent` as a value; this will
remove any existing values for that field."
def retrieve
return_value = super
return_value = return_value[0] if return_value && return_value.is_a?(Array)
return_value
end
def should
if @should
if @should.is_a? Array
@should[0]
else
devfail "command is not an array"
end
else
nil
end
end
end
newproperty(:special) do
desc "Special schedules"
def specials
%w{reboot yearly annually monthly weekly daily midnight hourly}
end
validate do |value|
raise ArgumentError, "Invalid special schedule #{value.inspect}" unless specials.include?(value)
end
end
newproperty(:minute, :parent => CronParam) do
self.boundaries = [0, 59]
desc "The minute at which to run the cron job.
Optional; if specified, must be between 0 and 59, inclusive."
end
newproperty(:hour, :parent => CronParam) do
self.boundaries = [0, 23]
desc "The hour at which to run the cron job. Optional;
if specified, must be between 0 and 23, inclusive."
end
newproperty(:weekday, :parent => CronParam) do
def alpha
%w{sunday monday tuesday wednesday thursday friday saturday}
end
self.boundaries = [0, 7]
desc "The weekday on which to run the command.
Optional; if specified, must be between 0 and 7, inclusive, with
0 (or 7) being Sunday, or must be the name of the day (e.g., Tuesday)."
end
newproperty(:month, :parent => CronParam) do
def alpha
%w{january february march april may june july
august september october november december}
end
self.boundaries = [1, 12]
desc "The month of the year. Optional; if specified
must be between 1 and 12 or the month name (e.g., December)."
end
newproperty(:monthday, :parent => CronParam) do
self.boundaries = [1, 31]
desc "The day of the month on which to run the
command. Optional; if specified, must be between 1 and 31."
end
newproperty(:environment) do
desc "Any environment settings associated with this cron job. They
will be stored between the header and the job in the crontab. There
can be no guarantees that other, earlier settings will not also
affect a given cron job.
Also, Puppet cannot automatically determine whether an existing,
unmanaged environment setting is associated with a given cron
job. If you already have cron jobs with environment settings,
then Puppet will keep those settings in the same place in the file,
but will not associate them with a specific job.
Settings should be specified exactly as they should appear in
the crontab, e.g., `PATH=/bin:/usr/bin:/usr/sbin`."
validate do |value|
unless value =~ /^\s*(\w+)\s*=\s*(.*)\s*$/ or value == :absent or value == "absent"
raise ArgumentError, "Invalid environment setting #{value.inspect}"
end
end
def insync?(is)
if is.is_a? Array
return is.sort == @should.sort
else
return is == @should
end
end
def is_to_s(newvalue)
if newvalue
if newvalue.is_a?(Array)
newvalue.join(",")
else
newvalue
end
else
nil
end
end
def should
@should
end
def should_to_s(newvalue = @should)
if newvalue
newvalue.join(",")
else
nil
end
end
end
newparam(:name) do
desc "The symbolic name of the cron job. This name
is used for human reference only and is generated automatically
for cron jobs found on the system. This generally won't
matter, as Puppet will do its best to match existing cron jobs
against specified jobs (and Puppet adds a comment to cron jobs it adds), but it is at least possible that converting from
unmanaged jobs to managed jobs might require manual
intervention."
isnamevar
end
newproperty(:user) do
desc "The user to run the command as. This user must
be allowed to run cron jobs, which is not currently checked by
Puppet.
The user defaults to whomever Puppet is running as."
defaultto { Etc.getpwuid(Process.uid).name || "root" }
end
newproperty(:target) do
desc "Where the cron job should be stored. For crontab-style
entries this is the same as the user and defaults that way.
Other providers default accordingly."
defaultto {
if provider.is_a?(@resource.class.provider(:crontab))
if val = @resource.should(:user)
val
else
raise ArgumentError,
"You must provide a user with crontab entries"
end
elsif provider.class.ancestors.include?(Puppet::Provider::ParsedFile)
provider.class.default_target
else
nil
end
}
end
# We have to reorder things so that :provide is before :target
attr_accessor :uid
def value(name)
name = symbolize(name)
ret = nil
if obj = @parameters[name]
ret = obj.should
ret ||= obj.retrieve
if ret == :absent
ret = nil
end
end
unless ret
case name
when :command
devfail "No command, somehow"
when :special
# nothing
else
#ret = (self.class.validproperty?(name).default || "*").to_s
ret = "*"
end
end
ret
end
end
diff --git a/lib/puppet/type/exec.rb b/lib/puppet/type/exec.rb
index 606888c75..8019af67e 100755
--- a/lib/puppet/type/exec.rb
+++ b/lib/puppet/type/exec.rb
@@ -1,679 +1,649 @@
module Puppet
newtype(:exec) do
include Puppet::Util::Execution
require 'timeout'
@doc = "Executes external commands. It is critical that all commands
executed using this mechanism can be run multiple times without
harm, i.e., they are *idempotent*. One useful way to create idempotent
commands is to use the checks like `creates` to avoid running the
command unless some condition is met.
- Note also that you can restrict an `exec` to only run when it receives
+ Note that you can restrict an `exec` to only run when it receives
events by using the `refreshonly` parameter; this is a useful way to
have your configuration respond to events with arbitrary commands.
- It is worth noting that `exec` is special, in that it is not
- currently considered an error to have multiple `exec` instances
- with the same name. This was done purely because it had to be this
- way in order to get certain functionality, but it complicates things.
- In particular, you will not be able to use `exec` instances that
- share their commands with other instances as a dependency, since
- Puppet has no way of knowing which instance you mean.
-
- For example:
-
- # defined in the production class
- exec { \"make\":
- cwd => \"/prod/build/dir\",
- path => \"/usr/bin:/usr/sbin:/bin\"
- }
-
- . etc. .
-
- # defined in the test class
- exec { \"make\":
- cwd => \"/test/build/dir\",
- path => \"/usr/bin:/usr/sbin:/bin\"
- }
-
- Any other type would throw an error, complaining that you had
- the same instance being managed in multiple places, but these are
- obviously different images, so `exec` had to be treated specially.
-
- It is recommended to avoid duplicate names whenever possible.
-
- Note that if an `exec` receives an event from another resource,
+ Note also that if an `exec` receives an event from another resource,
it will get executed again (or execute the command specified in `refresh`, if there is one).
There is a strong tendency to use `exec` to do whatever work Puppet
can't already do; while this is obviously acceptable (and unavoidable)
in the short term, it is highly recommended to migrate work from `exec`
to native Puppet types as quickly as possible. If you find that
you are doing a lot of work with `exec`, please at least notify
us at Puppet Labs what you are doing, and hopefully we can work with
you to get a native resource type for the work you are doing."
require 'open3'
# Create a new check mechanism. It's basically just a parameter that
# provides one extra 'check' method.
def self.newcheck(name, &block)
@checks ||= {}
check = newparam(name, &block)
@checks[name] = check
end
def self.checks
@checks.keys
end
newproperty(:returns, :array_matching => :all, :event => :executed_command) do |property|
include Puppet::Util::Execution
munge do |value|
value.to_s
end
def event_name
:executed_command
end
defaultto "0"
attr_reader :output
desc "The expected return code(s). An error will be returned if the
executed command returns something else. Defaults to 0. Can be
specified as an array of acceptable return codes or a single value."
# Make output a bit prettier
def change_to_s(currentvalue, newvalue)
"executed successfully"
end
# First verify that all of our checks pass.
def retrieve
# Default to somethinng
if @resource.check
return :notrun
else
return self.should
end
end
# Actually execute the command.
def sync
olddir = nil
# We need a dir to change to, even if it's just the cwd
dir = self.resource[:cwd] || Dir.pwd
event = :executed_command
tries = self.resource[:tries]
try_sleep = self.resource[:try_sleep]
begin
tries.times do |try|
# Only add debug messages for tries > 1 to reduce log spam.
debug("Exec try #{try+1}/#{tries}") if tries > 1
@output, @status = @resource.run(self.resource[:command])
break if self.should.include?(@status.exitstatus.to_s)
if try_sleep > 0 and tries > 1
debug("Sleeping for #{try_sleep} seconds between tries")
sleep try_sleep
end
end
rescue Timeout::Error
self.fail "Command exceeded timeout" % value.inspect
end
if log = @resource[:logoutput]
case log
when :true
log = @resource[:loglevel]
when :on_failure
unless self.should.include?(@status.exitstatus.to_s)
log = @resource[:loglevel]
else
log = :false
end
end
unless log == :false
@output.split(/\n/).each { |line|
self.send(log, line)
}
end
end
unless self.should.include?(@status.exitstatus.to_s)
self.fail("#{self.resource[:command]} returned #{@status.exitstatus} instead of one of [#{self.should.join(",")}]")
end
event
end
end
newparam(:command) do
isnamevar
desc "The actual command to execute. Must either be fully qualified
or a search path for the command must be provided. If the command
succeeds, any output produced will be logged at the instance's
normal log level (usually `notice`), but if the command fails
(meaning its return code does not match the specified code) then
any output is logged at the `err` log level."
end
newparam(:path) do
desc "The search path used for command execution.
Commands must be fully qualified if no path is specified. Paths
can be specified as an array or as a colon-separated list."
# Support both arrays and colon-separated fields.
def value=(*values)
@value = values.flatten.collect { |val|
if val =~ /;/ # recognize semi-colon separated paths
val.split(";")
elsif val =~ /^\w:[^:]*$/ # heuristic to avoid splitting a driveletter away
val
else
val.split(":")
end
}.flatten
end
end
newparam(:user) do
desc "The user to run the command as. Note that if you
use this then any error output is not currently captured. This
is because of a bug within Ruby. If you are using Puppet to
create this user, the exec will automatically require the user,
as long as it is specified by name."
# Most validation is handled by the SUIDManager class.
validate do |user|
self.fail "Only root can execute commands as other users" unless Puppet.features.root?
end
end
newparam(:group) do
desc "The group to run the command as. This seems to work quite
haphazardly on different platforms -- it is a platform issue
not a Ruby or Puppet one, since the same variety exists when
running commnands as different users in the shell."
# Validation is handled by the SUIDManager class.
end
newparam(:cwd) do
desc "The directory from which to run the command. If
this directory does not exist, the command will fail."
validate do |dir|
unless dir =~ /^#{File::SEPARATOR}/
self.fail("CWD must be a fully qualified path")
end
end
munge do |dir|
dir = dir[0] if dir.is_a?(Array)
dir
end
end
newparam(:logoutput) do
desc "Whether to log output. Defaults to logging output at the
loglevel for the `exec` resource. Use *on_failure* to only
log the output when the command reports an error. Values are
**true**, *false*, *on_failure*, and any legal log level."
newvalues(:true, :false, :on_failure)
end
newparam(:refresh) do
desc "How to refresh this command. By default, the exec is just
called again when it receives an event from another resource,
but this parameter allows you to define a different command
for refreshing."
validate do |command|
@resource.validatecmd(command)
end
end
newparam(:env) do
desc "This parameter is deprecated. Use 'environment' instead."
munge do |value|
warning "'env' is deprecated on exec; use 'environment' instead."
resource[:environment] = value
end
end
newparam(:environment) do
desc "Any additional environment variables you want to set for a
command. Note that if you use this to set PATH, it will override
the `path` attribute. Multiple environment variables should be
specified as an array."
validate do |values|
values = [values] unless values.is_a? Array
values.each do |value|
unless value =~ /\w+=/
raise ArgumentError, "Invalid environment setting '#{value}'"
end
end
end
end
newparam(:timeout) do
desc "The maximum time the command should take. If the command takes
longer than the timeout, the command is considered to have failed
and will be stopped. Use any negative number to disable the timeout.
The time is specified in seconds."
munge do |value|
value = value.shift if value.is_a?(Array)
if value.is_a?(String)
unless value =~ /^[-\d.]+$/
raise ArgumentError, "The timeout must be a number."
end
Float(value)
else
value
end
end
defaultto 300
end
newparam(:tries) do
desc "The number of times execution of the command should be tried.
Defaults to '1'. This many attempts will be made to execute
the command until an acceptable return code is returned.
Note that the timeout paramater applies to each try rather than
to the complete set of tries."
munge do |value|
if value.is_a?(String)
unless value =~ /^[\d]+$/
raise ArgumentError, "Tries must be an integer"
end
value = Integer(value)
end
raise ArgumentError, "Tries must be an integer >= 1" if value < 1
value
end
defaultto 1
end
newparam(:try_sleep) do
desc "The time to sleep in seconds between 'tries'."
munge do |value|
if value.is_a?(String)
unless value =~ /^[-\d.]+$/
raise ArgumentError, "try_sleep must be a number"
end
value = Float(value)
end
raise ArgumentError, "try_sleep cannot be a negative number" if value < 0
value
end
defaultto 0
end
newcheck(:refreshonly) do
desc "The command should only be run as a
refresh mechanism for when a dependent object is changed. It only
makes sense to use this option when this command depends on some
other object; it is useful for triggering an action:
# Pull down the main aliases file
file { \"/etc/aliases\":
source => \"puppet://server/module/aliases\"
}
# Rebuild the database, but only when the file changes
exec { newaliases:
path => [\"/usr/bin\", \"/usr/sbin\"],
subscribe => File[\"/etc/aliases\"],
refreshonly => true
}
Note that only `subscribe` and `notify` can trigger actions, not `require`,
so it only makes sense to use `refreshonly` with `subscribe` or `notify`."
newvalues(:true, :false)
# We always fail this test, because we're only supposed to run
# on refresh.
def check(value)
# We have to invert the values.
if value == :true
false
else
true
end
end
end
newcheck(:creates) do
desc "A file that this command creates. If this
parameter is provided, then the command will only be run
if the specified file does not exist:
exec { \"tar xf /my/tar/file.tar\":
cwd => \"/var/tmp\",
creates => \"/var/tmp/myfile\",
path => [\"/usr/bin\", \"/usr/sbin\"]
}
"
# FIXME if they try to set this and fail, then we should probably
# fail the entire exec, right?
validate do |files|
files = [files] unless files.is_a? Array
files.each do |file|
self.fail("'creates' must be set to a fully qualified path") unless file
unless file =~ %r{^#{File::SEPARATOR}}
self.fail "'creates' files must be fully qualified."
end
end
end
# If the file exists, return false (i.e., don't run the command),
# else return true
def check(value)
! FileTest.exists?(value)
end
end
newcheck(:unless) do
desc "If this parameter is set, then this `exec` will run unless
the command returns 0. For example:
exec { \"/bin/echo root >> /usr/lib/cron/cron.allow\":
path => \"/usr/bin:/usr/sbin:/bin\",
unless => \"grep root /usr/lib/cron/cron.allow 2>/dev/null\"
}
This would add `root` to the cron.allow file (on Solaris) unless
`grep` determines it's already there.
Note that this command follows the same rules as the main command,
which is to say that it must be fully qualified if the path is not set.
"
validate do |cmds|
cmds = [cmds] unless cmds.is_a? Array
cmds.each do |cmd|
@resource.validatecmd(cmd)
end
end
# Return true if the command does not return 0.
def check(value)
begin
output, status = @resource.run(value, true)
rescue Timeout::Error
err "Check #{value.inspect} exceeded timeout"
return false
end
status.exitstatus != 0
end
end
newcheck(:onlyif) do
desc "If this parameter is set, then this `exec` will only run if
the command returns 0. For example:
exec { \"logrotate\":
path => \"/usr/bin:/usr/sbin:/bin\",
onlyif => \"test `du /var/log/messages | cut -f1` -gt 100000\"
}
This would run `logrotate` only if that test returned true.
Note that this command follows the same rules as the main command,
which is to say that it must be fully qualified if the path is not set.
Also note that onlyif can take an array as its value, e.g.:
onlyif => [\"test -f /tmp/file1\", \"test -f /tmp/file2\"]
This will only run the exec if /all/ conditions in the array return true.
"
validate do |cmds|
cmds = [cmds] unless cmds.is_a? Array
cmds.each do |cmd|
@resource.validatecmd(cmd)
end
end
# Return true if the command returns 0.
def check(value)
begin
output, status = @resource.run(value, true)
rescue Timeout::Error
err "Check #{value.inspect} exceeded timeout"
return false
end
status.exitstatus == 0
end
end
# Exec names are not isomorphic with the objects.
@isomorphic = false
validate do
validatecmd(self[:command])
end
# FIXME exec should autorequire any exec that 'creates' our cwd
autorequire(:file) do
reqs = []
# Stick the cwd in there if we have it
reqs << self[:cwd] if self[:cwd]
self[:command].scan(/^(#{File::SEPARATOR}\S+)/) { |str|
reqs << str
}
self[:command].scan(/^"([^"]+)"/) { |str|
reqs << str
}
[:onlyif, :unless].each { |param|
next unless tmp = self[param]
tmp = [tmp] unless tmp.is_a? Array
tmp.each do |line|
# And search the command line for files, adding any we
# find. This will also catch the command itself if it's
# fully qualified. It might not be a bad idea to add
# unqualified files, but, well, that's a bit more annoying
# to do.
reqs += line.scan(%r{(#{File::SEPARATOR}\S+)})
end
}
# For some reason, the += isn't causing a flattening
reqs.flatten!
reqs
end
autorequire(:user) do
# Autorequire users if they are specified by name
if user = self[:user] and user !~ /^\d+$/
user
end
end
def self.instances
[]
end
# Verify that we pass all of the checks. The argument determines whether
# we skip the :refreshonly check, which is necessary because we now check
# within refresh
def check(refreshing = false)
self.class.checks.each { |check|
next if refreshing and check == :refreshonly
if @parameters.include?(check)
val = @parameters[check].value
val = [val] unless val.is_a? Array
val.each do |value|
return false unless @parameters[check].check(value)
end
end
}
true
end
# Verify that we have the executable
def checkexe(cmd)
exe = extractexe(cmd)
if self[:path]
if Puppet.features.posix? and !File.exists?(exe)
withenv :PATH => self[:path].join(File::PATH_SEPARATOR) do
exe = which(exe) || raise(ArgumentError,"Could not find command '#{exe}'")
end
elsif Puppet.features.microsoft_windows? and !File.exists?(exe)
self[:path].each do |path|
[".exe", ".ps1", ".bat", ".com", ""].each do |extension|
file = File.join(path, exe+extension)
return if File.exists?(file)
end
end
end
end
raise ArgumentError, "Could not find executable '#{exe}'" unless FileTest.exists?(exe)
unless FileTest.executable?(exe)
raise ArgumentError,
"'#{exe}' is not executable"
end
end
def output
if self.property(:returns).nil?
return nil
else
return self.property(:returns).output
end
end
# Run the command, or optionally run a separately-specified command.
def refresh
if self.check(true)
if cmd = self[:refresh]
self.run(cmd)
else
self.property(:returns).sync
end
end
end
# Run a command.
def run(command, check = false)
output = nil
status = nil
dir = nil
checkexe(command)
if dir = self[:cwd]
unless File.directory?(dir)
if check
dir = nil
else
self.fail "Working directory '#{dir}' does not exist"
end
end
end
dir ||= Dir.pwd
if check
debug "Executing check '#{command}'"
else
debug "Executing '#{command}'"
end
begin
# Do our chdir
Dir.chdir(dir) do
environment = {}
environment[:PATH] = self[:path].join(":") if self[:path]
if envlist = self[:environment]
envlist = [envlist] unless envlist.is_a? Array
envlist.each do |setting|
if setting =~ /^(\w+)=((.|\n)+)$/
name = $1
value = $2
if environment.include? name
warning(
"Overriding environment setting '#{name}' with '#{value}'"
)
end
environment[name] = value
else
warning "Cannot understand environment setting #{setting.inspect}"
end
end
end
withenv environment do
Timeout::timeout(self[:timeout]) do
output, status = Puppet::Util::SUIDManager.run_and_capture(
[command], self[:user], self[:group]
)
end
# The shell returns 127 if the command is missing.
if status.exitstatus == 127
raise ArgumentError, output
end
end
end
rescue Errno::ENOENT => detail
self.fail detail.to_s
end
return output, status
end
def validatecmd(cmd)
exe = extractexe(cmd)
# if we're not fully qualified, require a path
self.fail "'#{cmd}' is both unqualifed and specified no search path" if File.expand_path(exe) != exe and self[:path].nil?
end
def extractexe(cmd)
# easy case: command was quoted
if cmd =~ /^"([^"]+)"/
$1
else
cmd.split(/ /)[0]
end
end
end
end
diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb
index eee948cd5..cbb51bbed 100644
--- a/lib/puppet/type/file.rb
+++ b/lib/puppet/type/file.rb
@@ -1,801 +1,791 @@
require 'digest/md5'
require 'cgi'
require 'etc'
require 'uri'
require 'fileutils'
require 'puppet/network/handler'
require 'puppet/util/diff'
require 'puppet/util/checksums'
require 'puppet/network/client'
require 'puppet/util/backups'
Puppet::Type.newtype(:file) do
include Puppet::Util::MethodHelper
include Puppet::Util::Checksums
include Puppet::Util::Backups
@doc = "Manages local files, including setting ownership and
permissions, creation of both files and directories, and
retrieving entire files from remote servers. As Puppet matures, it
expected that the `file` resource will be used less and less to
manage content, and instead native resources will be used to do so.
If you find that you are often copying files in from a central
location, rather than using native resources, please contact
Puppet Labs and we can hopefully work with you to develop a
native resource to support what you are doing."
def self.title_patterns
[ [ /^(.*?)\/*\Z/m, [ [ :path, lambda{|x| x} ] ] ] ]
end
newparam(:path) do
desc "The path to the file to manage. Must be fully qualified."
isnamevar
validate do |value|
# accept various path syntaxes: lone slash, posix, win32, unc
- unless (Puppet.features.posix? and (value =~ /^\/$/ or value =~ /^\/[^\/]/)) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/))
+ unless (Puppet.features.posix? and value =~ /^\//) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/))
fail Puppet::Error, "File paths must be fully qualified, not '#{value}'"
end
end
# convert the current path in an index into the collection and the last
# path name. The aim is to use less storage for all common paths in a hierarchy
munge do |value|
path, name = File.split(value.gsub(/\/+/,'/'))
{ :index => Puppet::FileCollection.collection.index(path), :name => name }
end
# and the reverse
unmunge do |value|
basedir = Puppet::FileCollection.collection.path(value[:index])
# a lone slash as :name indicates a root dir on windows
if value[:name] == '/'
basedir
else
File.join( basedir, value[:name] )
end
end
end
newparam(:backup) do
desc "Whether files should be backed up before
being replaced. The preferred method of backing files up is via
a `filebucket`, which stores files by their MD5 sums and allows
easy retrieval without littering directories with backups. You
can specify a local filebucket or a network-accessible
server-based filebucket by setting `backup => bucket-name`.
Alternatively, if you specify any value that begins with a `.`
(e.g., `.puppet-bak`), then Puppet will use copy the file in
the same directory with that value as the extension of the
backup. Setting `backup => false` disables all backups of the
file in question.
Puppet automatically creates a local filebucket named `puppet` and
defaults to backing up there. To use a server-based filebucket,
you must specify one in your configuration
filebucket { main:
server => puppet
}
The `puppet master` daemon creates a filebucket by default,
so you can usually back up to your main server with this
configuration. Once you've described the bucket in your
configuration, you can use it in any file
file { \"/my/file\":
source => \"/path/in/nfs/or/something\",
backup => main
}
This will back the file up to the central server.
At this point, the benefits of using a filebucket are that you do not
have backup files lying around on each of your machines, a given
version of a file is only backed up once, and you can restore
any given file manually, no matter how old. Eventually,
transactional support will be able to automatically restore
filebucketed files.
"
defaultto "puppet"
munge do |value|
# I don't really know how this is happening.
value = value.shift if value.is_a?(Array)
case value
when false, "false", :false
false
when true, "true", ".puppet-bak", :true
".puppet-bak"
when String
value
else
self.fail "Invalid backup type #{value.inspect}"
end
end
end
newparam(:recurse) do
desc "Whether and how deeply to do recursive
management."
newvalues(:true, :false, :inf, :remote, /^[0-9]+$/)
# Replace the validation so that we allow numbers in
# addition to string representations of them.
validate { |arg| }
munge do |value|
newval = super(value)
case newval
when :true, :inf; true
when :false; false
when :remote; :remote
when Integer, Fixnum, Bignum
self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit"
# recurse == 0 means no recursion
return false if value == 0
resource[:recurselimit] = value
true
when /^\d+$/
self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit"
value = Integer(value)
# recurse == 0 means no recursion
return false if value == 0
resource[:recurselimit] = value
true
else
self.fail "Invalid recurse value #{value.inspect}"
end
end
end
newparam(:recurselimit) do
desc "How deeply to do recursive management."
newvalues(/^[0-9]+$/)
munge do |value|
newval = super(value)
case newval
when Integer, Fixnum, Bignum; value
when /^\d+$/; Integer(value)
else
self.fail "Invalid recurselimit value #{value.inspect}"
end
end
end
newparam(:replace, :boolean => true) do
desc "Whether or not to replace a file that is
sourced but exists. This is useful for using file sources
purely for initialization."
newvalues(:true, :false)
aliasvalue(:yes, :true)
aliasvalue(:no, :false)
defaultto :true
end
newparam(:force, :boolean => true) do
desc "Force the file operation. Currently only used when replacing
directories with links."
newvalues(:true, :false)
defaultto false
end
newparam(:ignore) do
desc "A parameter which omits action on files matching
specified patterns during recursion. Uses Ruby's builtin globbing
engine, so shell metacharacters are fully supported, e.g. `[a-z]*`.
Matches that would descend into the directory structure are ignored,
e.g., `*/*`."
validate do |value|
unless value.is_a?(Array) or value.is_a?(String) or value == false
self.devfail "Ignore must be a string or an Array"
end
end
end
newparam(:links) do
desc "How to handle links during file actions. During file copying,
`follow` will copy the target file instead of the link, `manage`
will copy the link itself, and `ignore` will just pass it by.
When not copying, `manage` and `ignore` behave equivalently
(because you cannot really ignore links entirely during local recursion), and `follow` will manage the file to which the
link points."
newvalues(:follow, :manage)
defaultto :manage
end
newparam(:purge, :boolean => true) do
desc "Whether unmanaged files should be purged. If you have a filebucket
configured the purged files will be uploaded, but if you do not,
this will destroy data. Only use this option for generated
files unless you really know what you are doing. This option only
makes sense when recursively managing directories.
Note that when using `purge` with `source`, Puppet will purge any files
that are not on the remote system."
defaultto :false
newvalues(:true, :false)
end
newparam(:sourceselect) do
desc "Whether to copy all valid sources, or just the first one. This parameter
is only used in recursive copies; by default, the first valid source is the
only one used as a recursive source, but if this parameter is set to `all`,
then all valid sources will have all of their contents copied to the local host,
and for sources that have the same file, the source earlier in the list will
be used."
defaultto :first
newvalues(:first, :all)
end
# Autorequire any parent directories.
autorequire(:file) do
basedir = File.dirname(self[:path])
if basedir != self[:path]
basedir
else
nil
end
end
# Autorequire the owner and group of the file.
{:user => :owner, :group => :group}.each do |type, property|
autorequire(type) do
if @parameters.include?(property)
# The user/group property automatically converts to IDs
next unless should = @parameters[property].shouldorig
val = should[0]
if val.is_a?(Integer) or val =~ /^\d+$/
nil
else
val
end
end
end
end
CREATORS = [:content, :source, :target]
+ SOURCE_ONLY_CHECKSUMS = [:none, :ctime, :mtime]
validate do
- count = 0
+ creator_count = 0
CREATORS.each do |param|
- count += 1 if self.should(param)
+ creator_count += 1 if self.should(param)
end
- count += 1 if @parameters.include?(:source)
- self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if count > 1
+ creator_count += 1 if @parameters.include?(:source)
+ self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if creator_count > 1
self.fail "You cannot specify a remote recursion without a source" if !self[:source] and self[:recurse] == :remote
+ self.fail "You cannot specify source when using checksum 'none'" if self[:checksum] == :none && !self[:source].nil?
+
+ SOURCE_ONLY_CHECKSUMS.each do |checksum_type|
+ self.fail "You cannot specify content when using checksum '#{checksum_type}'" if self[:checksum] == checksum_type && !self[:content].nil?
+ end
+
self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" if !self[:recurse] and self[:recurselimit]
end
def self.[](path)
return nil unless path
super(path.gsub(/\/+/, '/').sub(/\/$/, ''))
end
- # List files, but only one level deep.
- def self.instances(base = "/")
- return [] unless FileTest.directory?(base)
-
- files = []
- Dir.entries(base).reject { |e|
- e == "." or e == ".."
- }.each do |name|
- path = File.join(base, name)
- if obj = self[path]
- obj[:audit] = :all
- files << obj
- else
- files << self.new(
- :name => path, :audit => :all
- )
- end
- end
- files
+ def self.instances(base = '/')
+ return self.new(:name => base, :recurse => true, :recurselimit => 1, :audit => :all).recurse_local.values
end
@depthfirst = false
# Determine the user to write files as.
def asuser
if self.should(:owner) and ! self.should(:owner).is_a?(Symbol)
writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) {
FileTest.writable?(File.dirname(self[:path]))
}
# If the parent directory is writeable, then we execute
# as the user in question. Otherwise we'll rely on
# the 'owner' property to do things.
asuser = self.should(:owner) if writeable
end
asuser
end
def bucket
return @bucket if @bucket
backup = self[:backup]
return nil unless backup
return nil if backup =~ /^\./
unless catalog or backup == "puppet"
fail "Can not find filebucket for backups without a catalog"
end
unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet"
fail "Could not find filebucket #{backup} specified in backup"
end
return default_bucket unless filebucket
@bucket = filebucket.bucket
@bucket
end
def default_bucket
Puppet::Type.type(:filebucket).mkdefaultbucket.bucket
end
# Does the file currently exist? Just checks for whether
# we have a stat
def exist?
stat ? true : false
end
# We have to do some extra finishing, to retrieve our bucket if
# there is one.
def finish
# Look up our bucket, if there is one
bucket
super
end
# Create any children via recursion or whatever.
def eval_generate
return [] unless self.recurse?
recurse
#recurse.reject do |resource|
# catalog.resource(:file, resource[:path])
#end.each do |child|
# catalog.add_resource child
# catalog.relationship_graph.add_edge self, child
#end
end
def flush
# We want to make sure we retrieve metadata anew on each transaction.
@parameters.each do |name, param|
param.flush if param.respond_to?(:flush)
end
@stat = nil
end
def initialize(hash)
# Used for caching clients
@clients = {}
super
# If they've specified a source, we get our 'should' values
# from it.
unless self[:ensure]
if self[:target]
self[:ensure] = :symlink
elsif self[:content]
self[:ensure] = :file
end
end
@stat = nil
end
# Configure discovered resources to be purged.
def mark_children_for_purging(children)
children.each do |name, child|
next if child[:source]
child[:ensure] = :absent
end
end
# Create a new file or directory object as a child to the current
# object.
def newchild(path)
full_path = File.join(self[:path], path)
# Add some new values to our original arguments -- these are the ones
# set at initialization. We specifically want to exclude any param
# values set by the :source property or any default values.
# LAK:NOTE This is kind of silly, because the whole point here is that
# the values set at initialization should live as long as the resource
# but values set by default or by :source should only live for the transaction
# or so. Unfortunately, we don't have a straightforward way to manage
# the different lifetimes of this data, so we kludge it like this.
# The right-side hash wins in the merge.
options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? }
# These should never be passed to our children.
[:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param|
options.delete(param) if options.include?(param)
end
self.class.new(options)
end
# Files handle paths specially, because they just lengthen their
# path names, rather than including the full parent's title each
# time.
def pathbuilder
# We specifically need to call the method here, so it looks
# up our parent in the catalog graph.
if parent = parent()
# We only need to behave specially when our parent is also
# a file
if parent.is_a?(self.class)
# Remove the parent file name
list = parent.pathbuilder
list.pop # remove the parent's path info
return list << self.ref
else
return super
end
else
return [self.ref]
end
end
# Should we be purging?
def purge?
@parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true")
end
# Recursively generate a list of file resources, which will
# be used to copy remote files, manage local files, and/or make links
# to map to another directory.
def recurse
children = {}
children = recurse_local if self[:recurse] != :remote
if self[:target]
recurse_link(children)
elsif self[:source]
recurse_remote(children)
end
# If we're purging resources, then delete any resource that isn't on the
# remote system.
mark_children_for_purging(children) if self.purge?
result = children.values.sort { |a, b| a[:path] <=> b[:path] }
remove_less_specific_files(result)
end
# This is to fix bug #2296, where two files recurse over the same
# set of files. It's a rare case, and when it does happen you're
# not likely to have many actual conflicts, which is good, because
# this is a pretty inefficient implementation.
def remove_less_specific_files(files)
mypath = self[:path].split(File::Separator)
other_paths = catalog.vertices.
select { |r| r.is_a?(self.class) and r[:path] != self[:path] }.
collect { |r| r[:path].split(File::Separator) }.
select { |p| p[0,mypath.length] == mypath }
return files if other_paths.empty?
files.reject { |file|
path = file[:path].split(File::Separator)
other_paths.any? { |p| path[0,p.length] == p }
}
end
# A simple method for determining whether we should be recursing.
def recurse?
return false unless @parameters.include?(:recurse)
val = @parameters[:recurse].value
!!(val and (val == true or val == :remote))
end
# Recurse the target of the link.
def recurse_link(children)
perform_recursion(self[:target]).each do |meta|
if meta.relative_path == "."
self[:ensure] = :directory
next
end
children[meta.relative_path] ||= newchild(meta.relative_path)
if meta.ftype == "directory"
children[meta.relative_path][:ensure] = :directory
else
children[meta.relative_path][:ensure] = :link
children[meta.relative_path][:target] = meta.full_path
end
end
children
end
# Recurse the file itself, returning a Metadata instance for every found file.
def recurse_local
result = perform_recursion(self[:path])
return {} unless result
result.inject({}) do |hash, meta|
next hash if meta.relative_path == "."
hash[meta.relative_path] = newchild(meta.relative_path)
hash
end
end
# Recurse against our remote file.
def recurse_remote(children)
sourceselect = self[:sourceselect]
total = self[:source].collect do |source|
next unless result = perform_recursion(source)
return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory"
result.each { |data| data.source = "#{source}/#{data.relative_path}" }
break result if result and ! result.empty? and sourceselect == :first
result
end.flatten
# This only happens if we have sourceselect == :all
unless sourceselect == :first
found = []
total.reject! do |data|
result = found.include?(data.relative_path)
found << data.relative_path unless found.include?(data.relative_path)
result
end
end
total.each do |meta|
if meta.relative_path == "."
parameter(:source).metadata = meta
next
end
children[meta.relative_path] ||= newchild(meta.relative_path)
children[meta.relative_path][:source] = meta.source
children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file"
children[meta.relative_path].parameter(:source).metadata = meta
end
children
end
def perform_recursion(path)
Puppet::FileServing::Metadata.search(
path,
:links => self[:links],
:recurse => (self[:recurse] == :remote ? true : self[:recurse]),
:recurselimit => self[:recurselimit],
:ignore => self[:ignore],
:checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none
)
end
# Remove any existing data. This is only used when dealing with
# links or directories.
def remove_existing(should)
return unless s = stat
self.fail "Could not back up; will not replace" unless perform_backup
unless should.to_s == "link"
return if s.ftype.to_s == should.to_s
end
case s.ftype
when "directory"
if self[:force] == :true
debug "Removing existing directory for replacement with #{should}"
FileUtils.rmtree(self[:path])
else
notice "Not removing directory; use 'force' to override"
end
when "link", "file"
debug "Removing existing #{s.ftype} for replacement with #{should}"
File.unlink(self[:path])
else
self.fail "Could not back up files of type #{s.ftype}"
end
expire
end
def retrieve
if source = parameter(:source)
source.copy_source_values
end
super
end
# Set the checksum, from another property. There are multiple
# properties that modify the contents of a file, and they need the
# ability to make sure that the checksum value is in sync.
def setchecksum(sum = nil)
if @parameters.include? :checksum
if sum
@parameters[:checksum].checksum = sum
else
# If they didn't pass in a sum, then tell checksum to
# figure it out.
currentvalue = @parameters[:checksum].retrieve
@parameters[:checksum].checksum = currentvalue
end
end
end
# Should this thing be a normal file? This is a relatively complex
# way of determining whether we're trying to create a normal file,
# and it's here so that the logic isn't visible in the content property.
def should_be_file?
return true if self[:ensure] == :file
# I.e., it's set to something like "directory"
return false if e = self[:ensure] and e != :present
# The user doesn't really care, apparently
if self[:ensure] == :present
return true unless s = stat
return(s.ftype == "file" ? true : false)
end
# If we've gotten here, then :ensure isn't set
return true if self[:content]
return true if stat and stat.ftype == "file"
false
end
# Stat our file. Depending on the value of the 'links' attribute, we
# use either 'stat' or 'lstat', and we expect the properties to use the
# resulting stat object accordingly (mostly by testing the 'ftype'
# value).
cached_attr(:stat) do
method = :stat
# Files are the only types that support links
if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy
method = :lstat
end
path = self[:path]
begin
File.send(method, self[:path])
rescue Errno::ENOENT => error
return nil
rescue Errno::EACCES => error
warning "Could not stat; permission denied"
return nil
end
end
# We have to hack this just a little bit, because otherwise we'll get
# an error when the target and the contents are created as properties on
# the far side.
def to_trans(retrieve = true)
obj = super
obj.delete(:target) if obj[:target] == :notlink
obj
end
# Write out the file. Requires the property name for logging.
# Write will be done by the content property, along with checksum computation
def write(property)
remove_existing(:file)
use_temporary_file = write_temporary_file?
if use_temporary_file
path = "#{self[:path]}.puppettmp_#{rand(10000)}"
path = "#{self[:path]}.puppettmp_#{rand(10000)}" while File.exists?(path) or File.symlink?(path)
else
path = self[:path]
end
mode = self.should(:mode) # might be nil
umask = mode ? 000 : 022
mode_int = mode ? mode.to_i(8) : nil
content_checksum = Puppet::Util.withumask(umask) { File.open(path, 'w', mode_int ) { |f| write_content(f) } }
# And put our new file in place
if use_temporary_file # This is only not true when our file is empty.
begin
fail_if_checksum_is_wrong(path, content_checksum) if validate_checksum?
File.rename(path, self[:path])
rescue => detail
fail "Could not rename temporary file #{path} to #{self[:path]}: #{detail}"
ensure
# Make sure the created file gets removed
File.unlink(path) if FileTest.exists?(path)
end
end
# make sure all of the modes are actually correct
property_fix
end
private
# Should we validate the checksum of the file we're writing?
def validate_checksum?
self[:checksum] !~ /time/
end
# Make sure the file we wrote out is what we think it is.
def fail_if_checksum_is_wrong(path, content_checksum)
newsum = parameter(:checksum).sum_file(path)
return if [:absent, nil, content_checksum].include?(newsum)
self.fail "File written to disk did not match checksum; discarding changes (#{content_checksum} vs #{newsum})"
end
# write the current content. Note that if there is no content property
# simply opening the file with 'w' as done in write is enough to truncate
# or write an empty length file.
def write_content(file)
(content = property(:content)) && content.write(file)
end
private
def write_temporary_file?
# unfortunately we don't know the source file size before fetching it
# so let's assume the file won't be empty
(c = property(:content) and c.length) || (s = @parameters[:source] and 1)
end
# There are some cases where all of the work does not get done on
# file creation/modification, so we have to do some extra checking.
def property_fix
properties.each do |thing|
next unless [:mode, :owner, :group, :seluser, :selrole, :seltype, :selrange].include?(thing.name)
# Make sure we get a new stat objct
expire
currentvalue = thing.retrieve
- thing.sync unless thing.insync?(currentvalue)
+ thing.sync unless thing.safe_insync?(currentvalue)
end
end
end
# We put all of the properties in separate files, because there are so many
# of them. The order these are loaded is important, because it determines
# the order they are in the property lit.
require 'puppet/type/file/checksum'
require 'puppet/type/file/content' # can create the file
require 'puppet/type/file/source' # can create the file
require 'puppet/type/file/target' # creates a different type of file
require 'puppet/type/file/ensure' # can create the file
require 'puppet/type/file/owner'
require 'puppet/type/file/group'
require 'puppet/type/file/mode'
require 'puppet/type/file/type'
require 'puppet/type/file/selcontext' # SELinux file context
require 'puppet/type/file/ctime'
require 'puppet/type/file/mtime'
diff --git a/lib/puppet/type/file/checksum.rb b/lib/puppet/type/file/checksum.rb
index 732460738..5586b1383 100755
--- a/lib/puppet/type/file/checksum.rb
+++ b/lib/puppet/type/file/checksum.rb
@@ -1,33 +1,33 @@
require 'puppet/util/checksums'
# Specify which checksum algorithm to use when checksumming
# files.
Puppet::Type.type(:file).newparam(:checksum) do
include Puppet::Util::Checksums
desc "The checksum type to use when checksumming a file.
The default checksum parameter, if checksums are enabled, is md5."
- newvalues "md5", "md5lite", "timestamp", "mtime", "time", "none"
+ newvalues "md5", "md5lite", "mtime", "ctime", "none"
defaultto :md5
def sum(content)
type = value || :md5 # because this might be called before defaults are set
"{#{type}}" + send(type, content)
end
def sum_file(path)
type = value || :md5 # because this might be called before defaults are set
method = type.to_s + "_file"
"{#{type}}" + send(method, path).to_s
end
def sum_stream(&block)
type = value || :md5 # same comment as above
method = type.to_s + "_stream"
checksum = send(method, &block)
"{#{type}}#{checksum}"
end
end
diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb
index b8f30a9c7..cf924f371 100755
--- a/lib/puppet/type/file/content.rb
+++ b/lib/puppet/type/file/content.rb
@@ -1,216 +1,217 @@
require 'net/http'
require 'uri'
require 'tempfile'
require 'puppet/util/checksums'
require 'puppet/network/http/api/v1'
require 'puppet/network/http/compression'
module Puppet
Puppet::Type.type(:file).newproperty(:content) do
include Puppet::Util::Diff
include Puppet::Util::Checksums
include Puppet::Network::HTTP::API::V1
include Puppet::Network::HTTP::Compression.module
attr_reader :actual_content
desc "Specify the contents of a file as a string. Newlines, tabs, and
spaces can be specified using the escaped syntax (e.g., \\n for a newline). The primary purpose of this parameter is to provide a
kind of limited templating::
define resolve(nameserver1, nameserver2, domain, search) {
$str = \"search $search
domain $domain
nameserver $nameserver1
nameserver $nameserver2
\"
file { \"/etc/resolv.conf\":
content => $str
}
}
This attribute is especially useful when used with
`PuppetTemplating templating`:trac:."
# Store a checksum as the value, rather than the actual content.
# Simplifies everything.
munge do |value|
if value == :absent
value
elsif checksum?(value)
# XXX This is potentially dangerous because it means users can't write a file whose
# entire contents are a plain checksum
value
else
@actual_content = value
resource.parameter(:checksum).sum(value)
end
end
# Checksums need to invert how changes are printed.
def change_to_s(currentvalue, newvalue)
# Our "new" checksum value is provided by the source.
if source = resource.parameter(:source) and tmp = source.checksum
newvalue = tmp
end
if currentvalue == :absent
return "defined content as '#{newvalue}'"
elsif newvalue == :absent
return "undefined content from '#{currentvalue}'"
else
return "content changed '#{currentvalue}' to '#{newvalue}'"
end
end
def checksum_type
if source = resource.parameter(:source)
result = source.checksum
else checksum = resource.parameter(:checksum)
result = resource[:checksum]
end
if result =~ /^\{(\w+)\}.+/
return $1.to_sym
else
return result
end
end
def length
(actual_content and actual_content.length) || 0
end
def content
self.should
end
# Override this method to provide diffs if asked for.
# Also, fix #872: when content is used, and replace is true, the file
# should be insync when it exists
def insync?(is)
if resource.should_be_file?
return false if is == :absent
else
return true
end
return true if ! @resource.replace?
- return true unless self.should
result = super
if ! result and Puppet[:show_diff]
write_temporarily do |path|
print diff(@resource[:path], path)
end
end
result
end
def retrieve
return :absent unless stat = @resource.stat
ftype = stat.ftype
# Don't even try to manage the content on directories or links
return nil if ["directory","link"].include?(ftype)
begin
resource.parameter(:checksum).sum_file(resource[:path])
rescue => detail
raise Puppet::Error, "Could not read #{ftype} #{@resource.title}: #{detail}"
end
end
# Make sure we're also managing the checksum property.
def should=(value)
@resource.newattr(:checksum) unless @resource.parameter(:checksum)
super
end
# Just write our content out to disk.
def sync
return_event = @resource.stat ? :file_changed : :file_created
# We're safe not testing for the 'source' if there's no 'should'
# because we wouldn't have gotten this far if there weren't at least
# one valid value somewhere.
@resource.write(:content)
return_event
end
def write_temporarily
tempfile = Tempfile.new("puppet-file")
tempfile.open
write(tempfile)
tempfile.close
yield tempfile.path
tempfile.delete
end
def write(file)
resource.parameter(:checksum).sum_stream { |sum|
each_chunk_from(actual_content || resource.parameter(:source)) { |chunk|
sum << chunk
file.print chunk
}
}
end
def self.standalone?
Puppet.settings[:name] == "apply"
end
def each_chunk_from(source_or_content)
if source_or_content.is_a?(String)
yield source_or_content
+ elsif source_or_content.nil? && resource.parameter(:ensure) && [:present, :file].include?(resource.parameter(:ensure).value)
+ yield ''
elsif source_or_content.nil?
yield read_file_from_filebucket
elsif self.class.standalone?
yield source_or_content.content
elsif source_or_content.local?
chunk_file_from_disk(source_or_content) { |chunk| yield chunk }
else
chunk_file_from_source(source_or_content) { |chunk| yield chunk }
end
end
private
def chunk_file_from_disk(source_or_content)
File.open(source_or_content.full_path, "r") do |src|
while chunk = src.read(8192)
yield chunk
end
end
end
def chunk_file_from_source(source_or_content)
request = Puppet::Indirector::Request.new(:file_content, :find, source_or_content.full_path.sub(/^\//,''))
connection = Puppet::Network::HttpPool.http_instance(source_or_content.server, source_or_content.port)
connection.request_get(indirection2uri(request), add_accept_encoding({"Accept" => "raw"})) do |response|
case response.code
when "404"; nil
when /^2/; uncompress(response) { |uncompressor| response.read_body { |chunk| yield uncompressor.uncompress(chunk) } }
else
# Raise the http error if we didn't get a 'success' of some kind.
message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}"
raise Net::HTTPError.new(message, response)
end
end
end
def read_file_from_filebucket
raise "Could not get filebucket from file" unless dipper = resource.bucket
sum = should.sub(/\{\w+\}/, '')
dipper.getfile(sum)
rescue => detail
fail "Could not retrieve content for #{should} from filebucket: #{detail}"
end
end
end
diff --git a/lib/puppet/type/file/owner.rb b/lib/puppet/type/file/owner.rb
index d473da20e..483cc7fce 100755
--- a/lib/puppet/type/file/owner.rb
+++ b/lib/puppet/type/file/owner.rb
@@ -1,52 +1,52 @@
module Puppet
Puppet::Type.type(:file).newproperty(:owner) do
desc "To whom the file should belong. Argument can be user name or
user ID."
@event = :file_changed
def insync?(current)
- provider.insync?(current, @should)
+ provider.is_owner_insync?(current, @should)
end
# We want to print names, not numbers
def is_to_s(currentvalue)
provider.id2name(currentvalue) || currentvalue
end
def should_to_s(newvalue = @should)
case newvalue
when Symbol
newvalue.to_s
when Integer
provider.id2name(newvalue) || newvalue
when String
newvalue
else
raise Puppet::DevError, "Invalid uid type #{newvalue.class}(#{newvalue})"
end
end
def retrieve
if self.should
@should = @should.collect do |val|
unless val.is_a?(Integer)
if tmp = provider.validuser?(val)
val = tmp
else
raise "Could not find user #{val}"
end
else
val
end
end
end
provider.retrieve(@resource)
end
def sync
provider.sync(resource[:path], resource[:links], @should)
end
end
end
diff --git a/lib/puppet/type/file/source.rb b/lib/puppet/type/file/source.rb
index 7d03de2b0..bc464e1c3 100755
--- a/lib/puppet/type/file/source.rb
+++ b/lib/puppet/type/file/source.rb
@@ -1,197 +1,196 @@
require 'puppet/file_serving/content'
require 'puppet/file_serving/metadata'
module Puppet
# Copy files from a local or remote source. This state *only* does any work
# when the remote file is an actual file; in that case, this state copies
# the file down. If the remote file is a dir or a link or whatever, then
# this state, during retrieval, modifies the appropriate other states
# so that things get taken care of appropriately.
Puppet::Type.type(:file).newparam(:source) do
include Puppet::Util::Diff
attr_accessor :source, :local
desc "Copy a file over the current file. Uses `checksum` to
determine when a file should be copied. Valid values are either
fully qualified paths to files, or URIs. Currently supported URI
types are *puppet* and *file*.
This is one of the primary mechanisms for getting content into
applications that Puppet does not directly support and is very
useful for those configuration files that don't change much across
sytems. For instance:
class sendmail {
file { \"/etc/mail/sendmail.cf\":
source => \"puppet://server/modules/module_name/sendmail.cf\"
}
}
You can also leave out the server name, in which case `puppet agent`
will fill in the name of its configuration server and `puppet apply`
will use the local filesystem. This makes it easy to use the same
configuration in both local and centralized forms.
Currently, only the `puppet` scheme is supported for source
URL's. Puppet will connect to the file server running on
`server` to retrieve the contents of the file. If the
`server` part is empty, the behavior of the command-line
interpreter (`puppet apply`) and the client demon (`puppet agent`) differs
slightly: `apply` will look such a file up on the module path
on the local host, whereas `agent` will connect to the
puppet server that it received the manifest from.
See the [fileserver configuration documentation](http://projects.puppetlabs.com/projects/puppet/wiki/File_Serving_Configuration) for information on how to configure
and use file services within Puppet.
If you specify multiple file sources for a file, then the first
source that exists will be used. This allows you to specify
what amount to search paths for files:
file { \"/path/to/my/file\":
source => [
\"/modules/nfs/files/file.$host\",
\"/modules/nfs/files/file.$operatingsystem\",
\"/modules/nfs/files/file\"
]
}
This will use the first found file as the source.
You cannot currently copy links using this mechanism; set `links`
to `follow` if any remote sources are links.
"
validate do |sources|
sources = [sources] unless sources.is_a?(Array)
sources.each do |source|
begin
uri = URI.parse(URI.escape(source))
rescue => detail
self.fail "Could not understand source #{source}: #{detail}"
end
self.fail "Cannot use URLs of type '#{uri.scheme}' as source for fileserving" unless uri.scheme.nil? or %w{file puppet}.include?(uri.scheme)
end
end
munge do |sources|
sources = [sources] unless sources.is_a?(Array)
sources.collect { |source| source.sub(/\/$/, '') }
end
def change_to_s(currentvalue, newvalue)
# newvalue = "{md5}#{@metadata.checksum}"
if @resource.property(:ensure).retrieve == :absent
return "creating from source #{metadata.source} with contents #{metadata.checksum}"
else
return "replacing from source #{metadata.source} with contents #{metadata.checksum}"
end
end
def checksum
metadata && metadata.checksum
end
# Look up (if necessary) and return remote content.
cached_attr(:content) do
raise Puppet::DevError, "No source for content was stored with the metadata" unless metadata.source
unless tmp = Puppet::FileServing::Content.find(metadata.source)
fail "Could not find any content at %s" % metadata.source
end
tmp.content
end
# Copy the values from the source to the resource. Yay.
def copy_source_values
devfail "Somehow got asked to copy source values without any metadata" unless metadata
# Take each of the stats and set them as states on the local file
# if a value has not already been provided.
[:owner, :mode, :group, :checksum].each do |metadata_method|
param_name = (metadata_method == :checksum) ? :content : metadata_method
next if metadata_method == :owner and !Puppet.features.root?
next if metadata_method == :checksum and metadata.ftype == "directory"
if resource[param_name].nil? or resource[param_name] == :absent
resource[param_name] = metadata.send(metadata_method)
end
end
if resource[:ensure] == :absent
# We know all we need to
elsif metadata.ftype != "link"
resource[:ensure] = metadata.ftype
elsif @resource[:links] == :follow
resource[:ensure] = :present
else
resource[:ensure] = "link"
resource[:target] = metadata.destination
end
end
def pinparams
[:mode, :type, :owner, :group, :content]
end
def found?
! (metadata.nil? or metadata.ftype.nil?)
end
# Provide, and retrieve if necessary, the metadata for this file. Fail
# if we can't find data about this host, and fail if there are any
# problems in our query.
cached_attr(:metadata) do
return nil unless value
result = nil
value.each do |source|
begin
if data = Puppet::FileServing::Metadata.find(source)
result = data
result.source = source
break
end
rescue => detail
fail detail, "Could not retrieve file metadata for #{source}: #{detail}"
end
end
fail "Could not retrieve information from source(s) #{value.join(", ")}" unless result
result
end
# Make sure we're also checking the checksum
def value=(value)
super
checks = (pinparams + [:ensure])
checks.delete(:checksum)
resource[:audit] = checks
- resource[:checksum] = :md5 unless resource.property(:checksum)
end
def local?
found? and uri and (uri.scheme || "file") == "file"
end
def full_path
URI.unescape(uri.path) if found? and uri
end
def server
(uri and uri.host) or Puppet.settings[:server]
end
def port
(uri and uri.port) or Puppet.settings[:masterport]
end
private
def uri
@uri ||= URI.parse(URI.escape(metadata.source))
end
end
end
diff --git a/lib/puppet/type/file/target.rb b/lib/puppet/type/file/target.rb
index 9e7229dda..b9fe9213b 100644
--- a/lib/puppet/type/file/target.rb
+++ b/lib/puppet/type/file/target.rb
@@ -1,74 +1,74 @@
module Puppet
Puppet::Type.type(:file).newproperty(:target) do
desc "The target for creating a link. Currently, symlinks are the
only type supported."
newvalue(:notlink) do
# We do nothing if the value is absent
return :nochange
end
# Anything else, basically
newvalue(/./) do
@resource[:ensure] = :link if ! @resource.should(:ensure)
# Only call mklink if ensure didn't call us in the first place.
currentensure = @resource.property(:ensure).retrieve
- mklink if @resource.property(:ensure).insync?(currentensure)
+ mklink if @resource.property(:ensure).safe_insync?(currentensure)
end
# Create our link.
def mklink
raise Puppet::Error, "Cannot symlink on Microsoft Windows" if Puppet.features.microsoft_windows?
target = self.should
# Clean up any existing objects. The argument is just for logging,
# it doesn't determine what's removed.
@resource.remove_existing(target)
raise Puppet::Error, "Could not remove existing file" if FileTest.exists?(@resource[:path])
Dir.chdir(File.dirname(@resource[:path])) do
Puppet::Util::SUIDManager.asuser(@resource.asuser) do
mode = @resource.should(:mode)
if mode
Puppet::Util.withumask(000) do
File.symlink(target, @resource[:path])
end
else
File.symlink(target, @resource[:path])
end
end
@resource.send(:property_fix)
:link_created
end
end
def insync?(currentvalue)
if [:nochange, :notlink].include?(self.should) or @resource.recurse?
return true
elsif ! @resource.replace? and File.exists?(@resource[:path])
return true
else
return super(currentvalue)
end
end
def retrieve
if stat = @resource.stat
if stat.ftype == "link"
return File.readlink(@resource[:path])
else
return :notlink
end
else
return :absent
end
end
end
end
diff --git a/lib/puppet/type/host.rb b/lib/puppet/type/host.rb
index 8ab750459..2666e50ae 100755
--- a/lib/puppet/type/host.rb
+++ b/lib/puppet/type/host.rb
@@ -1,98 +1,98 @@
module Puppet
newtype(:host) do
ensurable
newproperty(:ip) do
desc "The host's IP address, IPv4 or IPv6."
validate do |value|
unless value =~ /((([0-9a-fA-F]+:){7}[0-9a-fA-F]+)|(([0-9a-fA-F]+:)*[0-9a-fA-F]+)?::(([0-9a-fA-F]+:)*[0-9a-fA-F]+)?)|((25[0-5]|2[0-4][\d]|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})/
raise Puppet::Error, "Invalid IP address"
end
end
end
newproperty(:host_aliases) do
desc "Any aliases the host might have. Multiple values must be
specified as an array."
def insync?(is)
is == @should
end
def is_to_s(currentvalue = @is)
currentvalue = [currentvalue] unless currentvalue.is_a? Array
currentvalue.join(" ")
end
def retrieve
is = super
case is
when String
is = is.split(/\s*,\s*/)
when Symbol
is = [is]
when Array
# nothing
else
raise Puppet::DevError, "Invalid @is type #{is.class}"
end
is
end
# We actually want to return the whole array here, not just the first
# value.
def should
if defined?(@should)
if @should == [:absent]
return :absent
else
return @should
end
else
return nil
end
end
def should_to_s(newvalue = @should)
newvalue.join(" ")
end
validate do |value|
raise Puppet::Error, "Host aliases cannot include whitespace" if value =~ /\s/
end
end
newproperty(:target) do
desc "The file in which to store service information. Only used by
- those providers that write to disk."
+ those providers that write to disk. On most systems this defaults to `/etc/hosts`."
defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile)
@resource.class.defaultprovider.default_target
else
nil
end
}
end
newparam(:name) do
desc "The host name."
isnamevar
validate do |value|
# LAK:NOTE See http://snurl.com/21zf8 [groups_google_com]
x = value.split('.').each do |hostpart|
unless hostpart =~ /^([\d\w]+|[\d\w][\d\w\-]+[\d\w])$/
raise Puppet::Error, "Invalid host name"
end
end
end
end
@doc = "Installs and manages host entries. For most systems, these
entries will just be in `/etc/hosts`, but some systems (notably OS X)
will have different solutions."
end
end
diff --git a/lib/puppet/type/mount.rb b/lib/puppet/type/mount.rb
index d048c90f1..da9a70bdf 100755
--- a/lib/puppet/type/mount.rb
+++ b/lib/puppet/type/mount.rb
@@ -1,224 +1,224 @@
module Puppet
# We want the mount to refresh when it changes.
newtype(:mount, :self_refresh => true) do
@doc = "Manages mounted filesystems, including putting mount
information into the mount table. The actual behavior depends
on the value of the 'ensure' parameter.
Note that if a `mount` receives an event from another resource,
it will try to remount the filesystems if `ensure` is set to `mounted`."
feature :refreshable, "The provider can remount the filesystem.",
:methods => [:remount]
# Use the normal parent class, because we actually want to
# call code when sync is called.
newproperty(:ensure) do
desc "Control what to do with this mount. Set this attribute to
`umounted` to make sure the filesystem is in the filesystem table
but not mounted (if the filesystem is currently mounted, it will be unmounted). Set it to `absent` to unmount (if necessary) and remove
the filesystem from the fstab. Set to `mounted` to add it to the
fstab and mount it. Set to `present` to add to fstab but not change
mount/unmount status"
newvalue(:defined) do
provider.create
return :mount_created
end
aliasvalue :present, :defined
newvalue(:unmounted) do
if provider.mounted?
syncothers
provider.unmount
return :mount_unmounted
else
provider.create
return :mount_created
end
end
newvalue(:absent, :event => :mount_deleted) do
provider.unmount if provider.mounted?
provider.destroy
end
newvalue(:mounted, :event => :mount_mounted) do
# Create the mount point if it does not already exist.
current_value = self.retrieve
provider.create if current_value.nil? or current_value == :absent
syncothers
# The fs can be already mounted if it was absent but mounted
provider.mount unless provider.mounted?
end
def insync?(is)
if should == :defined and is != :absent
true
else
super
end
end
def retrieve
# We need to special case :mounted; if we're absent, we still
# want
curval = super()
if curval == :absent
return :absent
elsif provider.mounted?
return :mounted
else
return :unmounted
end
end
def syncothers
# We have to flush any changes to disk.
currentvalues = @resource.retrieve_resource
# Determine if there are any out-of-sync properties.
oos = @resource.send(:properties).find_all do |prop|
unless currentvalues.include?(prop)
raise Puppet::DevError, "Parent has property %s but it doesn't appear in the current values", [prop.name]
end
if prop.name == :ensure
false
else
- ! prop.insync?(currentvalues[prop])
+ ! prop.safe_insync?(currentvalues[prop])
end
end.each { |prop| prop.sync }.length
@resource.flush if oos > 0
end
end
newproperty(:device) do
desc "The device providing the mount. This can be whatever
device is supporting by the mount, including network
devices or devices specified by UUID rather than device
path, depending on the operating system."
end
# Solaris specifies two devices, not just one.
newproperty(:blockdevice) do
desc "The device to fsck. This is property is only valid
on Solaris, and in most cases will default to the correct
value."
# Default to the device but with "dsk" replaced with "rdsk".
defaultto do
if Facter["operatingsystem"].value == "Solaris"
device = @resource.value(:device)
if device =~ %r{/dsk/}
device.sub(%r{/dsk/}, "/rdsk/")
else
nil
end
else
nil
end
end
end
newproperty(:fstype) do
desc "The mount type. Valid values depend on the
operating system. This is a required option."
end
newproperty(:options) do
desc "Mount options for the mounts, as they would
appear in the fstab."
end
newproperty(:pass) do
desc "The pass in which the mount is checked."
defaultto {
0 if @resource.managed?
}
end
newproperty(:atboot) do
desc "Whether to mount the mount at boot. Not all platforms
support this."
end
newproperty(:dump) do
desc "Whether to dump the mount. Not all platform support this.
Valid values are `1` or `0`. or `2` on FreeBSD, Default is `0`."
if Facter["operatingsystem"].value == "FreeBSD"
newvalue(%r{(0|1|2)})
else
newvalue(%r{(0|1)})
end
newvalue(%r{(0|1)})
defaultto {
0 if @resource.managed?
}
end
newproperty(:target) do
desc "The file in which to store the mount table. Only used by
those providers that write to disk."
defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile)
@resource.class.defaultprovider.default_target
else
nil
end
}
end
newparam(:name) do
desc "The mount path for the mount."
isnamevar
end
newparam(:path) do
desc "The deprecated name for the mount point. Please use `name` now."
def value=(value)
warning "'path' is deprecated for mounts. Please use 'name'."
@resource[:name] = value
super
end
end
newparam(:remounts) do
desc "Whether the mount can be remounted `mount -o remount`. If
this is false, then the filesystem will be unmounted and remounted
manually, which is prone to failure."
newvalues(:true, :false)
defaultto do
case Facter.value(:operatingsystem)
- when "FreeBSD", "Darwin"
+ when "FreeBSD", "Darwin", "AIX"
false
else
true
end
end
end
def refresh
# Only remount if we're supposed to be mounted.
provider.remount if self.should(:fstype) != "swap" and provider.mounted?
end
def value(name)
name = symbolize(name)
ret = nil
if property = @parameters[name]
return property.value
end
end
end
end
diff --git a/lib/puppet/type/package.rb b/lib/puppet/type/package.rb
index 51a866332..d73d90dff 100644
--- a/lib/puppet/type/package.rb
+++ b/lib/puppet/type/package.rb
@@ -1,321 +1,319 @@
# Define the different packaging systems. Each package system is implemented
# in a module, which then gets used to individually extend each package object.
# This allows packages to exist on the same machine using different packaging
# systems.
module Puppet
newtype(:package) do
@doc = "Manage packages. There is a basic dichotomy in package
support right now: Some package types (e.g., yum and apt) can
retrieve their own package files, while others (e.g., rpm and sun) cannot. For those package formats that cannot retrieve
their own files, you can use the `source` parameter to point to
the correct file.
Puppet will automatically guess the packaging format that you are
using based on the platform you are on, but you can override it
using the `provider` parameter; each provider defines what it
requires in order to function, and you must meet those requirements
to use a given provider."
feature :installable, "The provider can install packages.",
:methods => [:install]
feature :uninstallable, "The provider can uninstall packages.",
:methods => [:uninstall]
feature :upgradeable, "The provider can upgrade to the latest version of a
package. This feature is used by specifying `latest` as the
desired value for the package.",
:methods => [:update, :latest]
feature :purgeable, "The provider can purge packages. This generally means
that all traces of the package are removed, including
existing configuration files. This feature is thus destructive
and should be used with the utmost care.",
:methods => [:purge]
feature :versionable, "The provider is capable of interrogating the
package database for installed version(s), and can select
which out of a set of available versions of a package to
install if asked."
feature :holdable, "The provider is capable of placing packages on hold
such that they are not automatically upgraded as a result of
other package dependencies unless explicit action is taken by
a user or another package. Held is considered a superset of
installed.",
:methods => [:hold]
ensurable do
desc "What state the package should be in.
*latest* only makes sense for those packaging formats that can
retrieve new packages on their own and will throw an error on
those that cannot. For those packaging systems that allow you
to specify package versions, specify them here. Similarly,
*purged* is only useful for packaging systems that support
the notion of managing configuration files separately from
'normal' system files."
attr_accessor :latest
newvalue(:present, :event => :package_installed) do
provider.install
end
newvalue(:absent, :event => :package_removed) do
provider.uninstall
end
newvalue(:purged, :event => :package_purged, :required_features => :purgeable) do
provider.purge
end
newvalue(:held, :event => :package_held, :required_features => :holdable) do
provider.hold
end
# Alias the 'present' value.
aliasvalue(:installed, :present)
newvalue(:latest, :required_features => :upgradeable) do
# Because yum always exits with a 0 exit code, there's a retrieve
# in the "install" method. So, check the current state now,
# to compare against later.
current = self.retrieve
begin
provider.update
rescue => detail
self.fail "Could not update: #{detail}"
end
if current == :absent
:package_installed
else
:package_changed
end
end
newvalue(/./, :required_features => :versionable) do
begin
provider.install
rescue => detail
self.fail "Could not update: #{detail}"
end
if self.retrieve == :absent
:package_installed
else
:package_changed
end
end
defaultto :installed
# Override the parent method, because we've got all kinds of
# funky definitions of 'in sync'.
def insync?(is)
- @should ||= []
-
@latest ||= nil
@lateststamp ||= (Time.now.to_i - 1000)
# Iterate across all of the should values, and see how they
# turn out.
@should.each { |should|
case should
when :present
return true unless [:absent, :purged, :held].include?(is)
when :latest
# Short-circuit packages that are not present
return false if is == :absent or is == :purged
# Don't run 'latest' more than about every 5 minutes
if @latest and ((Time.now.to_i - @lateststamp) / 60) < 5
#self.debug "Skipping latest check"
else
begin
@latest = provider.latest
@lateststamp = Time.now.to_i
rescue => detail
error = Puppet::Error.new("Could not get latest version: #{detail}")
error.set_backtrace(detail.backtrace)
raise error
end
end
case is
when @latest
return true
when :present
# This will only happen on retarded packaging systems
# that can't query versions.
return true
else
self.debug "#{@resource.name} #{is.inspect} is installed, latest is #{@latest.inspect}"
end
when :absent
return true if is == :absent or is == :purged
when :purged
return true if is == :purged
when is
return true
end
}
false
end
# This retrieves the current state. LAK: I think this method is unused.
def retrieve
provider.properties[:ensure]
end
# Provide a bit more information when logging upgrades.
def should_to_s(newvalue = @should)
if @latest
@latest.to_s
else
super(newvalue)
end
end
end
newparam(:name) do
desc "The package name. This is the name that the packaging
system uses internally, which is sometimes (especially on Solaris)
a name that is basically useless to humans. If you want to
abstract package installation, then you can use aliases to provide
a common name to packages:
# In the 'openssl' class
$ssl = $operatingsystem ? {
solaris => SMCossl,
default => openssl
}
# It is not an error to set an alias to the same value as the
# object name.
package { $ssl:
ensure => installed,
alias => openssl
}
. etc. .
$ssh = $operatingsystem ? {
solaris => SMCossh,
default => openssh
}
# Use the alias to specify a dependency, rather than
# having another selector to figure it out again.
package { $ssh:
ensure => installed,
alias => openssh,
require => Package[openssl]
}
"
isnamevar
end
newparam(:source) do
desc "Where to find the actual package. This must be a local file
(or on a network file system) or a URL that your specific
packaging type understands; Puppet will not retrieve files for you."
end
newparam(:instance) do
desc "A read-only parameter set by the package."
end
newparam(:status) do
desc "A read-only parameter set by the package."
end
newparam(:type) do
desc "Deprecated form of `provider`."
munge do |value|
warning "'type' is deprecated; use 'provider' instead"
@resource[:provider] = value
@resource[:provider]
end
end
newparam(:adminfile) do
desc "A file containing package defaults for installing packages.
This is currently only used on Solaris. The value will be
validated according to system rules, which in the case of
Solaris means that it should either be a fully qualified path
or it should be in `/var/sadm/install/admin`."
end
newparam(:responsefile) do
desc "A file containing any necessary answers to questions asked by
the package. This is currently used on Solaris and Debian. The
value will be validated according to system rules, but it should
generally be a fully qualified path."
end
newparam(:configfiles) do
desc "Whether configfiles should be kept or replaced. Most packages
types do not support this parameter."
defaultto :keep
newvalues(:keep, :replace)
end
newparam(:category) do
desc "A read-only parameter set by the package."
end
newparam(:platform) do
desc "A read-only parameter set by the package."
end
newparam(:root) do
desc "A read-only parameter set by the package."
end
newparam(:vendor) do
desc "A read-only parameter set by the package."
end
newparam(:description) do
desc "A read-only parameter set by the package."
end
newparam(:allowcdrom) do
desc "Tells apt to allow cdrom sources in the sources.list file.
Normally apt will bail if you try this."
newvalues(:true, :false)
end
newparam(:flavor) do
desc "Newer versions of OpenBSD support 'flavors', which are
further specifications for which type of package you want."
end
autorequire(:file) do
autos = []
[:responsefile, :adminfile].each { |param|
if val = self[param]
autos << val
end
}
if source = self[:source]
if source =~ /^#{File::SEPARATOR}/
autos << source
end
end
autos
end
# This only exists for testing.
def clear
if obj = @parameters[:ensure]
obj.latest = nil
end
end
# The 'query' method returns a hash of info if the package
# exists and returns nil if it does not.
def exists?
@provider.get(:ensure) != :absent
end
end
end
diff --git a/lib/puppet/type/service.rb b/lib/puppet/type/service.rb
index c00f02789..0d09c3d5d 100644
--- a/lib/puppet/type/service.rb
+++ b/lib/puppet/type/service.rb
@@ -1,185 +1,191 @@
# This is our main way of managing processes right now.
#
# a service is distinct from a process in that services
# can only be managed through the interface of an init script
# which is why they have a search path for initscripts and such
module Puppet
newtype(:service) do
@doc = "Manage running services. Service support unfortunately varies
widely by platform -- some platforms have very little if any
concept of a running service, and some have a very codified and
powerful concept. Puppet's service support will generally be able
to make up for any inherent shortcomings (e.g., if there is no
'status' command, then Puppet will look in the process table for a
command matching the service name), but the more information you
can provide the better behaviour you will get. Or, you can just
use a platform that has very good service support.
Note that if a `service` receives an event from another resource,
the service will get restarted. The actual command to restart the
service depends on the platform. You can provide a special command
for restarting with the `restart` attribute."
feature :refreshable, "The provider can restart the service.",
:methods => [:restart]
feature :enableable, "The provider can enable and disable the service",
:methods => [:disable, :enable, :enabled?]
feature :controllable, "The provider uses a control variable."
newproperty(:enable, :required_features => :enableable) do
desc "Whether a service should be enabled to start at boot.
This property behaves quite differently depending on the platform;
wherever possible, it relies on local tools to enable or disable
a given service."
newvalue(:true, :event => :service_enabled) do
provider.enable
end
newvalue(:false, :event => :service_disabled) do
provider.disable
end
def retrieve
provider.enabled?
end
end
# Handle whether the service should actually be running right now.
newproperty(:ensure) do
desc "Whether a service should be running."
newvalue(:stopped, :event => :service_stopped) do
provider.stop
end
newvalue(:running, :event => :service_started) do
provider.start
end
aliasvalue(:false, :stopped)
aliasvalue(:true, :running)
def retrieve
provider.status
end
def sync
event = super()
if property = @resource.property(:enable)
val = property.retrieve
- property.sync unless property.insync?(val)
+ property.sync unless property.safe_insync?(val)
end
event
end
end
newparam(:binary) do
desc "The path to the daemon. This is only used for
systems that do not support init scripts. This binary will be
used to start the service if no `start` parameter is
provided."
end
newparam(:hasstatus) do
desc "Declare the the service's init script has a
functional status command. Based on testing, it was found
that a large number of init scripts on different platforms do
not support any kind of status command; thus, you must specify
manually whether the service you are running has such a
command (or you can specify a specific command using the
`status` parameter).
If you do not specify anything, then the service name will be
looked for in the process table."
newvalues(:true, :false)
end
newparam(:name) do
desc "The name of the service to run. This name is used to find
the service in whatever service subsystem it is in."
isnamevar
end
newparam(:path) do
desc "The search path for finding init scripts. Multiple values should
be separated by colons or provided as an array."
munge do |value|
value = [value] unless value.is_a?(Array)
# LAK:NOTE See http://snurl.com/21zf8 [groups_google_com]
# It affects stand-alone blocks, too.
paths = value.flatten.collect { |p| x = p.split(":") }.flatten
end
defaultto { provider.class.defpath if provider.class.respond_to?(:defpath) }
end
newparam(:pattern) do
desc "The pattern to search for in the process table.
This is used for stopping services on platforms that do not
support init scripts, and is also used for determining service
status on those service whose init scripts do not include a status
command.
If this is left unspecified and is needed to check the status
of a service, then the service name will be used instead.
The pattern can be a simple string or any legal Ruby pattern."
defaultto { @resource[:binary] || @resource[:name] }
end
newparam(:restart) do
desc "Specify a *restart* command manually. If left
unspecified, the service will be stopped and then started."
end
newparam(:start) do
desc "Specify a *start* command manually. Most service subsystems
support a `start` command, so this will not need to be
specified."
end
newparam(:status) do
- desc "Specify a *status* command manually. If left
- unspecified, the status method will be determined
- automatically, usually by looking for the service in the
- process table."
+ desc "Specify a *status* command manually. This command must
+ return 0 if the service is running and a nonzero value otherwise.
+ Ideally, these return codes should conform to
+ [the LSB's specification for init script status actions](http://refspecs.freestandards.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/iniscrptact.html),
+ but puppet only considers the difference between 0 and nonzero
+ to be relevant.
+
+ If left unspecified, the status method will be determined
+ automatically, usually by looking for the service in the process
+ table."
end
newparam(:stop) do
desc "Specify a *stop* command manually."
end
newparam(:control) do
desc "The control variable used to manage services (originally for HP-UX).
Defaults to the upcased service name plus `START` replacing dots with
underscores, for those providers that support the `controllable` feature."
defaultto { resource.name.gsub(".","_").upcase + "_START" if resource.provider.controllable? }
end
newparam :hasrestart do
desc "Specify that an init script has a `restart` option. Otherwise,
the init script's `stop` and `start` methods are used."
newvalues(:true, :false)
end
newparam(:manifest) do
desc "Specify a command to config a service, or a path to a manifest to do so."
end
# Basically just a synonym for restarting. Used to respond
# to events.
def refresh
# Only restart if we're actually running
if (@parameters[:ensure] || newattr(:ensure)).retrieve == :running
provider.restart
else
debug "Skipping restart; service is not running"
end
end
end
end
diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb
index 761d5d71b..e7389a0d1 100755
--- a/lib/puppet/type/user.rb
+++ b/lib/puppet/type/user.rb
@@ -1,438 +1,436 @@
require 'etc'
require 'facter'
require 'puppet/property/list'
require 'puppet/property/ordered_list'
require 'puppet/property/keyvalue'
module Puppet
newtype(:user) do
@doc = "Manage users. This type is mostly built to manage system
users, so it is lacking some features useful for managing normal
users.
This resource type uses the prescribed native tools for creating
groups and generally uses POSIX APIs for retrieving information
about them. It does not directly modify `/etc/passwd` or anything."
feature :allows_duplicates,
"The provider supports duplicate users with the same UID."
feature :manages_homedir,
"The provider can create and remove home directories."
feature :manages_passwords,
"The provider can modify user passwords, by accepting a password
hash."
feature :manages_password_age,
"The provider can set age requirements and restrictions for
passwords."
feature :manages_solaris_rbac,
"The provider can manage roles and normal users"
feature :manages_expiry,
"The provider can manage the expiry date for a user."
newproperty(:ensure, :parent => Puppet::Property::Ensure) do
newvalue(:present, :event => :user_created) do
provider.create
end
newvalue(:absent, :event => :user_removed) do
provider.delete
end
newvalue(:role, :event => :role_created, :required_features => :manages_solaris_rbac) do
provider.create_role
end
desc "The basic state that the object should be in."
# If they're talking about the thing at all, they generally want to
# say it should exist.
defaultto do
if @resource.managed?
:present
else
nil
end
end
def retrieve
if provider.exists?
if provider.respond_to?(:is_role?) and provider.is_role?
return :role
else
return :present
end
else
return :absent
end
end
end
newproperty(:home) do
desc "The home directory of the user. The directory must be created
separately and is not currently checked for existence."
end
newproperty(:uid) do
desc "The user ID. Must be specified numerically. For new users
being created, if no user ID is specified then one will be
chosen automatically, which will likely result in the same user
having different IDs on different systems, which is not
recommended. This is especially noteworthy if you use Puppet
to manage the same user on both Darwin and other platforms,
since Puppet does the ID generation for you on Darwin, but the
tools do so on other platforms."
munge do |value|
case value
when String
if value =~ /^[-0-9]+$/
value = Integer(value)
end
end
return value
end
end
newproperty(:gid) do
desc "The user's primary group. Can be specified numerically or
by name."
munge do |value|
if value.is_a?(String) and value =~ /^[-0-9]+$/
Integer(value)
else
value
end
end
def insync?(is)
- return true unless self.should
-
# We know the 'is' is a number, so we need to convert the 'should' to a number,
# too.
@should.each do |value|
return true if number = Puppet::Util.gid(value) and is == number
end
false
end
def sync
found = false
@should.each do |value|
if number = Puppet::Util.gid(value)
provider.gid = number
found = true
break
end
end
fail "Could not find group(s) #{@should.join(",")}" unless found
# Use the default event.
end
end
newproperty(:comment) do
desc "A description of the user. Generally is a user's full name."
end
newproperty(:shell) do
desc "The user's login shell. The shell must exist and be
executable."
end
newproperty(:password, :required_features => :manages_passwords) do
desc "The user's password, in whatever encrypted format the local machine requires. Be sure to enclose any value that includes a dollar sign ($) in single quotes (\')."
validate do |value|
raise ArgumentError, "Passwords cannot include ':'" if value.is_a?(String) and value.include?(":")
end
def change_to_s(currentvalue, newvalue)
if currentvalue == :absent
return "created password"
else
return "changed password"
end
end
end
newproperty(:password_min_age, :required_features => :manages_password_age) do
desc "The minimum amount of time in days a password must be used before it may be changed"
munge do |value|
case value
when String
Integer(value)
else
value
end
end
validate do |value|
- if value.to_s !~ /^\d+$/
+ if value.to_s !~ /^-?\d+$/
raise ArgumentError, "Password minimum age must be provided as a number"
end
end
end
newproperty(:password_max_age, :required_features => :manages_password_age) do
desc "The maximum amount of time in days a password may be used before it must be changed"
munge do |value|
case value
when String
Integer(value)
else
value
end
end
validate do |value|
- if value.to_s !~ /^\d+$/
+ if value.to_s !~ /^-?\d+$/
raise ArgumentError, "Password maximum age must be provided as a number"
end
end
end
newproperty(:groups, :parent => Puppet::Property::List) do
desc "The groups of which the user is a member. The primary
group should not be listed. Multiple groups should be
specified as an array."
validate do |value|
if value =~ /^\d+$/
raise ArgumentError, "Group names must be provided, not numbers"
end
raise ArgumentError, "Group names must be provided as an array, not a comma-separated list" if value.include?(",")
end
end
newparam(:name) do
desc "User name. While limitations are determined for
each operating system, it is generally a good idea to keep to
the degenerate 8 characters, beginning with a letter."
isnamevar
end
newparam(:membership) do
desc "Whether specified groups should be treated as the only groups
of which the user is a member or whether they should merely
be treated as the minimum membership list."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newparam(:allowdupe, :boolean => true) do
desc "Whether to allow duplicate UIDs."
newvalues(:true, :false)
defaultto false
end
newparam(:managehome, :boolean => true) do
desc "Whether to manage the home directory when managing the user."
newvalues(:true, :false)
defaultto false
validate do |val|
if val.to_s == "true"
raise ArgumentError, "User provider #{provider.class.name} can not manage home directories" unless provider.class.manages_homedir?
end
end
end
newproperty(:expiry, :required_features => :manages_expiry) do
desc "The expiry date for this user. Must be provided in
a zero padded YYYY-MM-DD format - e.g 2010-02-19."
validate do |value|
if value !~ /^\d{4}-\d{2}-\d{2}$/
raise ArgumentError, "Expiry dates must be YYYY-MM-DD"
end
end
end
# Autorequire the group, if it's around
autorequire(:group) do
autos = []
if obj = @parameters[:gid] and groups = obj.shouldorig
groups = groups.collect { |group|
if group =~ /^\d+$/
Integer(group)
else
group
end
}
groups.each { |group|
case group
when Integer
if resource = catalog.resources.find { |r| r.is_a?(Puppet::Type.type(:group)) and r.should(:gid) == group }
autos << resource
end
else
autos << group
end
}
end
if obj = @parameters[:groups] and groups = obj.should
autos += groups.split(",")
end
autos
end
# Provide an external hook. Yay breaking out of APIs.
def exists?
provider.exists?
end
def retrieve
absent = false
properties.inject({}) { |prophash, property|
current_value = :absent
if absent
prophash[property] = :absent
else
current_value = property.retrieve
prophash[property] = current_value
end
if property.name == :ensure and current_value == :absent
absent = true
end
prophash
}
end
newproperty(:roles, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do
desc "The roles the user has. Multiple roles should be
specified as an array."
def membership
:role_membership
end
validate do |value|
if value =~ /^\d+$/
raise ArgumentError, "Role names must be provided, not numbers"
end
raise ArgumentError, "Role names must be provided as an array, not a comma-separated list" if value.include?(",")
end
end
#autorequire the roles that the user has
autorequire(:user) do
reqs = []
if roles_property = @parameters[:roles] and roles = roles_property.should
reqs += roles.split(',')
end
reqs
end
newparam(:role_membership) do
desc "Whether specified roles should be treated as the only roles
of which the user is a member or whether they should merely
be treated as the minimum membership list."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:auths, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do
desc "The auths the user has. Multiple auths should be
specified as an array."
def membership
:auth_membership
end
validate do |value|
if value =~ /^\d+$/
raise ArgumentError, "Auth names must be provided, not numbers"
end
raise ArgumentError, "Auth names must be provided as an array, not a comma-separated list" if value.include?(",")
end
end
newparam(:auth_membership) do
desc "Whether specified auths should be treated as the only auths
of which the user is a member or whether they should merely
be treated as the minimum membership list."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:profiles, :parent => Puppet::Property::OrderedList, :required_features => :manages_solaris_rbac) do
desc "The profiles the user has. Multiple profiles should be
specified as an array."
def membership
:profile_membership
end
validate do |value|
if value =~ /^\d+$/
raise ArgumentError, "Profile names must be provided, not numbers"
end
raise ArgumentError, "Profile names must be provided as an array, not a comma-separated list" if value.include?(",")
end
end
newparam(:profile_membership) do
desc "Whether specified roles should be treated as the only roles
of which the user is a member or whether they should merely
be treated as the minimum membership list."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:keys, :parent => Puppet::Property::KeyValue, :required_features => :manages_solaris_rbac) do
desc "Specify user attributes in an array of keyvalue pairs"
def membership
:key_membership
end
validate do |value|
raise ArgumentError, "key value pairs must be seperated by an =" unless value.include?("=")
end
end
newparam(:key_membership) do
desc "Whether specified key value pairs should be treated as the only attributes
of the user or whether they should merely
be treated as the minimum list."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:project, :required_features => :manages_solaris_rbac) do
desc "The name of the project associated with a user"
end
end
end
diff --git a/lib/puppet/type/yumrepo.rb b/lib/puppet/type/yumrepo.rb
index 160b2164d..9b4c79428 100644
--- a/lib/puppet/type/yumrepo.rb
+++ b/lib/puppet/type/yumrepo.rb
@@ -1,359 +1,359 @@
# Description of yum repositories
require 'puppet/util/inifile'
module Puppet
# A property for one entry in a .ini-style file
class IniProperty < Puppet::Property
def insync?(is)
# A should property of :absent is the same as nil
- if is.nil? && (should.nil? || should == :absent)
+ if is.nil? && should == :absent
return true
end
super(is)
end
def sync
- if insync?(retrieve)
+ if safe_insync?(retrieve)
result = nil
else
result = set(self.should)
if should == :absent
resource.section[inikey] = nil
else
resource.section[inikey] = should
end
end
result
end
def retrieve
resource.section[inikey]
end
def inikey
name.to_s
end
# Set the key associated with this property to KEY, instead
# of using the property's NAME
def self.inikey(key)
# Override the inikey instance method
# Is there a way to do this without resorting to strings ?
# Using a block fails because the block can't access
# the variable 'key' in the outer scope
self.class_eval("def inikey ; \"#{key.to_s}\" ; end")
end
end
# Doc string for properties that can be made 'absent'
ABSENT_DOC="Set this to 'absent' to remove it from the file completely"
newtype(:yumrepo) do
@doc = "The client-side description of a yum repository. Repository
configurations are found by parsing `/etc/yum.conf` and
the files indicated by the `reposdir` option in that file
(see yum.conf(5) for details)
Most parameters are identical to the ones documented
in yum.conf(5)
Continuation lines that yum supports for example for the
baseurl are not supported. No attempt is made to access
files included with the **include** directive"
class << self
attr_accessor :filetype
# The writer is only used for testing, there should be no need
# to change yumconf or inifile in any other context
attr_accessor :yumconf
attr_writer :inifile
end
self.filetype = Puppet::Util::FileType.filetype(:flat)
@inifile = nil
@yumconf = "/etc/yum.conf"
# Where to put files for brand new sections
@defaultrepodir = nil
def self.instances
l = []
check = validproperties
clear
inifile.each_section do |s|
next if s.name == "main"
obj = new(:name => s.name, :audit => check)
current_values = obj.retrieve
obj.eachproperty do |property|
if current_values[property].nil?
obj.delete(property.name)
else
property.should = current_values[property]
end
end
obj.delete(:audit)
l << obj
end
l
end
# Return the Puppet::Util::IniConfig::File for the whole yum config
def self.inifile
if @inifile.nil?
@inifile = read
main = @inifile['main']
raise Puppet::Error, "File #{yumconf} does not contain a main section" if main.nil?
reposdir = main['reposdir']
reposdir ||= "/etc/yum.repos.d, /etc/yum/repos.d"
reposdir.gsub!(/[\n,]/, " ")
reposdir.split.each do |dir|
Dir::glob("#{dir}/*.repo").each do |file|
@inifile.read(file) if File.file?(file)
end
end
reposdir.split.each do |dir|
if File::directory?(dir) && File::writable?(dir)
@defaultrepodir = dir
break
end
end
end
@inifile
end
# Parse the yum config files. Only exposed for the tests
# Non-test code should use self.inifile to get at the
# underlying file
def self.read
result = Puppet::Util::IniConfig::File.new
result.read(yumconf)
main = result['main']
raise Puppet::Error, "File #{yumconf} does not contain a main section" if main.nil?
reposdir = main['reposdir']
reposdir ||= "/etc/yum.repos.d, /etc/yum/repos.d"
reposdir.gsub!(/[\n,]/, " ")
reposdir.split.each do |dir|
Dir::glob("#{dir}/*.repo").each do |file|
result.read(file) if File.file?(file)
end
end
if @defaultrepodir.nil?
reposdir.split.each do |dir|
if File::directory?(dir) && File::writable?(dir)
@defaultrepodir = dir
break
end
end
end
result
end
# Return the Puppet::Util::IniConfig::Section with name NAME
# from the yum config
def self.section(name)
result = inifile[name]
if result.nil?
# Brand new section
path = yumconf
path = File::join(@defaultrepodir, "#{name}.repo") unless @defaultrepodir.nil?
Puppet::info "create new repo #{name} in file #{path}"
result = inifile.add_section(name, path)
end
result
end
# Store all modifications back to disk
def self.store
inifile.store
unless Puppet[:noop]
target_mode = 0644 # FIXME: should be configurable
inifile.each_file do |file|
current_mode = File.stat(file).mode & 0777
unless current_mode == target_mode
Puppet::info "changing mode of #{file} from %03o to %03o" % [current_mode, target_mode]
File.chmod(target_mode, file)
end
end
end
end
# This is only used during testing.
def self.clear
@inifile = nil
@yumconf = "/etc/yum.conf"
@defaultrepodir = nil
end
# Return the Puppet::Util::IniConfig::Section for this yumrepo resource
def section
self.class.section(self[:name])
end
# Store modifications to this yumrepo resource back to disk
def flush
self.class.store
end
newparam(:name) do
desc "The name of the repository. This corresponds to the
repositoryid parameter in yum.conf(5)."
isnamevar
end
newproperty(:descr, :parent => Puppet::IniProperty) do
desc "A human readable description of the repository.
This corresponds to the name parameter in yum.conf(5).
#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(/.*/) { }
inikey "name"
end
newproperty(:mirrorlist, :parent => Puppet::IniProperty) do
desc "The URL that holds the list of mirrors for this repository.
#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
# Should really check that it's a valid URL
newvalue(/.*/) { }
end
newproperty(:baseurl, :parent => Puppet::IniProperty) do
desc "The URL for this repository.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
# Should really check that it's a valid URL
newvalue(/.*/) { }
end
newproperty(:enabled, :parent => Puppet::IniProperty) do
desc "Whether this repository is enabled or disabled. Possible
values are '0', and '1'.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(%r{(0|1)}) { }
end
newproperty(:gpgcheck, :parent => Puppet::IniProperty) do
desc "Whether to check the GPG signature on packages installed
from this repository. Possible values are '0', and '1'.
\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(%r{(0|1)}) { }
end
newproperty(:gpgkey, :parent => Puppet::IniProperty) do
desc "The URL for the GPG key with which packages from this
repository are signed.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
# Should really check that it's a valid URL
newvalue(/.*/) { }
end
newproperty(:include, :parent => Puppet::IniProperty) do
desc "A URL from which to include the config.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
# Should really check that it's a valid URL
newvalue(/.*/) { }
end
newproperty(:exclude, :parent => Puppet::IniProperty) do
desc "List of shell globs. Matching packages will never be
considered in updates or installs for this repo.
#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(/.*/) { }
end
newproperty(:includepkgs, :parent => Puppet::IniProperty) do
desc "List of shell globs. If this is set, only packages
matching one of the globs will be considered for
update or install.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(/.*/) { }
end
newproperty(:enablegroups, :parent => Puppet::IniProperty) do
desc "Determines whether yum will allow the use of
package groups for this repository. Possible
values are '0', and '1'.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(%r{(0|1)}) { }
end
newproperty(:failovermethod, :parent => Puppet::IniProperty) do
desc "Either 'roundrobin' or 'priority'.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(%r{roundrobin|priority}) { }
end
newproperty(:keepalive, :parent => Puppet::IniProperty) do
desc "Either '1' or '0'. This tells yum whether or not HTTP/1.1
keepalive should be used with this repository.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(%r{(0|1)}) { }
end
newproperty(:http_caching, :parent => Puppet::IniProperty) do
desc "Either 'packages' or 'all' or 'none'.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(%r(packages|all|none)) { }
end
newproperty(:timeout, :parent => Puppet::IniProperty) do
desc "Number of seconds to wait for a connection before timing
out.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(%r{[0-9]+}) { }
end
newproperty(:metadata_expire, :parent => Puppet::IniProperty) do
desc "Number of seconds after which the metadata will expire.
#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(%r{[0-9]+}) { }
end
newproperty(:protect, :parent => Puppet::IniProperty) do
desc "Enable or disable protection for this repository. Requires
that the protectbase plugin is installed and enabled.
#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(%r{(0|1)}) { }
end
newproperty(:priority, :parent => Puppet::IniProperty) do
desc "Priority of this repository from 1-99. Requires that
the priorities plugin is installed and enabled.
#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(%r{[1-9][0-9]?}) { }
end
newproperty(:cost, :parent => Puppet::IniProperty) do
desc "Cost of this repository.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(%r{\d+}) { }
end
newproperty(:proxy, :parent => Puppet::IniProperty) do
desc "URL to the proxy server for this repository.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
# Should really check that it's a valid URL
newvalue(/.*/) { }
end
newproperty(:proxy_username, :parent => Puppet::IniProperty) do
desc "Username for this proxy.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(/.*/) { }
end
newproperty(:proxy_password, :parent => Puppet::IniProperty) do
desc "Password for this proxy.\n#{ABSENT_DOC}"
newvalue(:absent) { self.should = :absent }
newvalue(/.*/) { }
end
end
end
diff --git a/lib/puppet/type/zpool.rb b/lib/puppet/type/zpool.rb
index 49cce552a..df06522e8 100755
--- a/lib/puppet/type/zpool.rb
+++ b/lib/puppet/type/zpool.rb
@@ -1,92 +1,88 @@
module Puppet
class Property
class VDev < Property
def flatten_and_sort(array)
array.collect { |a| a.split(' ') }.flatten.sort
end
def insync?(is)
- return true unless self.should
-
return @should == [:absent] if is == :absent
flatten_and_sort(is) == flatten_and_sort(@should)
end
end
class MultiVDev < VDev
def insync?(is)
- return true unless self.should
-
return @should == [:absent] if is == :absent
return false unless is.length == @should.length
is.each_with_index { |list, i| return false unless flatten_and_sort(list) == flatten_and_sort(@should[i]) }
#if we made it this far we are in sync
true
end
end
end
newtype(:zpool) do
@doc = "Manage zpools. Create and delete zpools. The provider WILL NOT SYNC, only report differences.
Supports vdevs with mirrors, raidz, logs and spares."
ensurable
newproperty(:disk, :array_matching => :all, :parent => Puppet::Property::VDev) do
desc "The disk(s) for this pool. Can be an array or space separated string"
end
newproperty(:mirror, :array_matching => :all, :parent => Puppet::Property::MultiVDev) do
desc "List of all the devices to mirror for this pool. Each mirror should be a space separated string:
mirror => [\"disk1 disk2\", \"disk3 disk4\"]
"
validate do |value|
raise ArgumentError, "mirror names must be provided as string separated, not a comma-separated list" if value.include?(",")
end
end
newproperty(:raidz, :array_matching => :all, :parent => Puppet::Property::MultiVDev) do
desc "List of all the devices to raid for this pool. Should be an array of space separated strings:
raidz => [\"disk1 disk2\", \"disk3 disk4\"]
"
validate do |value|
raise ArgumentError, "raid names must be provided as string separated, not a comma-separated list" if value.include?(",")
end
end
newproperty(:spare, :array_matching => :all, :parent => Puppet::Property::VDev) do
desc "Spare disk(s) for this pool."
end
newproperty(:log, :array_matching => :all, :parent => Puppet::Property::VDev) do
desc "Log disks for this pool. (doesn't support mirroring yet)"
end
newparam(:pool) do
desc "The name for this pool."
isnamevar
end
newparam(:raid_parity) do
desc "Determines parity when using raidz property."
end
validate do
has_should = [:disk, :mirror, :raidz].select { |prop| self.should(prop) }
self.fail "You cannot specify #{has_should.join(" and ")} on this type (only one)" if has_should.length > 1
end
end
end
diff --git a/lib/puppet/util/checksums.rb b/lib/puppet/util/checksums.rb
index 6fdf14ecf..e129301e6 100644
--- a/lib/puppet/util/checksums.rb
+++ b/lib/puppet/util/checksums.rb
@@ -1,136 +1,148 @@
# A stand-alone module for calculating checksums
# in a generic way.
module Puppet::Util::Checksums
class FakeChecksum
def <<(*args)
self
end
end
# Is the provided string a checksum?
def checksum?(string)
string =~ /^\{(\w{3,5})\}\S+/
end
# Strip the checksum type from an existing checksum
def sumdata(checksum)
checksum =~ /^\{(\w+)\}(.+)/ ? $2 : nil
end
# Strip the checksum type from an existing checksum
def sumtype(checksum)
checksum =~ /^\{(\w+)\}/ ? $1 : nil
end
# Calculate a checksum using Digest::MD5.
def md5(content)
require 'digest/md5'
Digest::MD5.hexdigest(content)
end
# Calculate a checksum of the first 500 chars of the content using Digest::MD5.
def md5lite(content)
md5(content[0..511])
end
# Calculate a checksum of a file's content using Digest::MD5.
def md5_file(filename, lite = false)
require 'digest/md5'
digest = Digest::MD5.new
checksum_file(digest, filename, lite)
end
# Calculate a checksum of the first 500 chars of a file's content using Digest::MD5.
def md5lite_file(filename)
md5_file(filename, true)
end
def md5_stream(&block)
require 'digest/md5'
digest = Digest::MD5.new
yield digest
digest.hexdigest
end
alias :md5lite_stream :md5_stream
# Return the :mtime timestamp of a file.
def mtime_file(filename)
File.stat(filename).send(:mtime)
end
# by definition this doesn't exist
# but we still need to execute the block given
def mtime_stream
noop_digest = FakeChecksum.new
yield noop_digest
nil
end
- alias :ctime_stream :mtime_stream
+ def mtime(content)
+ ""
+ end
# Calculate a checksum using Digest::SHA1.
def sha1(content)
require 'digest/sha1'
Digest::SHA1.hexdigest(content)
end
# Calculate a checksum of the first 500 chars of the content using Digest::SHA1.
def sha1lite(content)
sha1(content[0..511])
end
# Calculate a checksum of a file's content using Digest::SHA1.
def sha1_file(filename, lite = false)
require 'digest/sha1'
digest = Digest::SHA1.new
checksum_file(digest, filename, lite)
end
# Calculate a checksum of the first 500 chars of a file's content using Digest::SHA1.
def sha1lite_file(filename)
sha1_file(filename, true)
end
def sha1_stream
require 'digest/sha1'
digest = Digest::SHA1.new
yield digest
digest.hexdigest
end
alias :sha1lite_stream :sha1_stream
# Return the :ctime of a file.
def ctime_file(filename)
File.stat(filename).send(:ctime)
end
+ alias :ctime_stream :mtime_stream
+
+ def ctime(content)
+ ""
+ end
+
# Return a "no checksum"
def none_file(filename)
""
end
def none_stream
noop_digest = FakeChecksum.new
yield noop_digest
""
end
+ def none(content)
+ ""
+ end
+
private
# Perform an incremental checksum on a file.
def checksum_file(digest, filename, lite = false)
buffer = lite ? 512 : 4096
File.open(filename, 'r') do |file|
while content = file.read(buffer)
digest << content
break if lite
end
end
digest.hexdigest
end
end
diff --git a/lib/puppet/util/command_line/puppetd b/lib/puppet/util/command_line/puppetd
index cb8589c5f..7d78ce90f 100755
--- a/lib/puppet/util/command_line/puppetd
+++ b/lib/puppet/util/command_line/puppetd
@@ -1,186 +1,187 @@
#!/usr/bin/env ruby
# == Synopsis
#
# Retrieve the client configuration from the puppet master and apply
# it to the local host.
#
# Currently must be run out periodically, using cron or something similar.
#
# = Usage
#
# puppet agent [-D|--daemonize|--no-daemonize] [-d|--debug]
# [--detailed-exitcodes] [--disable] [--enable]
# [-h|--help] [--fqdn ] [-l|--logdest syslog||console]
# [-o|--onetime] [--serve ] [-t|--test] [--noop]
# [--digest ] [--fingerprint] [-V|--version]
# [-v|--verbose] [-w|--waitforcert ]
#
# = Description
#
# This is the main puppet client. Its job is to retrieve the local machine's
# configuration from a remote server and apply it. In order to successfully
# communicate with the remote server, the client must have a certificate signed
# by a certificate authority that the server trusts; the recommended method
# for this, at the moment, is to run a certificate authority as part of the
# puppet server (which is the default). The client will connect and request
# a signed certificate, and will continue connecting until it receives one.
#
# Once the client has a signed certificate, it will retrieve its configuration
# and apply it.
#
# = Usage Notes
#
# +puppet agent+ does its best to find a compromise between interactive use and
# daemon use. Run with no arguments and no configuration, it will go into the
# backgroun, attempt to get a signed certificate, and retrieve and apply its
# configuration every 30 minutes.
#
# Some flags are meant specifically for interactive use -- in particular,
# +test+, +tags+ or +fingerprint+ are useful. +test+ enables verbose logging, causes
# the daemon to stay in the foreground, exits if the server's configuration is
# invalid (this happens if, for instance, you've left a syntax error on the
# server), and exits after running the configuration once (rather than hanging
# around as a long-running process).
#
# +tags+ allows you to specify what portions of a configuration you want to apply.
# Puppet elements are tagged with all of the class or definition names that
# contain them, and you can use the +tags+ flag to specify one of these names,
# causing only configuration elements contained within that class or definition
# to be applied. This is very useful when you are testing new configurations --
# for instance, if you are just starting to manage +ntpd+, you would put all of
# the new elements into an +ntpd+ class, and call puppet with +--tags ntpd+,
# which would only apply that small portion of the configuration during your
# testing, rather than applying the whole thing.
#
# +fingerprint+ is a one-time flag. In this mode +puppet agent+ will run once and
# display on the console (and in the log) the current certificate (or certificate
# request) fingerprint. Providing the +--digest+ option allows to use a different
# digest algorithm to generate the fingerprint. The main use is to verify that
# before signing a certificate request on the master, the certificate request the
# master received is the same as the one the client sent (to prevent against
# man-in-the-middle attacks when signing certificates).
#
#
# = Options
#
# Note that any configuration parameter that's valid in the configuration file
# is also a valid long argument. For example, 'server' is a valid configuration
# parameter, so you can specify '--server ' as an argument.
#
# See the configuration file documentation at
# http://docs.puppetlabs.com/references/stable/configuration.html for
# the full list of acceptable parameters. A commented list of all
# configuration options can also be generated by running puppet agent with
# '--genconfig'.
#
# daemonize::
# Send the process into the background. This is the default.
#
# no-daemonize::
# Do not send the process into the background.
#
# debug::
# Enable full debugging.
#
# digest::
# Change the certificate fingerprinting digest algorithm. The default is MD5.
# Valid values depends on the version of OpenSSL installed, but should always
# at least contain MD5, MD2, SHA1 and SHA256.
#
# detailed-exitcodes::
# Provide transaction information via exit codes. If this is enabled, an
# exit code of '2' means there were changes, and an exit code of '4' means
# that there were failures during the transaction. This option only makes
# sense in conjunction with --onetime.
#
# disable::
# Disable working on the local system. This puts a lock file in place,
# causing +puppet agent+ not to work on the system until the lock file is removed.
# This is useful if you are testing a configuration and do not want the central
# configuration to override the local state until everything is tested and
# committed.
#
# +puppet agent+ uses the same lock file while it is running, so no more than one
# +puppet agent+ process is working at a time.
#
# +puppet agent+ exits after executing this.
#
# enable::
# Enable working on the local system. This removes any lock file, causing
# +puppet agent+ to start managing the local system again (although it will continue
# to use its normal scheduling, so it might not start for another half hour).
#
# +puppet agent+ exits after executing this.
#
# fqdn::
# Set the fully-qualified domain name of the client. This is only used for
# certificate purposes, but can be used to override the discovered hostname.
# If you need to use this flag, it is generally an indication of a setup problem.
#
# help::
# Print this help message
#
# logdest::
# Where to send messages. Choose between syslog, the console, and a log file.
# Defaults to sending messages to syslog, or the console if debugging or
# verbosity is enabled.
#
# no-client::
# Do not create a config client. This will cause the daemon to run
# without ever checking for its configuration automatically, and only
# makes sense when used in conjunction with --listen.
#
# onetime::
# Run the configuration once. Runs a single (normally daemonized) Puppet run.
# Useful for interactively running puppet agent when used in conjunction with
# the --no-daemonize option.
#
# fingerprint::
# Display the current certificate or certificate signing request fingerprint
# and then exit. Use the +--digest+ option to change the digest algorithm used.
#
# serve::
# Start another type of server. By default, +puppet agent+ will start
# a service handler that allows authenticated and authorized remote nodes to
# trigger the configuration to be pulled down and applied. You can specify
# any handler here that does not require configuration, e.g., filebucket, ca,
# or resource. The handlers are in +lib/puppet/network/handler+, and the names
# must match exactly, both in the call to +serve+ and in +namespaceauth.conf+.
#
# test::
# Enable the most common options used for testing. These are +onetime+,
-# +verbose+, +ignorecache, +no-daemonize+, and +no-usecacheonfailure+.
+# +verbose+, +ignorecache, +no-daemonize+, +no-usecacheonfailure+,
+# +detailed-exit-codes+, +no-splay+, and +show_diff+.
#
# noop::
# Use +noop+ mode where the daemon runs in a no-op or dry-run mode. This is useful
# for seeing what changes Puppet will make without actually executing the changes.
#
# verbose::
# Turn on verbose reporting.
#
# version::
# Print the puppet version number and exit.
#
# waitforcert::
# This option only matters for daemons that do not yet have certificates
# and it is enabled by default, with a value of 120 (seconds). This causes
# +puppet agent+ to connect to the server every 2 minutes and ask it to sign a
# certificate request. This is useful for the initial setup of a puppet
# client. You can turn off waiting for certificates by specifying a time
# of 0.
#
# = Example
#
# puppet agent --server puppet.domain.com
#
# = Author
#
# Luke Kanies
#
# = Copyright
#
# Copyright (c) 2005, 2006 Puppet Labs, LLC
# Licensed under the GNU Public License
#Puppet::Application[:agent].run
diff --git a/lib/puppet/util/command_line/puppetmasterd b/lib/puppet/util/command_line/puppetmasterd
index baf8a7581..445169820 100755
--- a/lib/puppet/util/command_line/puppetmasterd
+++ b/lib/puppet/util/command_line/puppetmasterd
@@ -1,65 +1,74 @@
#!/usr/bin/env ruby
#
# = Synopsis
#
# The central puppet server. Functions as a certificate authority by default.
#
# = Usage
#
# puppet master [-D|--daemonize|--no-daemonize] [-d|--debug] [-h|--help]
# [-l|--logdest |console|syslog] [-v|--verbose] [-V|--version]
+# [--compile ] [--apply ]
#
# = Description
#
# This is the puppet central daemon.
#
# = Options
#
# Note that any configuration parameter that's valid in the configuration file
# is also a valid long argument. For example, 'ssldir' is a valid configuration
# parameter, so you can specify '--ssldir ' as an argument.
#
# See the configuration file documentation at
# http://docs.puppetlabs.com/references/stable/configuration.html for
# the full list of acceptable parameters. A commented list of all
# configuration options can also be generated by running puppetmasterdd with
# '--genconfig'.
#
# daemonize::
# Send the process into the background. This is the default.
#
# no-daemonize::
# Do not send the process into the background.
#
# debug::
# Enable full debugging.
#
# help::
# Print this help message.
#
# logdest::
# Where to send messages. Choose between syslog, the console, and a log file.
# Defaults to sending messages to syslog, or the console
# if debugging or verbosity is enabled.
#
# verbose::
# Enable verbosity.
#
# version::
# Print the puppet version number and exit.
#
+# compile::
+# Capability to compile a catalogue and output it in JSON from the Puppet master. Uses
+# facts contained in the $vardir/yaml/ directory to compile the catalog.
+#
+# apply::
+# Capability to apply JSON catalog (such as one generated with --compile). You can either specify
+# a JSON file or pipe in JSON from standard input.
+#
# = Example
#
# puppet master
#
# = Author
#
# Luke Kanies
#
# = Copyright
#
# Copyright (c) 2005 Puppet Labs, LLC
# Licensed under the GNU Public License
#Puppet::Application[:master].run
diff --git a/lib/puppet/util/zaml.rb b/lib/puppet/util/zaml.rb
index 9fda5ae3b..6ac956565 100644
--- a/lib/puppet/util/zaml.rb
+++ b/lib/puppet/util/zaml.rb
@@ -1,321 +1,320 @@
#
# ZAML -- A partial replacement for YAML, writen with speed and code clarity
# in mind. ZAML fixes one YAML bug (loading Exceptions) and provides
# a replacement for YAML.dump unimaginatively called ZAML.dump,
# which is faster on all known cases and an order of magnitude faster
# with complex structures.
#
# http://github.com/hallettj/zaml
#
# Authors: Markus Roberts, Jesse Hallett, Ian McIntosh, Igal Koshevoy, Simon Chiang
#
require 'yaml'
class ZAML
VERSION = "0.1.1"
#
# Class Methods
#
def self.dump(stuff, where='')
z = new
stuff.to_zaml(z)
where << z.to_s
end
#
# Instance Methods
#
def initialize
@result = []
@indent = nil
@structured_key_prefix = nil
Label.counter_reset
emit('--- ')
end
def nested(tail=' ')
old_indent = @indent
@indent = "#{@indent || "\n"}#{tail}"
yield
@indent = old_indent
end
class Label
#
# YAML only wants objects in the datastream once; if the same object
# occurs more than once, we need to emit a label ("&idxxx") on the
# first occurrence and then emit a back reference (*idxxx") on any
# subsequent occurrence(s).
#
# To accomplish this we keeps a hash (by object id) of the labels of
# the things we serialize as we begin to serialize them. The labels
# initially serialize as an empty string (since most objects are only
# going to be be encountered once), but can be changed to a valid
# (by assigning it a number) the first time it is subsequently used,
# if it ever is. Note that we need to do the label setup BEFORE we
# start to serialize the object so that circular structures (in
# which we will encounter a reference to the object as we serialize
# it can be handled).
#
def self.counter_reset
@@previously_emitted_object = {}
@@next_free_label_number = 0
end
- def initialize(obj,indent)
- @indent = indent
+ def initialize(obj)
@this_label_number = nil
@@previously_emitted_object[obj.object_id] = self
end
def to_s
- @this_label_number ? ('&id%03d%s' % [@this_label_number, @indent]) : ''
+ @this_label_number ? ('&id%03d ' % @this_label_number) : ''
end
def reference
@this_label_number ||= (@@next_free_label_number += 1)
@reference ||= '*id%03d' % @this_label_number
end
def self.for(obj)
@@previously_emitted_object[obj.object_id]
end
end
def new_label_for(obj)
- Label.new(obj,(Hash === obj || Array === obj) ? "#{@indent || "\n"} " : ' ')
+ Label.new(obj)
end
def first_time_only(obj)
if label = Label.for(obj)
emit(label.reference)
else
if @structured_key_prefix and not obj.is_a? String
emit(@structured_key_prefix)
@structured_key_prefix = nil
end
emit(new_label_for(obj))
yield
end
end
def emit(s)
@result << s
@recent_nl = false unless s.kind_of?(Label)
end
def nl(s='')
emit(@indent || "\n") unless @recent_nl
emit(s)
@recent_nl = true
end
def to_s
@result.join
end
def prefix_structured_keys(x)
@structured_key_prefix = x
yield
nl unless @structured_key_prefix
@structured_key_prefix = nil
end
end
################################################################
#
# Behavior for custom classes
#
################################################################
class Object
def to_yaml_properties
instance_variables.sort # Default YAML behavior
end
def yaml_property_munge(x)
x
end
def zamlized_class_name(root)
cls = self.class
"!ruby/#{root.name.downcase}#{cls == root ? '' : ":#{cls.respond_to?(:name) ? cls.name : cls}"}"
end
def to_zaml(z)
z.first_time_only(self) {
z.emit(zamlized_class_name(Object))
z.nested {
instance_variables = to_yaml_properties
if instance_variables.empty?
z.emit(" {}")
else
instance_variables.each { |v|
z.nl
v[1..-1].to_zaml(z) # Remove leading '@'
z.emit(': ')
yaml_property_munge(instance_variable_get(v)).to_zaml(z)
}
end
}
}
end
end
################################################################
#
# Behavior for built-in classes
#
################################################################
class NilClass
def to_zaml(z)
z.emit('') # NOTE: blank turns into nil in YAML.load
end
end
class Symbol
def to_zaml(z)
z.emit(self.inspect)
end
end
class TrueClass
def to_zaml(z)
z.emit('true')
end
end
class FalseClass
def to_zaml(z)
z.emit('false')
end
end
class Numeric
def to_zaml(z)
z.emit(self)
end
end
class Regexp
def to_zaml(z)
z.first_time_only(self) { z.emit("#{zamlized_class_name(Regexp)} #{inspect}") }
end
end
class Exception
def to_zaml(z)
z.emit(zamlized_class_name(Exception))
z.nested {
z.nl("message: ")
message.to_zaml(z)
}
end
#
# Monkey patch for buggy Exception restore in YAML
#
# This makes it work for now but is not very future-proof; if things
# change we'll most likely want to remove this. To mitigate the risks
# as much as possible, we test for the bug before appling the patch.
#
if respond_to? :yaml_new and yaml_new(self, :tag, "message" => "blurp").message != "blurp"
def self.yaml_new( klass, tag, val )
o = YAML.object_maker( klass, {} ).exception(val.delete( 'message'))
val.each_pair do |k,v|
o.instance_variable_set("@#{k}", v)
end
o
end
end
end
class String
ZAML_ESCAPES = %w{\x00 \x01 \x02 \x03 \x04 \x05 \x06 \a \x08 \t \n \v \f \r \x0e \x0f \x10 \x11 \x12 \x13 \x14 \x15 \x16 \x17 \x18 \x19 \x1a \e \x1c \x1d \x1e \x1f }
def escaped_for_zaml
gsub( /\x5C/, "\\\\\\" ). # Demi-kludge for Maglev/rubinius; the regexp should be /\\/ but parsetree chokes on that.
gsub( /"/, "\\\"" ).
gsub( /([\x00-\x1F])/ ) { |x| ZAML_ESCAPES[ x.unpack("C")[0] ] }.
gsub( /([\x80-\xFF])/ ) { |x| "\\x#{x.unpack("C")[0].to_s(16)}" }
end
def to_zaml(z)
z.first_time_only(self) {
num = '[-+]?(0x)?\d+\.?\d*'
case
when self == ''
z.emit('""')
# when self =~ /[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\xFF]/
# z.emit("!binary |\n")
# z.emit([self].pack("m*"))
when (
(self =~ /\A(true|false|yes|no|on|null|off|#{num}(:#{num})*|!|=|~)$/i) or
(self =~ /\A\n* /) or
(self =~ /[\s:]$/) or
(self =~ /^[>|][-+\d]*\s/i) or
(self[-1..-1] =~ /\s/) or
(self =~ /[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\xFF]/) or
(self =~ /[,\[\]\{\}\r\t]|:\s|\s#/) or
(self =~ /\A([-:?!#&*'"]|<<|%.+:.)/)
)
z.emit("\"#{escaped_for_zaml}\"")
when self =~ /\n/
if self[-1..-1] == "\n" then z.emit('|+') else z.emit('|-') end
z.nested { split("\n",-1).each { |line| z.nl; z.emit(line.chomp("\n")) } }
else
z.emit(self)
end
}
end
end
class Hash
def to_zaml(z)
z.first_time_only(self) {
z.nested {
if empty?
z.emit('{}')
else
each_pair { |k, v|
z.nl
z.prefix_structured_keys('? ') { k.to_zaml(z) }
z.emit(': ')
v.to_zaml(z)
}
end
}
}
end
end
class Array
def to_zaml(z)
z.first_time_only(self) {
z.nested {
if empty?
z.emit('[]')
else
each { |v| z.nl('- '); v.to_zaml(z) }
end
}
}
end
end
class Time
def to_zaml(z)
# 2008-12-06 10:06:51.373758 -07:00
ms = ("%0.6f" % (usec * 1e-6)).sub(/^\d+\./,'')
offset = "%+0.2i:%0.2i" % [utc_offset / 3600, (utc_offset / 60) % 60]
z.emit(self.strftime("%Y-%m-%d %H:%M:%S.#{ms} #{offset}"))
end
end
class Date
def to_zaml(z)
z.emit(strftime('%Y-%m-%d'))
end
end
class Range
def to_zaml(z)
z.first_time_only(self) {
z.emit(zamlized_class_name(Range))
z.nested {
z.nl
z.emit('begin: ')
z.emit(first)
z.nl
z.emit('end: ')
z.emit(last)
z.nl
z.emit('excl: ')
z.emit(exclude_end?)
}
}
end
end
diff --git a/spec/fixtures/unit/provider/mount/mount-output.aix.txt b/spec/fixtures/unit/provider/mount/mount-output.aix.txt
new file mode 100644
index 000000000..54edb9c1c
--- /dev/null
+++ b/spec/fixtures/unit/provider/mount/mount-output.aix.txt
@@ -0,0 +1,7 @@
+/dev/hd4 / jfs2 Nov 11 12:11 rw,log=/dev/hd8
+/dev/hd2 /usr jfs2 Nov 11 12:11 rw,log=/dev/hd8
+/dev/hd9var /var jfs2 Nov 11 12:11 rw,log=/dev/hd8
+/dev/hd3 /tmp jfs2 Nov 11 12:11 rw,log=/dev/hd8
+/dev/hd1 /home jfs2 Nov 11 12:11 rw,log=/dev/hd8
+/proc /proc procfs Nov 11 12:11 rw
+/dev/hd10opt /opt jfs2 Nov 11 12:11 rw,log=/dev/hd8
diff --git a/spec/unit/parser/compiler_spec.rb b/spec/unit/parser/compiler_spec.rb
index 95f3853e2..687f2ecb9 100755
--- a/spec/unit/parser/compiler_spec.rb
+++ b/spec/unit/parser/compiler_spec.rb
@@ -1,765 +1,850 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
class CompilerTestResource
attr_accessor :builtin, :virtual, :evaluated, :type, :title
def initialize(type, title)
@type = type
@title = title
end
def [](attr)
return nil if attr == :stage
:main
end
def ref
"#{type.to_s.capitalize}[#{title}]"
end
def evaluated?
@evaluated
end
def builtin_type?
@builtin
end
def virtual?
@virtual
end
def evaluate
end
end
describe Puppet::Parser::Compiler do
def resource(type, title)
Puppet::Parser::Resource.new(type, title, :scope => @scope)
end
before :each do
@node = Puppet::Node.new "testnode"
@known_resource_types = Puppet::Resource::TypeCollection.new "development"
@compiler = Puppet::Parser::Compiler.new(@node)
@scope = Puppet::Parser::Scope.new(:compiler => @compiler, :source => stub('source'))
@scope_resource = Puppet::Parser::Resource.new(:file, "/my/file", :scope => @scope)
@scope.resource = @scope_resource
@compiler.environment.stubs(:known_resource_types).returns @known_resource_types
end
it "should have a class method that compiles, converts, and returns a catalog" do
compiler = stub 'compiler'
Puppet::Parser::Compiler.expects(:new).with(@node).returns compiler
catalog = stub 'catalog'
compiler.expects(:compile).returns catalog
converted_catalog = stub 'converted_catalog'
catalog.expects(:to_resource).returns converted_catalog
Puppet::Parser::Compiler.compile(@node).should equal(converted_catalog)
end
it "should fail intelligently when a class-level compile fails" do
Puppet::Parser::Compiler.expects(:new).raises ArgumentError
lambda { Puppet::Parser::Compiler.compile(@node) }.should raise_error(Puppet::Error)
end
it "should use the node's environment as its environment" do
@compiler.environment.should equal(@node.environment)
end
it "should include the resource type collection helper" do
Puppet::Parser::Compiler.ancestors.should be_include(Puppet::Resource::TypeCollectionHelper)
end
it "should be able to return a class list containing all added classes" do
@compiler.add_class ""
@compiler.add_class "one"
@compiler.add_class "two"
@compiler.classlist.sort.should == %w{one two}.sort
end
describe "when initializing" do
it "should set its node attribute" do
@compiler.node.should equal(@node)
end
it "should detect when ast nodes are absent" do
@compiler.ast_nodes?.should be_false
end
it "should detect when ast nodes are present" do
@known_resource_types.expects(:nodes?).returns true
@compiler.ast_nodes?.should be_true
end
it "should copy the known_resource_types version to the catalog" do
@compiler.catalog.version.should == @known_resource_types.version
end
it "should copy any node classes into the class list" do
node = Puppet::Node.new("mynode")
node.classes = %w{foo bar}
compiler = Puppet::Parser::Compiler.new(node)
- compiler.classlist.should include("foo")
- compiler.classlist.should include("bar")
+ compiler.classlist.should =~ ['foo', 'bar']
+ end
+
+ it "should transform node class hashes into a class list" do
+ node = Puppet::Node.new("mynode")
+ node.classes = {'foo'=>{'one'=>'1'}, 'bar'=>{'two'=>'2'}}
+ compiler = Puppet::Parser::Compiler.new(node)
+
+ compiler.classlist.should =~ ['foo', 'bar']
end
it "should add a 'main' stage to the catalog" do
@compiler.catalog.resource(:stage, :main).should be_instance_of(Puppet::Parser::Resource)
end
end
describe "when managing scopes" do
it "should create a top scope" do
@compiler.topscope.should be_instance_of(Puppet::Parser::Scope)
end
it "should be able to create new scopes" do
@compiler.newscope(@compiler.topscope).should be_instance_of(Puppet::Parser::Scope)
end
it "should correctly set the level of newly created scopes" do
@compiler.newscope(@compiler.topscope, :level => 5).level.should == 5
end
it "should set the parent scope of the new scope to be the passed-in parent" do
scope = mock 'scope'
newscope = @compiler.newscope(scope)
newscope.parent.should equal(scope)
end
it "should set the parent scope of the new scope to its topscope if the parent passed in is nil" do
scope = mock 'scope'
newscope = @compiler.newscope(nil)
newscope.parent.should equal(@compiler.topscope)
end
end
describe "when compiling" do
def compile_methods
[:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated,
:finish, :store, :extract, :evaluate_relationships]
end
# Stub all of the main compile methods except the ones we're specifically interested in.
def compile_stub(*except)
(compile_methods - except).each { |m| @compiler.stubs(m) }
end
it "should set node parameters as variables in the top scope" do
params = {"a" => "b", "c" => "d"}
@node.stubs(:parameters).returns(params)
compile_stub(:set_node_parameters)
@compiler.compile
@compiler.topscope.lookupvar("a").should == "b"
@compiler.topscope.lookupvar("c").should == "d"
end
it "should set the client and server versions on the catalog" do
params = {"clientversion" => "2", "serverversion" => "3"}
@node.stubs(:parameters).returns(params)
compile_stub(:set_node_parameters)
@compiler.compile
@compiler.catalog.client_version.should == "2"
@compiler.catalog.server_version.should == "3"
end
it "should evaluate any existing classes named in the node" do
classes = %w{one two three four}
main = stub 'main'
one = stub 'one', :name => "one"
three = stub 'three', :name => "three"
@node.stubs(:name).returns("whatever")
@node.stubs(:classes).returns(classes)
@compiler.expects(:evaluate_classes).with(classes, @compiler.topscope)
@compiler.class.publicize_methods(:evaluate_node_classes) { @compiler.evaluate_node_classes }
end
+ it "should evaluate any parameterized classes named in the node" do
+ classes = {'foo'=>{'1'=>'one'}, 'bar'=>{'2'=>'two'}}
+ @node.stubs(:classes).returns(classes)
+ @compiler.expects(:evaluate_classes).with(classes, @compiler.topscope)
+ @compiler.compile
+ end
+
+
it "should evaluate the main class if it exists" do
compile_stub(:evaluate_main)
main_class = @known_resource_types.add Puppet::Resource::Type.new(:hostclass, "")
main_class.expects(:evaluate_code).with { |r| r.is_a?(Puppet::Parser::Resource) }
@compiler.topscope.expects(:source=).with(main_class)
@compiler.compile
end
it "should create a new, empty 'main' if no main class exists" do
compile_stub(:evaluate_main)
@compiler.compile
@known_resource_types.find_hostclass([""], "").should be_instance_of(Puppet::Resource::Type)
end
it "should add an edge between the main stage and main class" do
@compiler.compile
(stage = @compiler.catalog.resource(:stage, "main")).should be_instance_of(Puppet::Parser::Resource)
(klass = @compiler.catalog.resource(:class, "")).should be_instance_of(Puppet::Parser::Resource)
@compiler.catalog.edge?(stage, klass).should be_true
end
it "should evaluate any node classes" do
@node.stubs(:classes).returns(%w{one two three four})
@compiler.expects(:evaluate_classes).with(%w{one two three four}, @compiler.topscope)
@compiler.send(:evaluate_node_classes)
end
it "should evaluate all added collections" do
colls = []
# And when the collections fail to evaluate.
colls << mock("coll1-false")
colls << mock("coll2-false")
colls.each { |c| c.expects(:evaluate).returns(false) }
@compiler.add_collection(colls[0])
@compiler.add_collection(colls[1])
compile_stub(:evaluate_generators)
@compiler.compile
end
it "should ignore builtin resources" do
resource = resource(:file, "testing")
@compiler.add_resource(@scope, resource)
resource.expects(:evaluate).never
@compiler.compile
end
it "should evaluate unevaluated resources" do
resource = CompilerTestResource.new(:file, "testing")
@compiler.add_resource(@scope, resource)
# We have to now mark the resource as evaluated
resource.expects(:evaluate).with { |*whatever| resource.evaluated = true }
@compiler.compile
end
it "should not evaluate already-evaluated resources" do
resource = resource(:file, "testing")
resource.stubs(:evaluated?).returns true
@compiler.add_resource(@scope, resource)
resource.expects(:evaluate).never
@compiler.compile
end
it "should evaluate unevaluated resources created by evaluating other resources" do
resource = CompilerTestResource.new(:file, "testing")
@compiler.add_resource(@scope, resource)
resource2 = CompilerTestResource.new(:file, "other")
# We have to now mark the resource as evaluated
resource.expects(:evaluate).with { |*whatever| resource.evaluated = true; @compiler.add_resource(@scope, resource2) }
resource2.expects(:evaluate).with { |*whatever| resource2.evaluated = true }
@compiler.compile
end
describe "when finishing" do
before do
@compiler.send(:evaluate_main)
@catalog = @compiler.catalog
end
def add_resource(name, parent = nil)
resource = Puppet::Parser::Resource.new "file", name, :scope => @scope
@compiler.add_resource(@scope, resource)
@catalog.add_edge(parent, resource) if parent
resource
end
it "should call finish() on all resources" do
# Add a resource that does respond to :finish
resource = Puppet::Parser::Resource.new "file", "finish", :scope => @scope
resource.expects(:finish)
@compiler.add_resource(@scope, resource)
# And one that does not
dnf_resource = stub_everything "dnf", :ref => "File[dnf]", :type => "file"
@compiler.add_resource(@scope, dnf_resource)
@compiler.send(:finish)
end
it "should call finish() in add_resource order" do
resources = sequence('resources')
resource1 = add_resource("finish1")
resource1.expects(:finish).in_sequence(resources)
resource2 = add_resource("finish2")
resource2.expects(:finish).in_sequence(resources)
@compiler.send(:finish)
end
it "should add each container's metaparams to its contained resources" do
main = @catalog.resource(:class, :main)
main[:noop] = true
resource1 = add_resource("meh", main)
@compiler.send(:finish)
resource1[:noop].should be_true
end
it "should add metaparams recursively" do
main = @catalog.resource(:class, :main)
main[:noop] = true
resource1 = add_resource("meh", main)
resource2 = add_resource("foo", resource1)
@compiler.send(:finish)
resource2[:noop].should be_true
end
it "should prefer metaparams from immediate parents" do
main = @catalog.resource(:class, :main)
main[:noop] = true
resource1 = add_resource("meh", main)
resource2 = add_resource("foo", resource1)
resource1[:noop] = false
@compiler.send(:finish)
resource2[:noop].should be_false
end
it "should merge tags downward" do
main = @catalog.resource(:class, :main)
main.tag("one")
resource1 = add_resource("meh", main)
resource1.tag "two"
resource2 = add_resource("foo", resource1)
@compiler.send(:finish)
resource2.tags.should be_include("one")
resource2.tags.should be_include("two")
end
it "should work if only middle resources have metaparams set" do
main = @catalog.resource(:class, :main)
resource1 = add_resource("meh", main)
resource1[:noop] = true
resource2 = add_resource("foo", resource1)
@compiler.send(:finish)
resource2[:noop].should be_true
end
end
it "should return added resources in add order" do
resource1 = resource(:file, "yay")
@compiler.add_resource(@scope, resource1)
resource2 = resource(:file, "youpi")
@compiler.add_resource(@scope, resource2)
@compiler.resources.should == [resource1, resource2]
end
it "should add resources that do not conflict with existing resources" do
resource = resource(:file, "yay")
@compiler.add_resource(@scope, resource)
@compiler.catalog.should be_vertex(resource)
end
it "should fail to add resources that conflict with existing resources" do
path = Puppet.features.posix? ? "/foo" : "C:/foo"
file1 = Puppet::Type.type(:file).new :path => path
file2 = Puppet::Type.type(:file).new :path => path
@compiler.add_resource(@scope, file1)
lambda { @compiler.add_resource(@scope, file2) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError)
end
it "should add an edge from the scope resource to the added resource" do
resource = resource(:file, "yay")
@compiler.add_resource(@scope, resource)
@compiler.catalog.should be_edge(@scope.resource, resource)
end
it "should add an edge to any specified stage for class resources" do
other_stage = resource(:stage, "other")
@compiler.add_resource(@scope, other_stage)
resource = resource(:class, "foo")
resource[:stage] = 'other'
@compiler.add_resource(@scope, resource)
@compiler.catalog.edge?(other_stage, resource).should be_true
end
it "should fail if a non-class resource attempts to set a stage" do
other_stage = resource(:stage, "other")
@compiler.add_resource(@scope, other_stage)
resource = resource(:file, "foo")
resource[:stage] = 'other'
lambda { @compiler.add_resource(@scope, resource) }.should raise_error(ArgumentError)
end
it "should fail if an unknown stage is specified" do
resource = resource(:class, "foo")
resource[:stage] = 'other'
lambda { @compiler.add_resource(@scope, resource) }.should raise_error(ArgumentError)
end
it "should add edges from the class resources to the parent's stage if no stage is specified" do
main = @compiler.catalog.resource(:stage, :main)
foo_stage = resource(:stage, :foo_stage)
@compiler.add_resource(@scope, foo_stage)
resource = resource(:class, "foo")
@scope.stubs(:resource).returns(:stage => :foo_stage)
@compiler.add_resource(@scope, resource)
@compiler.catalog.should be_edge(foo_stage, resource)
end
it "should add edges from top-level class resources to the main stage if no stage is specified" do
main = @compiler.catalog.resource(:stage, :main)
resource = resource(:class, "foo")
@compiler.add_resource(@scope, resource)
@compiler.catalog.should be_edge(main, resource)
end
it "should not add non-class resources that don't specify a stage to the 'main' stage" do
main = @compiler.catalog.resource(:stage, :main)
resource = resource(:file, "foo")
@compiler.add_resource(@scope, resource)
@compiler.catalog.should_not be_edge(main, resource)
end
it "should not add any parent-edges to stages" do
stage = resource(:stage, "other")
@compiler.add_resource(@scope, stage)
@scope.resource = resource(:class, "foo")
@compiler.catalog.edge?(@scope.resource, stage).should be_false
end
it "should not attempt to add stages to other stages" do
other_stage = resource(:stage, "other")
second_stage = resource(:stage, "second")
@compiler.add_resource(@scope, other_stage)
@compiler.add_resource(@scope, second_stage)
second_stage[:stage] = "other"
@compiler.catalog.edge?(other_stage, second_stage).should be_false
end
it "should have a method for looking up resources" do
resource = resource(:yay, "foo")
@compiler.add_resource(@scope, resource)
@compiler.findresource("Yay[foo]").should equal(resource)
end
it "should be able to look resources up by type and title" do
resource = resource(:yay, "foo")
@compiler.add_resource(@scope, resource)
@compiler.findresource("Yay", "foo").should equal(resource)
end
it "should not evaluate virtual defined resources" do
resource = resource(:file, "testing")
resource.virtual = true
@compiler.add_resource(@scope, resource)
resource.expects(:evaluate).never
@compiler.compile
end
end
describe "when evaluating collections" do
it "should evaluate each collection" do
2.times { |i|
coll = mock 'coll%s' % i
@compiler.add_collection(coll)
# This is the hard part -- we have to emulate the fact that
# collections delete themselves if they are done evaluating.
coll.expects(:evaluate).with do
@compiler.delete_collection(coll)
end
}
@compiler.class.publicize_methods(:evaluate_collections) { @compiler.evaluate_collections }
end
it "should not fail when there are unevaluated resource collections that do not refer to specific resources" do
coll = stub 'coll', :evaluate => false
coll.expects(:resources).returns(nil)
@compiler.add_collection(coll)
lambda { @compiler.compile }.should_not raise_error
end
it "should fail when there are unevaluated resource collections that refer to a specific resource" do
coll = stub 'coll', :evaluate => false
coll.expects(:resources).returns(:something)
@compiler.add_collection(coll)
- lambda { @compiler.compile }.should raise_error(Puppet::ParseError)
+ lambda { @compiler.compile }.should raise_error Puppet::ParseError, 'Failed to realize virtual resources something'
end
it "should fail when there are unevaluated resource collections that refer to multiple specific resources" do
coll = stub 'coll', :evaluate => false
coll.expects(:resources).returns([:one, :two])
@compiler.add_collection(coll)
- lambda { @compiler.compile }.should raise_error(Puppet::ParseError)
+ lambda { @compiler.compile }.should raise_error Puppet::ParseError, 'Failed to realize virtual resources one, two'
end
end
describe "when evaluating relationships" do
it "should evaluate each relationship with its catalog" do
dep = stub 'dep'
dep.expects(:evaluate).with(@compiler.catalog)
@compiler.add_relationship dep
@compiler.evaluate_relationships
end
end
describe "when told to evaluate missing classes" do
it "should fail if there's no source listed for the scope" do
scope = stub 'scope', :source => nil
proc { @compiler.evaluate_classes(%w{one two}, scope) }.should raise_error(Puppet::DevError)
end
it "should tag the catalog with the name of each not-found class" do
@compiler.catalog.expects(:tag).with("notfound")
@scope.expects(:find_hostclass).with("notfound").returns(nil)
@compiler.evaluate_classes(%w{notfound}, @scope)
end
+ # I wish it would fail
+ it "should log when it can't find class" do
+ klasses = {'foo'=>nil}
+ @node.classes = klasses
+ @compiler.topscope.stubs(:find_hostclass).with('foo').returns(nil)
+ Puppet.expects(:info).with('Could not find class foo for testnode')
+ @compiler.compile
+ end
end
describe "when evaluating found classes" do
before do
@class = stub 'class', :name => "my::class"
@scope.stubs(:find_hostclass).with("myclass").returns(@class)
@resource = stub 'resource', :ref => "Class[myclass]", :type => "file"
end
it "should evaluate each class" do
@compiler.catalog.stubs(:tag)
@class.expects(:ensure_in_catalog).with(@scope)
@scope.stubs(:class_scope).with(@class)
@compiler.evaluate_classes(%w{myclass}, @scope)
end
+ it "should ensure each node class hash is in catalog and have appropriate parameters" do
+ klasses = {'foo'=>{'1'=>'one'}, 'bar::foo'=>{'2'=>'two'}, 'bar'=>{'1'=> [1,2,3], '2'=>{'foo'=>'bar'}}}
+ @node.classes = klasses
+ ast_obj = Puppet::Parser::AST::String.new(:value => 'foo')
+ klasses.each do |name, params|
+ klass = Puppet::Resource::Type.new(:hostclass, name, :arguments => {'1' => ast_obj, '2' => ast_obj})
+ @compiler.topscope.known_resource_types.add klass
+ end
+ catalog = @compiler.compile
+ catalog.classes.should =~ ['foo', 'bar::foo', 'settings', 'bar']
+
+ r1 = catalog.resources.detect {|r| r.title == 'Foo' }
+ r1.to_hash.should == {:'1' => 'one', :'2' => 'foo'}
+ r1.tags. should =~ ['class', 'foo']
+
+ r2 = catalog.resources.detect {|r| r.title == 'Bar::Foo' }
+ r2.to_hash.should == {:'1' => 'foo', :'2' => 'two'}
+ r2.tags.should =~ ['bar::foo', 'class', 'bar', 'foo']
+
+ r2 = catalog.resources.detect {|r| r.title == 'Bar' }
+ r2.to_hash.should == {:'1' => [1,2,3], :'2' => {'foo'=>'bar'}}
+ r2.tags.should =~ ['class', 'bar']
+ end
+
+ it "should ensure each node class is in catalog and has appropriate tags" do
+ klasses = ['bar::foo']
+ @node.classes = klasses
+ ast_obj = Puppet::Parser::AST::String.new(:value => 'foo')
+ klasses.each do |name|
+ klass = Puppet::Resource::Type.new(:hostclass, name, :arguments => {'1' => ast_obj, '2' => ast_obj})
+ @compiler.topscope.known_resource_types.add klass
+ end
+ catalog = @compiler.compile
+
+ r2 = catalog.resources.detect {|r| r.title == 'Bar::Foo' }
+ r2.tags.should =~ ['bar::foo', 'class', 'bar', 'foo']
+ end
+
+ it "should fail if required parameters are missing" do
+ klass = {'foo'=>{'1'=>'one'}}
+ @node.classes = klass
+ klass = Puppet::Resource::Type.new(:hostclass, 'foo', :arguments => {'1' => nil, '2' => nil})
+ @compiler.topscope.known_resource_types.add klass
+ lambda { @compiler.compile }.should raise_error Puppet::ParseError, "Must pass 2 to Class[Foo]"
+ end
+
+ it "should fail if invalid parameters are passed" do
+ klass = {'foo'=>{'3'=>'one'}}
+ @node.classes = klass
+ klass = Puppet::Resource::Type.new(:hostclass, 'foo', :arguments => {'1' => nil, '2' => nil})
+ @compiler.topscope.known_resource_types.add klass
+ lambda { @compiler.compile }.should raise_error Puppet::ParseError, "Invalid parameter 3"
+ end
+
+ it "should ensure class is in catalog without params" do
+ @node.classes = klasses = {'foo'=>nil}
+ foo = Puppet::Resource::Type.new(:hostclass, 'foo')
+ @compiler.topscope.known_resource_types.add foo
+ catalog = @compiler.compile
+ catalog.classes.should include 'foo'
+ end
+
it "should not evaluate the resources created for found classes unless asked" do
@compiler.catalog.stubs(:tag)
@resource.expects(:evaluate).never
@class.expects(:ensure_in_catalog).returns(@resource)
@scope.stubs(:class_scope).with(@class)
@compiler.evaluate_classes(%w{myclass}, @scope)
end
it "should immediately evaluate the resources created for found classes when asked" do
@compiler.catalog.stubs(:tag)
@resource.expects(:evaluate)
@class.expects(:ensure_in_catalog).returns(@resource)
@scope.stubs(:class_scope).with(@class)
@compiler.evaluate_classes(%w{myclass}, @scope, false)
end
it "should skip classes that have already been evaluated" do
@compiler.catalog.stubs(:tag)
@scope.stubs(:class_scope).with(@class).returns("something")
@compiler.expects(:add_resource).never
@resource.expects(:evaluate).never
Puppet::Parser::Resource.expects(:new).never
@compiler.evaluate_classes(%w{myclass}, @scope, false)
end
it "should skip classes previously evaluated with different capitalization" do
@compiler.catalog.stubs(:tag)
@scope.stubs(:find_hostclass).with("MyClass").returns(@class)
@scope.stubs(:class_scope).with(@class).returns("something")
@compiler.expects(:add_resource).never
@resource.expects(:evaluate).never
Puppet::Parser::Resource.expects(:new).never
@compiler.evaluate_classes(%w{MyClass}, @scope, false)
end
it "should return the list of found classes" do
@compiler.catalog.stubs(:tag)
@compiler.stubs(:add_resource)
@scope.stubs(:find_hostclass).with("notfound").returns(nil)
@scope.stubs(:class_scope).with(@class)
Puppet::Parser::Resource.stubs(:new).returns(@resource)
@class.stubs :ensure_in_catalog
@compiler.evaluate_classes(%w{myclass notfound}, @scope).should == %w{myclass}
end
end
describe "when evaluating AST nodes with no AST nodes present" do
it "should do nothing" do
@compiler.expects(:ast_nodes?).returns(false)
@compiler.known_resource_types.expects(:nodes).never
Puppet::Parser::Resource.expects(:new).never
@compiler.send(:evaluate_ast_node)
end
end
describe "when evaluating AST nodes with AST nodes present" do
before do
@compiler.known_resource_types.stubs(:nodes?).returns true
# Set some names for our test
@node.stubs(:names).returns(%w{a b c})
@compiler.known_resource_types.stubs(:node).with("a").returns(nil)
@compiler.known_resource_types.stubs(:node).with("b").returns(nil)
@compiler.known_resource_types.stubs(:node).with("c").returns(nil)
# It should check this last, of course.
@compiler.known_resource_types.stubs(:node).with("default").returns(nil)
end
it "should fail if the named node cannot be found" do
proc { @compiler.send(:evaluate_ast_node) }.should raise_error(Puppet::ParseError)
end
it "should evaluate the first node class matching the node name" do
node_class = stub 'node', :name => "c", :evaluate_code => nil
@compiler.known_resource_types.stubs(:node).with("c").returns(node_class)
node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil, :type => "node"
node_class.expects(:ensure_in_catalog).returns(node_resource)
@compiler.compile
end
it "should match the default node if no matching node can be found" do
node_class = stub 'node', :name => "default", :evaluate_code => nil
@compiler.known_resource_types.stubs(:node).with("default").returns(node_class)
node_resource = stub 'node resource', :ref => "Node[default]", :evaluate => nil, :type => "node"
node_class.expects(:ensure_in_catalog).returns(node_resource)
@compiler.compile
end
it "should evaluate the node resource immediately rather than using lazy evaluation" do
node_class = stub 'node', :name => "c"
@compiler.known_resource_types.stubs(:node).with("c").returns(node_class)
node_resource = stub 'node resource', :ref => "Node[c]", :type => "node"
node_class.expects(:ensure_in_catalog).returns(node_resource)
node_resource.expects(:evaluate)
@compiler.send(:evaluate_ast_node)
end
it "should set the node's scope as the top scope" do
node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil, :type => "node"
node_class = stub 'node', :name => "c", :ensure_in_catalog => node_resource
@compiler.known_resource_types.stubs(:node).with("c").returns(node_class)
# The #evaluate method normally does this.
scope = stub 'scope', :source => "mysource"
@compiler.topscope.expects(:class_scope).with(node_class).returns(scope)
node_resource.stubs(:evaluate)
@compiler.stubs :create_settings_scope
@compiler.compile
@compiler.topscope.should equal(scope)
end
end
describe "when managing resource overrides" do
before do
@override = stub 'override', :ref => "File[/foo]", :type => "my"
@resource = resource(:file, "/foo")
end
it "should be able to store overrides" do
lambda { @compiler.add_override(@override) }.should_not raise_error
end
it "should apply overrides to the appropriate resources" do
@compiler.add_resource(@scope, @resource)
@resource.expects(:merge).with(@override)
@compiler.add_override(@override)
@compiler.compile
end
it "should accept overrides before the related resource has been created" do
@resource.expects(:merge).with(@override)
# First store the override
@compiler.add_override(@override)
# Then the resource
@compiler.add_resource(@scope, @resource)
# And compile, so they get resolved
@compiler.compile
end
it "should fail if the compile is finished and resource overrides have not been applied" do
@compiler.add_override(@override)
- lambda { @compiler.compile }.should raise_error(Puppet::ParseError)
+ lambda { @compiler.compile }.should raise_error Puppet::ParseError, 'Could not find resource(s) File[/foo] for overriding'
end
end
end
diff --git a/spec/unit/property/keyvalue_spec.rb b/spec/unit/property/keyvalue_spec.rb
index 7666def56..a44d891d7 100644
--- a/spec/unit/property/keyvalue_spec.rb
+++ b/spec/unit/property/keyvalue_spec.rb
@@ -1,168 +1,168 @@
#!/usr/bin/env ruby
Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") }
require 'puppet/property/keyvalue'
klass = Puppet::Property::KeyValue
describe klass do
it "should be a subclass of Property" do
klass.superclass.must == Puppet::Property
end
describe "as an instance" do
before do
# Wow that's a messy interface to the resource.
klass.initvars
@resource = stub 'resource', :[]= => nil, :property => nil
@property = klass.new(:resource => @resource)
end
it "should have a , as default delimiter" do
@property.delimiter.should == ";"
end
it "should have a = as default separator" do
@property.separator.should == "="
end
it "should have a :membership as default membership" do
@property.membership.should == :key_value_membership
end
it "should return the same value passed into should_to_s" do
@property.should_to_s({:foo => "baz", :bar => "boo"}) == "foo=baz;bar=boo"
end
it "should return the passed in array values joined with the delimiter from is_to_s" do
@property.is_to_s({"foo" => "baz" , "bar" => "boo"}).should == "foo=baz;bar=boo"
end
describe "when calling inclusive?" do
it "should use the membership method to look up on the @resource" do
@property.expects(:membership).returns(:key_value_membership)
@resource.expects(:[]).with(:key_value_membership)
@property.inclusive?
end
it "should return true when @resource[membership] == inclusive" do
@property.stubs(:membership).returns(:key_value_membership)
@resource.stubs(:[]).with(:key_value_membership).returns(:inclusive)
@property.inclusive?.must == true
end
it "should return false when @resource[membership] != inclusive" do
@property.stubs(:membership).returns(:key_value_membership)
@resource.stubs(:[]).with(:key_value_membership).returns(:minimum)
@property.inclusive?.must == false
end
end
describe "when calling process_current_hash" do
it "should return {} if hash is :absent" do
@property.process_current_hash(:absent).must == {}
end
it "should set every key to nil if inclusive?" do
@property.stubs(:inclusive?).returns(true)
@property.process_current_hash({:foo => "bar", :do => "re"}).must == { :foo => nil, :do => nil }
end
it "should return the hash if !inclusive?" do
@property.stubs(:inclusive?).returns(false)
@property.process_current_hash({:foo => "bar", :do => "re"}).must == {:foo => "bar", :do => "re"}
end
end
describe "when calling should" do
it "should return nil if @should is nil" do
@property.should.must == nil
end
it "should call process_current_hash" do
@property.should = ["foo=baz", "bar=boo"]
@property.stubs(:retrieve).returns({:do => "re", :mi => "fa" })
@property.expects(:process_current_hash).returns({})
@property.should
end
it "should return the hashed values of @should and the nilled values of retrieve if inclusive" do
@property.should = ["foo=baz", "bar=boo"]
@property.expects(:retrieve).returns({:do => "re", :mi => "fa" })
@property.expects(:inclusive?).returns(true)
@property.should.must == { :foo => "baz", :bar => "boo", :do => nil, :mi => nil }
end
it "should return the hashed @should + the unique values of retrieve if !inclusive" do
@property.should = ["foo=baz", "bar=boo"]
@property.expects(:retrieve).returns({:foo => "diff", :do => "re", :mi => "fa"})
@property.expects(:inclusive?).returns(false)
@property.should.must == { :foo => "baz", :bar => "boo", :do => "re", :mi => "fa" }
end
end
describe "when calling retrieve" do
before do
@provider = mock("provider")
@property.stubs(:provider).returns(@provider)
end
it "should send 'name' to the provider" do
@provider.expects(:send).with(:keys)
@property.expects(:name).returns(:keys)
@property.retrieve
end
it "should return a hash with the provider returned info" do
@provider.stubs(:send).with(:keys).returns({"do" => "re", "mi" => "fa" })
@property.stubs(:name).returns(:keys)
@property.retrieve == {"do" => "re", "mi" => "fa" }
end
it "should return :absent when the provider returns :absent" do
@provider.stubs(:send).with(:keys).returns(:absent)
@property.stubs(:name).returns(:keys)
@property.retrieve == :absent
end
end
describe "when calling hashify" do
it "should return the array hashified" do
@property.hashify(["foo=baz", "bar=boo"]).must == { :foo => "baz", :bar => "boo" }
end
end
- describe "when calling insync?" do
+ describe "when calling safe_insync?" do
before do
@provider = mock("provider")
@property.stubs(:provider).returns(@provider)
@property.stubs(:name).returns(:prop_name)
end
it "should return true unless @should is defined and not nil" do
- @property.insync?("foo") == true
+ @property.safe_insync?("foo") == true
end
it "should return true if the passed in values is nil" do
@property.should = "foo"
- @property.insync?(nil) == true
+ @property.safe_insync?(nil) == true
end
it "should return true if hashified should value == (retrieved) value passed in" do
@provider.stubs(:prop_name).returns({ :foo => "baz", :bar => "boo" })
@property.should = ["foo=baz", "bar=boo"]
@property.expects(:inclusive?).returns(true)
- @property.insync?({ :foo => "baz", :bar => "boo" }).must == true
+ @property.safe_insync?({ :foo => "baz", :bar => "boo" }).must == true
end
it "should return false if prepared value != should value" do
@provider.stubs(:prop_name).returns({ "foo" => "bee", "bar" => "boo" })
@property.should = ["foo=baz", "bar=boo"]
@property.expects(:inclusive?).returns(true)
- @property.insync?({ "foo" => "bee", "bar" => "boo" }).must == false
+ @property.safe_insync?({ "foo" => "bee", "bar" => "boo" }).must == false
end
end
end
end
diff --git a/spec/unit/property/list_spec.rb b/spec/unit/property/list_spec.rb
index 3e8cc5402..c6c5db10e 100644
--- a/spec/unit/property/list_spec.rb
+++ b/spec/unit/property/list_spec.rb
@@ -1,166 +1,166 @@
#!/usr/bin/env ruby
Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") }
require 'puppet/property/list'
list_class = Puppet::Property::List
describe list_class do
it "should be a subclass of Property" do
list_class.superclass.must == Puppet::Property
end
describe "as an instance" do
before do
# Wow that's a messy interface to the resource.
list_class.initvars
@resource = stub 'resource', :[]= => nil, :property => nil
@property = list_class.new(:resource => @resource)
end
it "should have a , as default delimiter" do
@property.delimiter.should == ","
end
it "should have a :membership as default membership" do
@property.membership.should == :membership
end
it "should return the same value passed into should_to_s" do
@property.should_to_s("foo") == "foo"
end
it "should return the passed in array values joined with the delimiter from is_to_s" do
@property.is_to_s(["foo","bar"]).should == "foo,bar"
end
it "should be able to correctly convert ':absent' to a string" do
@property.is_to_s(:absent).should == "absent"
end
describe "when adding should to current" do
it "should add the arrays when current is an array" do
@property.add_should_with_current(["foo"], ["bar"]).should == ["foo", "bar"]
end
it "should return should if current is not a array" do
@property.add_should_with_current(["foo"], :absent).should == ["foo"]
end
it "should return only the uniq elements" do
@property.add_should_with_current(["foo", "bar"], ["foo", "baz"]).should == ["foo", "bar", "baz"]
end
end
describe "when calling inclusive?" do
it "should use the membership method to look up on the @resource" do
@property.expects(:membership).returns(:membership)
@resource.expects(:[]).with(:membership)
@property.inclusive?
end
it "should return true when @resource[membership] == inclusive" do
@property.stubs(:membership).returns(:membership)
@resource.stubs(:[]).with(:membership).returns(:inclusive)
@property.inclusive?.must == true
end
it "should return false when @resource[membership] != inclusive" do
@property.stubs(:membership).returns(:membership)
@resource.stubs(:[]).with(:membership).returns(:minimum)
@property.inclusive?.must == false
end
end
describe "when calling should" do
it "should return nil if @should is nil" do
@property.should.must == nil
end
it "should return the sorted values of @should as a string if inclusive" do
@property.should = ["foo", "bar"]
@property.expects(:inclusive?).returns(true)
@property.should.must == "bar,foo"
end
it "should return the uniq sorted values of @should + retrieve as a string if !inclusive" do
@property.should = ["foo", "bar"]
@property.expects(:inclusive?).returns(false)
@property.expects(:retrieve).returns(["foo","baz"])
@property.should.must == "bar,baz,foo"
end
end
describe "when calling retrieve" do
before do
@provider = mock("provider")
@property.stubs(:provider).returns(@provider)
end
it "should send 'name' to the provider" do
@provider.expects(:send).with(:group)
@property.expects(:name).returns(:group)
@property.retrieve
end
it "should return an array with the provider returned info" do
@provider.stubs(:send).with(:group).returns("foo,bar,baz")
@property.stubs(:name).returns(:group)
@property.retrieve == ["foo", "bar", "baz"]
end
it "should return :absent when the provider returns :absent" do
@provider.stubs(:send).with(:group).returns(:absent)
@property.stubs(:name).returns(:group)
@property.retrieve == :absent
end
end
- describe "when calling insync?" do
+ describe "when calling safe_insync?" do
it "should return true unless @should is defined and not nil" do
- @property.must be_insync("foo")
+ @property.must be_safe_insync("foo")
end
it "should return true unless the passed in values is not nil" do
@property.should = "foo"
- @property.must be_insync(nil)
+ @property.must be_safe_insync(nil)
end
it "should call prepare_is_for_comparison with value passed in and should" do
@property.should = "foo"
@property.expects(:prepare_is_for_comparison).with("bar")
@property.expects(:should)
- @property.insync?("bar")
+ @property.safe_insync?("bar")
end
it "should return true if 'is' value is array of comma delimited should values" do
@property.should = "bar,foo"
@property.expects(:inclusive?).returns(true)
- @property.must be_insync(["bar","foo"])
+ @property.must be_safe_insync(["bar","foo"])
end
it "should return true if 'is' value is :absent and should value is empty string" do
@property.should = ""
@property.expects(:inclusive?).returns(true)
- @property.must be_insync([])
+ @property.must be_safe_insync([])
end
it "should return false if prepared value != should value" do
@property.should = "bar,baz,foo"
@property.expects(:inclusive?).returns(true)
- @property.must_not be_insync(["bar","foo"])
+ @property.must_not be_safe_insync(["bar","foo"])
end
end
describe "when calling dearrayify" do
it "should sort and join the array with 'delimiter'" do
array = mock "array"
array.expects(:sort).returns(array)
array.expects(:join).with(@property.delimiter)
@property.dearrayify(array)
end
end
end
end
diff --git a/spec/unit/provider/mount_spec.rb b/spec/unit/provider/mount_spec.rb
index b034214ee..f567a4a40 100755
--- a/spec/unit/provider/mount_spec.rb
+++ b/spec/unit/provider/mount_spec.rb
@@ -1,137 +1,145 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
require 'puppet/provider/mount'
describe Puppet::Provider::Mount do
before :each do
@mounter = Object.new
@mounter.extend(Puppet::Provider::Mount)
@name = "/"
@resource = stub 'resource'
@resource.stubs(:[]).with(:name).returns(@name)
@mounter.stubs(:resource).returns(@resource)
end
describe Puppet::Provider::Mount, " when mounting" do
it "should use the 'mountcmd' method to mount" do
@mounter.stubs(:options).returns(nil)
@mounter.expects(:mountcmd)
@mounter.mount
end
it "should flush before mounting if a flush method exists" do
@mounter.meta_def(:flush) { }
@mounter.expects(:flush)
@mounter.stubs(:mountcmd)
@mounter.stubs(:options).returns(nil)
@mounter.mount
end
it "should add the options following '-o' if they exist and are not set to :absent" do
@mounter.stubs(:options).returns("ro")
@mounter.expects(:mountcmd).with { |*ary| ary[0] == "-o" and ary[1] == "ro" }
@mounter.mount
end
it "should specify the filesystem name to the mount command" do
@mounter.stubs(:options).returns(nil)
@mounter.expects(:mountcmd).with { |*ary| ary[-1] == @name }
@mounter.mount
end
end
describe Puppet::Provider::Mount, " when remounting" do
it "should use '-o remount' if the resource specifies it supports remounting" do
@mounter.stubs(:info)
@resource.stubs(:[]).with(:remounts).returns(:true)
@mounter.expects(:mountcmd).with("-o", "remount", @name)
@mounter.remount
end
it "should unmount and mount if the resource does not specify it supports remounting" do
@mounter.stubs(:info)
@resource.stubs(:[]).with(:remounts).returns(false)
@mounter.expects(:unmount)
@mounter.expects(:mount)
@mounter.remount
end
it "should log that it is remounting" do
@resource.stubs(:[]).with(:remounts).returns(:true)
@mounter.stubs(:mountcmd)
@mounter.expects(:info).with("Remounting")
@mounter.remount
end
end
describe Puppet::Provider::Mount, " when unmounting" do
it "should call the :umount command with the resource name" do
@mounter.expects(:umount).with(@name)
@mounter.unmount
end
end
describe Puppet::Provider::Mount, " when determining if it is mounted" do
it "should parse the results of running the mount command with no arguments" do
Facter.stubs(:value).returns("whatever")
@mounter.expects(:mountcmd).returns("")
@mounter.mounted?
end
it "should match ' on /private/var/automount' if the operating system is Darwin" do
Facter.stubs(:value).with("operatingsystem").returns("Darwin")
@mounter.expects(:mountcmd).returns("/dev/whatever on /private/var/automount/\ndevfs on /dev")
@mounter.should be_mounted
end
it "should match ' on ' if the operating system is Darwin" do
Facter.stubs(:value).with("operatingsystem").returns("Darwin")
@mounter.expects(:mountcmd).returns("/dev/disk03 on / (local, journaled)\ndevfs on /dev")
@mounter.should be_mounted
end
it "should match '^ on' if the operating system is Solaris" do
Facter.stubs(:value).with("operatingsystem").returns("Solaris")
@mounter.expects(:mountcmd).returns("/ on /dev/dsk/whatever\n/var on /dev/dsk/other")
@mounter.should be_mounted
end
it "should match '^ on' if the operating system is HP-UX" do
Facter.stubs(:value).with("operatingsystem").returns("HP-UX")
@mounter.expects(:mountcmd).returns("/ on /dev/dsk/whatever\n/var on /dev/dsk/other")
@mounter.should be_mounted
end
+ it "should match mounted devices if the operating system is AIX" do
+ Facter.stubs(:value).with("operatingsystem").returns("AIX")
+ mount_data = File.read(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'unit', 'provider', 'mount', 'mount-output.aix.txt'))
+ @mounter.expects(:mountcmd).returns(mount_data)
+
+ @mounter.should be_mounted
+ end
+
it "should match ' on ' if the operating system is not Darwin, Solaris, or HP-UX" do
Facter.stubs(:value).with("operatingsystem").returns("Debian")
@mounter.expects(:mountcmd).returns("/dev/dsk/whatever on / and stuff\n/dev/other/disk on /var and stuff")
@mounter.should be_mounted
end
it "should not be considered mounted if it did not match the mount output" do
Facter.stubs(:value).with("operatingsystem").returns("Debian")
@mounter.expects(:mountcmd).returns("/dev/dsk/whatever on /something/else and stuff\n/dev/other/disk on /var and stuff")
@mounter.should_not be_mounted
end
end
end
diff --git a/spec/unit/provider/nameservice/directoryservice_spec.rb b/spec/unit/provider/nameservice/directoryservice_spec.rb
new file mode 100755
index 000000000..661899db9
--- /dev/null
+++ b/spec/unit/provider/nameservice/directoryservice_spec.rb
@@ -0,0 +1,38 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+# We use this as a reasonable way to obtain all the support infrastructure.
+[:user, :group].each do |type_for_this_round|
+ provider_class = Puppet::Type.type(type_for_this_round).provider(:directoryservice)
+
+ describe provider_class do
+ before do
+ @resource = stub("resource")
+ @provider = provider_class.new(@resource)
+ end
+
+ it "[#6009] should handle nested arrays of members" do
+ current = ["foo", "bar", "baz"]
+ desired = ["foo", ["quux"], "qorp"]
+ group = 'example'
+
+ @resource.stubs(:[]).with(:name).returns(group)
+ @resource.stubs(:[]).with(:auth_membership).returns(true)
+ @provider.instance_variable_set(:@property_value_cache_hash,
+ { :members => current })
+
+ %w{bar baz}.each do |del|
+ @provider.expects(:execute).once.
+ with([:dseditgroup, '-o', 'edit', '-n', '.', '-d', del, group])
+ end
+
+ %w{quux qorp}.each do |add|
+ @provider.expects(:execute).once.
+ with([:dseditgroup, '-o', 'edit', '-n', '.', '-a', add, group])
+ end
+
+ expect { @provider.set(:members, desired) }.should_not raise_error
+ end
+ end
+end
diff --git a/spec/unit/provider/package/freebsd_spec.rb b/spec/unit/provider/package/freebsd_spec.rb
new file mode 100755
index 000000000..0d38a16cf
--- /dev/null
+++ b/spec/unit/provider/package/freebsd_spec.rb
@@ -0,0 +1,55 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+provider_class = Puppet::Type.type(:package).provider(:freebsd)
+
+describe provider_class do
+ before :each do
+ # Create a mock resource
+ @resource = stub 'resource'
+
+ # A catch all; no parameters set
+ @resource.stubs(:[]).returns(nil)
+
+ # But set name and source
+ @resource.stubs(:[]).with(:name).returns "mypackage"
+ @resource.stubs(:[]).with(:ensure).returns :installed
+
+ @provider = provider_class.new
+ @provider.resource = @resource
+ end
+
+ it "should have an install method" do
+ @provider = provider_class.new
+ @provider.should respond_to(:install)
+ end
+
+ describe "when installing" do
+ before :each do
+ @resource.stubs(:should).with(:ensure).returns(:installed)
+ end
+
+ it "should install a package from a path to a directory" do
+ # For better or worse, trailing '/' is needed. --daniel 2011-01-26
+ path = '/path/to/directory/'
+ @resource.stubs(:[]).with(:source).returns(path)
+ Puppet::Util::Execution.expects(:withenv).once.with({:PKG_PATH => path}).yields
+ @provider.expects(:pkgadd).once.with("mypackage")
+
+ expect { @provider.install }.should_not raise_error
+ end
+
+ %w{http https ftp}.each do |protocol|
+ it "should install a package via #{protocol}" do
+ # For better or worse, trailing '/' is needed. --daniel 2011-01-26
+ path = "#{protocol}://localhost/"
+ @resource.stubs(:[]).with(:source).returns(path)
+ Puppet::Util::Execution.expects(:withenv).once.with({:PACKAGESITE => path}).yields
+ @provider.expects(:pkgadd).once.with('-r', "mypackage")
+
+ expect { @provider.install }.should_not raise_error
+ end
+ end
+ end
+end
diff --git a/spec/unit/resource/type_spec.rb b/spec/unit/resource/type_spec.rb
index 7b240bb82..87b4ab420 100755
--- a/spec/unit/resource/type_spec.rb
+++ b/spec/unit/resource/type_spec.rb
@@ -1,740 +1,786 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
require 'puppet/resource/type'
describe Puppet::Resource::Type do
it "should have a 'name' attribute" do
Puppet::Resource::Type.new(:hostclass, "foo").name.should == "foo"
end
[:code, :doc, :line, :file, :resource_type_collection, :ruby_code].each do |attr|
it "should have a '#{attr}' attribute" do
type = Puppet::Resource::Type.new(:hostclass, "foo")
type.send(attr.to_s + "=", "yay")
type.send(attr).should == "yay"
end
end
[:hostclass, :node, :definition].each do |type|
it "should know when it is a #{type}" do
Puppet::Resource::Type.new(type, "foo").send("#{type}?").should be_true
end
end
it "should indirect 'resource_type'" do
Puppet::Resource::Type.indirection.name.should == :resource_type
end
it "should default to 'parser' for its terminus class" do
Puppet::Resource::Type.indirection.terminus_class.should == :parser
end
describe "when converting to json" do
before do
@type = Puppet::Resource::Type.new(:hostclass, "foo")
end
def from_json(json)
Puppet::Resource::Type.from_pson(json)
end
def double_convert
Puppet::Resource::Type.from_pson(PSON.parse(@type.to_pson))
end
it "should include the name and type" do
double_convert.name.should == @type.name
double_convert.type.should == @type.type
end
it "should include any arguments" do
@type.set_arguments("one" => nil, "two" => "foo")
double_convert.arguments.should == {"one" => nil, "two" => "foo"}
end
it "should include any extra attributes" do
@type.file = "/my/file"
@type.line = 50
double_convert.file.should == "/my/file"
double_convert.line.should == 50
end
end
describe "when a node" do
it "should allow a regex as its name" do
lambda { Puppet::Resource::Type.new(:node, /foo/) }.should_not raise_error
end
it "should allow a AST::HostName instance as its name" do
regex = Puppet::Parser::AST::Regex.new(:value => /foo/)
name = Puppet::Parser::AST::HostName.new(:value => regex)
lambda { Puppet::Resource::Type.new(:node, name) }.should_not raise_error
end
it "should match against the regexp in the AST::HostName when a HostName instance is provided" do
regex = Puppet::Parser::AST::Regex.new(:value => /\w/)
name = Puppet::Parser::AST::HostName.new(:value => regex)
node = Puppet::Resource::Type.new(:node, name)
node.match("foo").should be_true
end
it "should return the value of the hostname if provided a string-form AST::HostName instance as the name" do
name = Puppet::Parser::AST::HostName.new(:value => "foo")
node = Puppet::Resource::Type.new(:node, name)
node.name.should == "foo"
end
describe "and the name is a regex" do
it "should have a method that indicates that this is the case" do
Puppet::Resource::Type.new(:node, /w/).should be_name_is_regex
end
it "should set its namespace to ''" do
Puppet::Resource::Type.new(:node, /w/).namespace.should == ""
end
it "should return the regex converted to a string when asked for its name" do
Puppet::Resource::Type.new(:node, /ww/).name.should == "ww"
end
it "should downcase the regex when returning the name as a string" do
Puppet::Resource::Type.new(:node, /W/).name.should == "w"
end
it "should remove non-alpha characters when returning the name as a string" do
Puppet::Resource::Type.new(:node, /w*w/).name.should_not include("*")
end
it "should remove leading dots when returning the name as a string" do
Puppet::Resource::Type.new(:node, /.ww/).name.should_not =~ /^\./
end
it "should have a method for matching its regex name against a provided name" do
Puppet::Resource::Type.new(:node, /.ww/).should respond_to(:match)
end
it "should return true when its regex matches the provided name" do
Puppet::Resource::Type.new(:node, /\w/).match("foo").should be_true
end
it "should return false when its regex does not match the provided name" do
(!!Puppet::Resource::Type.new(:node, /\d/).match("foo")).should be_false
end
it "should return true when its name, as a string, is matched against an equal string" do
Puppet::Resource::Type.new(:node, "foo").match("foo").should be_true
end
it "should return false when its name is matched against an unequal string" do
Puppet::Resource::Type.new(:node, "foo").match("bar").should be_false
end
it "should match names insensitive to case" do
Puppet::Resource::Type.new(:node, "fOo").match("foO").should be_true
end
end
it "should return the name converted to a string when the name is not a regex" do
pending "Need to define LoadedCode behaviour first"
name = Puppet::Parser::AST::HostName.new(:value => "foo")
Puppet::Resource::Type.new(:node, name).name.should == "foo"
end
it "should return the name converted to a string when the name is a regex" do
pending "Need to define LoadedCode behaviour first"
name = Puppet::Parser::AST::HostName.new(:value => /regex/)
Puppet::Resource::Type.new(:node, name).name.should == /regex/.to_s
end
it "should mark any created scopes as a node scope" do
pending "Need to define LoadedCode behaviour first"
name = Puppet::Parser::AST::HostName.new(:value => /regex/)
Puppet::Resource::Type.new(:node, name).name.should == /regex/.to_s
end
end
describe "when initializing" do
it "should require a resource super type" do
Puppet::Resource::Type.new(:hostclass, "foo").type.should == :hostclass
end
it "should fail if provided an invalid resource super type" do
lambda { Puppet::Resource::Type.new(:nope, "foo") }.should raise_error(ArgumentError)
end
it "should set its name to the downcased, stringified provided name" do
Puppet::Resource::Type.new(:hostclass, "Foo::Bar".intern).name.should == "foo::bar"
end
it "should set its namespace to the downcased, stringified qualified name for classes" do
Puppet::Resource::Type.new(:hostclass, "Foo::Bar::Baz".intern).namespace.should == "foo::bar::baz"
end
[:definition, :node].each do |type|
it "should set its namespace to the downcased, stringified qualified portion of the name for #{type}s" do
Puppet::Resource::Type.new(type, "Foo::Bar::Baz".intern).namespace.should == "foo::bar"
end
end
%w{code line file doc}.each do |arg|
it "should set #{arg} if provided" do
type = Puppet::Resource::Type.new(:hostclass, "foo", arg.to_sym => "something")
type.send(arg).should == "something"
end
end
it "should set any provided arguments with the keys as symbols" do
type = Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {:foo => "bar", :baz => "biz"})
type.should be_valid_parameter("foo")
type.should be_valid_parameter("baz")
end
it "should set any provided arguments with they keys as strings" do
type = Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {"foo" => "bar", "baz" => "biz"})
type.should be_valid_parameter(:foo)
type.should be_valid_parameter(:baz)
end
it "should function if provided no arguments" do
type = Puppet::Resource::Type.new(:hostclass, "foo")
type.should_not be_valid_parameter(:foo)
end
end
describe "when testing the validity of an attribute" do
it "should return true if the parameter was typed at initialization" do
Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {"foo" => "bar"}).should be_valid_parameter("foo")
end
it "should return true if it is a metaparam" do
Puppet::Resource::Type.new(:hostclass, "foo").should be_valid_parameter("require")
end
it "should return true if the parameter is named 'name'" do
Puppet::Resource::Type.new(:hostclass, "foo").should be_valid_parameter("name")
end
it "should return false if it is not a metaparam and was not provided at initialization" do
Puppet::Resource::Type.new(:hostclass, "foo").should_not be_valid_parameter("yayness")
end
end
describe "when creating a subscope" do
before do
@scope = stub 'scope', :newscope => nil
@resource = stub 'resource'
@type = Puppet::Resource::Type.new(:hostclass, "foo")
end
it "should return a new scope created with the provided scope as the parent" do
@scope.expects(:newscope).returns "foo"
@type.subscope(@scope, @resource).should == "foo"
end
it "should set the source as itself" do
@scope.expects(:newscope).with { |args| args[:source] == @type }
@type.subscope(@scope, @resource)
end
it "should set the scope's namespace to its namespace" do
@type.expects(:namespace).returns "yayness"
@scope.expects(:newscope).with { |args| args[:namespace] == "yayness" }
@type.subscope(@scope, @resource)
end
it "should set the scope's resource to the provided resource" do
@scope.expects(:newscope).with { |args| args[:resource] == @resource }
@type.subscope(@scope, @resource)
end
end
describe "when setting its parameters in the scope" do
before do
@scope = Puppet::Parser::Scope.new(:compiler => stub("compiler", :environment => Puppet::Node::Environment.new), :source => stub("source"))
@resource = Puppet::Parser::Resource.new(:foo, "bar", :scope => @scope)
@type = Puppet::Resource::Type.new(:hostclass, "foo")
end
+ ['module_name', 'name', 'title'].each do |variable|
+ it "should allow #{variable} to be evaluated as param default" do
+ @type.module_name = "bar"
+ var = Puppet::Parser::AST::Variable.new({'value' => variable})
+ @type.set_arguments :foo => var
+ @type.set_resource_parameters(@resource, @scope)
+ @scope.lookupvar('foo').should == 'bar'
+ end
+ end
+
+ # this test is to clarify a crazy edge case
+ # if you specify these special names as params, the resource
+ # will override the special variables
+ it "resource should override defaults" do
+ @type.set_arguments :name => nil
+ @resource[:name] = 'foobar'
+ var = Puppet::Parser::AST::Variable.new({'value' => 'name'})
+ @type.set_arguments :foo => var
+ @type.set_resource_parameters(@resource, @scope)
+ @scope.lookupvar('foo').should == 'foobar'
+ end
+
it "should set each of the resource's parameters as variables in the scope" do
@type.set_arguments :foo => nil, :boo => nil
@resource[:foo] = "bar"
@resource[:boo] = "baz"
@type.set_resource_parameters(@resource, @scope)
@scope.lookupvar("foo").should == "bar"
@scope.lookupvar("boo").should == "baz"
end
it "should set the variables as strings" do
@type.set_arguments :foo => nil
@resource[:foo] = "bar"
@type.set_resource_parameters(@resource, @scope)
@scope.lookupvar("foo").should == "bar"
end
it "should fail if any of the resource's parameters are not valid attributes" do
@type.set_arguments :foo => nil
@resource[:boo] = "baz"
lambda { @type.set_resource_parameters(@resource, @scope) }.should raise_error(Puppet::ParseError)
end
it "should evaluate and set its default values as variables for parameters not provided by the resource" do
@type.set_arguments :foo => stub("value", :safeevaluate => "something")
@type.set_resource_parameters(@resource, @scope)
@scope.lookupvar("foo").should == "something"
end
it "should set all default values as parameters in the resource" do
@type.set_arguments :foo => stub("value", :safeevaluate => "something")
@type.set_resource_parameters(@resource, @scope)
@resource[:foo].should == "something"
end
it "should fail if the resource does not provide a value for a required argument" do
@type.set_arguments :foo => nil
@resource.expects(:to_hash).returns({})
lambda { @type.set_resource_parameters(@resource, @scope) }.should raise_error(Puppet::ParseError)
end
it "should set the resource's title as a variable if not otherwise provided" do
@type.set_resource_parameters(@resource, @scope)
@scope.lookupvar("title").should == "bar"
end
it "should set the resource's name as a variable if not otherwise provided" do
@type.set_resource_parameters(@resource, @scope)
@scope.lookupvar("name").should == "bar"
end
it "should set its module name in the scope if available" do
@type.module_name = "mymod"
@type.set_resource_parameters(@resource, @scope)
@scope.lookupvar("module_name").should == "mymod"
end
it "should set its caller module name in the scope if available" do
@scope.expects(:parent_module_name).returns "mycaller"
@type.set_resource_parameters(@resource, @scope)
@scope.lookupvar("caller_module_name").should == "mycaller"
end
end
describe "when describing and managing parent classes" do
before do
@code = Puppet::Resource::TypeCollection.new("env")
@parent = Puppet::Resource::Type.new(:hostclass, "bar")
@code.add @parent
@child = Puppet::Resource::Type.new(:hostclass, "foo", :parent => "bar")
@code.add @child
@env = stub "environment", :known_resource_types => @code
@scope = stub "scope", :environment => @env, :namespaces => [""]
end
it "should be able to define a parent" do
Puppet::Resource::Type.new(:hostclass, "foo", :parent => "bar")
end
it "should use the code collection to find the parent resource type" do
@child.parent_type(@scope).should equal(@parent)
end
it "should be able to find parent nodes" do
parent = Puppet::Resource::Type.new(:node, "bar")
@code.add parent
child = Puppet::Resource::Type.new(:node, "foo", :parent => "bar")
@code.add child
child.parent_type(@scope).should equal(parent)
end
it "should cache a reference to the parent type" do
@code.stubs(:hostclass).with("foo::bar").returns nil
@code.expects(:hostclass).with("bar").once.returns @parent
@child.parent_type(@scope)
@child.parent_type
end
it "should correctly state when it is another type's child" do
@child.parent_type(@scope)
@child.should be_child_of(@parent)
end
it "should be considered the child of a parent's parent" do
@grandchild = Puppet::Resource::Type.new(:hostclass, "baz", :parent => "foo")
@code.add @grandchild
@child.parent_type(@scope)
@grandchild.parent_type(@scope)
@grandchild.should be_child_of(@parent)
end
it "should correctly state when it is not another type's child" do
@notchild = Puppet::Resource::Type.new(:hostclass, "baz")
@code.add @notchild
@notchild.should_not be_child_of(@parent)
end
end
describe "when evaluating its code" do
before do
@compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode"))
@scope = Puppet::Parser::Scope.new :compiler => @compiler
@resource = Puppet::Parser::Resource.new(:foo, "yay", :scope => @scope)
# This is so the internal resource lookup works, yo.
@compiler.catalog.add_resource @resource
@known_resource_types = stub 'known_resource_types'
@resource.stubs(:known_resource_types).returns @known_resource_types
@type = Puppet::Resource::Type.new(:hostclass, "foo")
end
it "should add hostclass names to the classes list" do
@type.evaluate_code(@resource)
@compiler.catalog.classes.should be_include("foo")
end
it "should add node names to the classes list" do
@type = Puppet::Resource::Type.new(:node, "foo")
@type.evaluate_code(@resource)
@compiler.catalog.classes.should be_include("foo")
end
it "should not add defined resource names to the classes list" do
@type = Puppet::Resource::Type.new(:definition, "foo")
@type.evaluate_code(@resource)
@compiler.catalog.classes.should_not be_include("foo")
end
it "should set all of its parameters in a subscope" do
subscope = stub 'subscope', :compiler => @compiler
@type.expects(:subscope).with(@scope, @resource).returns subscope
@type.expects(:set_resource_parameters).with(@resource, subscope)
@type.evaluate_code(@resource)
end
it "should not create a subscope for the :main class" do
@resource.stubs(:title).returns(:main)
@type.expects(:subscope).never
@type.expects(:set_resource_parameters).with(@resource, @scope)
@type.evaluate_code(@resource)
end
it "should store the class scope" do
@type.evaluate_code(@resource)
@scope.class_scope(@type).should be_instance_of(@scope.class)
end
it "should still create a scope but not store it if the type is a definition" do
@type = Puppet::Resource::Type.new(:definition, "foo")
@type.evaluate_code(@resource)
@scope.class_scope(@type).should be_nil
end
it "should evaluate the AST code if any is provided" do
code = stub 'code'
@type.stubs(:code).returns code
@type.stubs(:subscope).returns stub_everything("subscope", :compiler => @compiler)
code.expects(:safeevaluate).with @type.subscope
@type.evaluate_code(@resource)
end
describe "and ruby code is provided" do
it "should create a DSL Resource API and evaluate it" do
@type.stubs(:ruby_code).returns(proc { "foo" })
@api = stub 'api'
Puppet::DSL::ResourceAPI.expects(:new).with { |res, scope, code| code == @type.ruby_code }.returns @api
@api.expects(:evaluate)
@type.evaluate_code(@resource)
end
end
it "should noop if there is no code" do
@type.expects(:code).returns nil
@type.evaluate_code(@resource)
end
describe "and it has a parent class" do
before do
@parent_type = Puppet::Resource::Type.new(:hostclass, "parent")
@type.parent = "parent"
@parent_resource = Puppet::Parser::Resource.new(:class, "parent", :scope => @scope)
@compiler.add_resource @scope, @parent_resource
@type.resource_type_collection = @scope.known_resource_types
@type.resource_type_collection.add @parent_type
end
it "should evaluate the parent's resource" do
@type.parent_type(@scope)
-
+
@type.evaluate_code(@resource)
@scope.class_scope(@parent_type).should_not be_nil
end
it "should not evaluate the parent's resource if it has already been evaluated" do
@parent_resource.evaluate
-
+
@type.parent_type(@scope)
@parent_resource.expects(:evaluate).never
@type.evaluate_code(@resource)
end
it "should use the parent's scope as its base scope" do
@type.parent_type(@scope)
@type.evaluate_code(@resource)
@scope.class_scope(@type).parent.object_id.should == @scope.class_scope(@parent_type).object_id
end
end
describe "and it has a parent node" do
before do
@type = Puppet::Resource::Type.new(:node, "foo")
@parent_type = Puppet::Resource::Type.new(:node, "parent")
@type.parent = "parent"
@parent_resource = Puppet::Parser::Resource.new(:node, "parent", :scope => @scope)
@compiler.add_resource @scope, @parent_resource
@type.resource_type_collection = @scope.known_resource_types
@type.resource_type_collection.stubs(:node).with("parent").returns(@parent_type)
@type.resource_type_collection.stubs(:node).with("Parent").returns(@parent_type)
end
it "should evaluate the parent's resource" do
@type.parent_type(@scope)
@type.evaluate_code(@resource)
@scope.class_scope(@parent_type).should_not be_nil
end
it "should not evaluate the parent's resource if it has already been evaluated" do
@parent_resource.evaluate
-
+
@type.parent_type(@scope)
@parent_resource.expects(:evaluate).never
@type.evaluate_code(@resource)
end
it "should use the parent's scope as its base scope" do
@type.parent_type(@scope)
@type.evaluate_code(@resource)
@scope.class_scope(@type).parent.object_id.should == @scope.class_scope(@parent_type).object_id
end
end
end
describe "when creating a resource" do
before do
@node = Puppet::Node.new("foo", :environment => 'env')
@compiler = Puppet::Parser::Compiler.new(@node)
@scope = Puppet::Parser::Scope.new(:compiler => @compiler)
@top = Puppet::Resource::Type.new :hostclass, "top"
@middle = Puppet::Resource::Type.new :hostclass, "middle", :parent => "top"
@code = Puppet::Resource::TypeCollection.new("env")
@code.add @top
@code.add @middle
-
+
@node.environment.stubs(:known_resource_types).returns(@code)
end
it "should create a resource instance" do
@top.ensure_in_catalog(@scope).should be_instance_of(Puppet::Parser::Resource)
end
it "should set its resource type to 'class' when it is a hostclass" do
Puppet::Resource::Type.new(:hostclass, "top").ensure_in_catalog(@scope).type.should == "Class"
end
it "should set its resource type to 'node' when it is a node" do
Puppet::Resource::Type.new(:node, "top").ensure_in_catalog(@scope).type.should == "Node"
end
it "should fail when it is a definition" do
lambda { Puppet::Resource::Type.new(:definition, "top").ensure_in_catalog(@scope) }.should raise_error(ArgumentError)
end
it "should add the created resource to the scope's catalog" do
@top.ensure_in_catalog(@scope)
@compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource)
end
+ it "should add specified parameters to the resource" do
+ @top.ensure_in_catalog(@scope, {'one'=>'1', 'two'=>'2'})
+ @compiler.catalog.resource(:class, "top")['one'].should == '1'
+ @compiler.catalog.resource(:class, "top")['two'].should == '2'
+ end
+
+ it "should not require params for a param class" do
+ @top.ensure_in_catalog(@scope, {})
+ @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource)
+ end
+
it "should evaluate the parent class if one exists" do
@middle.ensure_in_catalog(@scope)
@compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource)
end
+ it "should evaluate the parent class if one exists" do
+ @middle.ensure_in_catalog(@scope, {})
+
+ @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource)
+ end
+
+ it "should fail if you try to create duplicate class resources" do
+ othertop = Puppet::Parser::Resource.new(:class, 'top',:source => @source, :scope => @scope )
+ # add the same class resource to the catalog
+ @compiler.catalog.add_resource(othertop)
+ lambda { @top.ensure_in_catalog(@scope, {}) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError)
+ end
+
it "should fail to evaluate if a parent class is defined but cannot be found" do
othertop = Puppet::Resource::Type.new :hostclass, "something", :parent => "yay"
@code.add othertop
lambda { othertop.ensure_in_catalog(@scope) }.should raise_error(Puppet::ParseError)
end
it "should not create a new resource if one already exists" do
@compiler.catalog.expects(:resource).with(:class, "top").returns("something")
@compiler.catalog.expects(:add_resource).never
@top.ensure_in_catalog(@scope)
end
it "should return the existing resource when not creating a new one" do
@compiler.catalog.expects(:resource).with(:class, "top").returns("something")
@compiler.catalog.expects(:add_resource).never
@top.ensure_in_catalog(@scope).should == "something"
end
it "should not create a new parent resource if one already exists and it has a parent class" do
@top.ensure_in_catalog(@scope)
top_resource = @compiler.catalog.resource(:class, "top")
@middle.ensure_in_catalog(@scope)
@compiler.catalog.resource(:class, "top").should equal(top_resource)
end
# #795 - tag before evaluation.
it "should tag the catalog with the resource tags when it is evaluated" do
@middle.ensure_in_catalog(@scope)
@compiler.catalog.should be_tagged("middle")
end
it "should tag the catalog with the parent class tags when it is evaluated" do
@middle.ensure_in_catalog(@scope)
@compiler.catalog.should be_tagged("top")
end
end
describe "when merging code from another instance" do
def code(str)
Puppet::Parser::AST::Leaf.new :value => str
end
it "should fail unless it is a class" do
lambda { Puppet::Resource::Type.new(:node, "bar").merge("foo") }.should raise_error(Puppet::Error)
end
it "should fail unless the source instance is a class" do
dest = Puppet::Resource::Type.new(:hostclass, "bar")
source = Puppet::Resource::Type.new(:node, "foo")
lambda { dest.merge(source) }.should raise_error(Puppet::Error)
end
it "should fail if both classes have different parent classes" do
code = Puppet::Resource::TypeCollection.new("env")
{"a" => "b", "c" => "d"}.each do |parent, child|
code.add Puppet::Resource::Type.new(:hostclass, parent)
code.add Puppet::Resource::Type.new(:hostclass, child, :parent => parent)
end
lambda { code.hostclass("b").merge(code.hostclass("d")) }.should raise_error(Puppet::Error)
end
it "should fail if it's named 'main' and 'freeze_main' is enabled" do
Puppet.settings[:freeze_main] = true
code = Puppet::Resource::TypeCollection.new("env")
code.add Puppet::Resource::Type.new(:hostclass, "")
other = Puppet::Resource::Type.new(:hostclass, "")
lambda { code.hostclass("").merge(other) }.should raise_error(Puppet::Error)
end
it "should copy the other class's parent if it has not parent" do
dest = Puppet::Resource::Type.new(:hostclass, "bar")
parent = Puppet::Resource::Type.new(:hostclass, "parent")
source = Puppet::Resource::Type.new(:hostclass, "foo", :parent => "parent")
dest.merge(source)
dest.parent.should == "parent"
end
it "should copy the other class's documentation as its docs if it has no docs" do
dest = Puppet::Resource::Type.new(:hostclass, "bar")
source = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "yayness")
dest.merge(source)
dest.doc.should == "yayness"
end
it "should append the other class's docs to its docs if it has any" do
dest = Puppet::Resource::Type.new(:hostclass, "bar", :doc => "fooness")
source = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "yayness")
dest.merge(source)
dest.doc.should == "foonessyayness"
end
it "should turn its code into an ASTArray if necessary" do
dest = Puppet::Resource::Type.new(:hostclass, "bar", :code => code("foo"))
source = Puppet::Resource::Type.new(:hostclass, "foo", :code => code("bar"))
dest.merge(source)
dest.code.should be_instance_of(Puppet::Parser::AST::ASTArray)
end
it "should set the other class's code as its code if it has none" do
dest = Puppet::Resource::Type.new(:hostclass, "bar")
source = Puppet::Resource::Type.new(:hostclass, "foo", :code => code("bar"))
dest.merge(source)
dest.code.value.should == "bar"
end
it "should append the other class's code to its code if it has any" do
dcode = Puppet::Parser::AST::ASTArray.new :children => [code("dest")]
dest = Puppet::Resource::Type.new(:hostclass, "bar", :code => dcode)
scode = Puppet::Parser::AST::ASTArray.new :children => [code("source")]
source = Puppet::Resource::Type.new(:hostclass, "foo", :code => scode)
dest.merge(source)
dest.code.children.collect { |l| l.value }.should == %w{dest source}
end
end
end
diff --git a/spec/unit/type/file/content_spec.rb b/spec/unit/type/file/content_spec.rb
index cde643fc8..9178c94bf 100755
--- a/spec/unit/type/file/content_spec.rb
+++ b/spec/unit/type/file/content_spec.rb
@@ -1,461 +1,514 @@
#!/usr/bin/env ruby
Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") }
content = Puppet::Type.type(:file).attrclass(:content)
describe content do
before do
@resource = Puppet::Type.type(:file).new :path => "/foo/bar"
content.stubs(:standalone?).returns(false)
end
it "should be a subclass of Property" do
content.superclass.must == Puppet::Property
end
describe "when determining the checksum type" do
it "should use the type specified in the source checksum if a source is set" do
@resource[:source] = "/foo"
@resource.parameter(:source).expects(:checksum).returns "{md5lite}eh"
@content = content.new(:resource => @resource)
@content.checksum_type.should == :md5lite
end
it "should use the type specified by the checksum parameter if no source is set" do
@resource[:checksum] = :md5lite
@content = content.new(:resource => @resource)
@content.checksum_type.should == :md5lite
end
end
describe "when determining the actual content to write" do
it "should use the set content if available" do
@content = content.new(:resource => @resource)
@content.should = "ehness"
@content.actual_content.should == "ehness"
end
it "should not use the content from the source if the source is set" do
source = mock 'source'
@resource.expects(:parameter).never.with(:source).returns source
@content = content.new(:resource => @resource)
@content.actual_content.should be_nil
end
end
describe "when setting the desired content" do
it "should make the actual content available via an attribute" do
@content = content.new(:resource => @resource)
@content.stubs(:checksum_type).returns "md5"
@content.should = "this is some content"
@content.actual_content.should == "this is some content"
end
it "should store the checksum as the desired content" do
@content = content.new(:resource => @resource)
digest = Digest::MD5.hexdigest("this is some content")
@content.stubs(:checksum_type).returns "md5"
@content.should = "this is some content"
@content.should.must == "{md5}#{digest}"
end
it "should not checksum 'absent'" do
@content = content.new(:resource => @resource)
@content.should = :absent
@content.should.must == :absent
end
it "should accept a checksum as the desired content" do
@content = content.new(:resource => @resource)
digest = Digest::MD5.hexdigest("this is some content")
string = "{md5}#{digest}"
@content.should = string
@content.should.must == string
end
end
describe "when retrieving the current content" do
it "should return :absent if the file does not exist" do
@content = content.new(:resource => @resource)
@resource.expects(:stat).returns nil
@content.retrieve.should == :absent
end
it "should not manage content on directories" do
@content = content.new(:resource => @resource)
stat = mock 'stat', :ftype => "directory"
@resource.expects(:stat).returns stat
@content.retrieve.should be_nil
end
it "should not manage content on links" do
@content = content.new(:resource => @resource)
stat = mock 'stat', :ftype => "link"
@resource.expects(:stat).returns stat
@content.retrieve.should be_nil
end
it "should always return the checksum as a string" do
@content = content.new(:resource => @resource)
@resource[:checksum] = :mtime
stat = mock 'stat', :ftype => "file"
@resource.expects(:stat).returns stat
time = Time.now
@resource.parameter(:checksum).expects(:mtime_file).with(@resource[:path]).returns time
@content.retrieve.should == "{mtime}#{time}"
end
it "should return the checksum of the file if it exists and is a normal file" do
@content = content.new(:resource => @resource)
stat = mock 'stat', :ftype => "file"
@resource.expects(:stat).returns stat
@resource.parameter(:checksum).expects(:md5_file).with(@resource[:path]).returns "mysum"
@content.retrieve.should == "{md5}mysum"
end
end
describe "when testing whether the content is in sync" do
before do
@resource[:ensure] = :file
@content = content.new(:resource => @resource)
end
it "should return true if the resource shouldn't be a regular file" do
@resource.expects(:should_be_file?).returns false
- @content.must be_insync("whatever")
+ @content.should = "foo"
+ @content.must be_safe_insync("whatever")
end
it "should return false if the current content is :absent" do
- @content.should_not be_insync(:absent)
+ @content.should = "foo"
+ @content.should_not be_safe_insync(:absent)
end
it "should return false if the file should be a file but is not present" do
@resource.expects(:should_be_file?).returns true
+ @content.should = "foo"
- @content.should_not be_insync(:absent)
+ @content.should_not be_safe_insync(:absent)
end
describe "and the file exists" do
before do
@resource.stubs(:stat).returns mock("stat")
end
it "should return false if the current contents are different from the desired content" do
@content.should = "some content"
- @content.should_not be_insync("other content")
+ @content.should_not be_safe_insync("other content")
end
it "should return true if the sum for the current contents is the same as the sum for the desired content" do
@content.should = "some content"
- @content.must be_insync("{md5}" + Digest::MD5.hexdigest("some content"))
+ @content.must be_safe_insync("{md5}" + Digest::MD5.hexdigest("some content"))
end
describe "and Puppet[:show_diff] is set" do
before do
Puppet[:show_diff] = true
end
it "should display a diff if the current contents are different from the desired content" do
@content.should = "some content"
@content.expects(:diff).returns("my diff").once
@content.expects(:print).with("my diff").once
- @content.insync?("other content")
+ @content.safe_insync?("other content")
end
it "should not display a diff if the sum for the current contents is the same as the sum for the desired content" do
@content.should = "some content"
@content.expects(:diff).never
- @content.insync?("{md5}" + Digest::MD5.hexdigest("some content"))
+ @content.safe_insync?("{md5}" + Digest::MD5.hexdigest("some content"))
end
end
end
describe "and :replace is false" do
before do
@resource.stubs(:replace?).returns false
end
it "should be insync if the file exists and the content is different" do
@resource.stubs(:stat).returns mock('stat')
- @content.must be_insync("whatever")
+ @content.must be_safe_insync("whatever")
end
it "should be insync if the file exists and the content is right" do
@resource.stubs(:stat).returns mock('stat')
- @content.must be_insync("something")
+ @content.must be_safe_insync("something")
end
it "should not be insync if the file does not exist" do
- @content.should_not be_insync(:absent)
+ @content.should = "foo"
+ @content.should_not be_safe_insync(:absent)
end
end
end
describe "when changing the content" do
before do
@content = content.new(:resource => @resource)
@content.should = "some content"
@resource.stubs(:[]).with(:path).returns "/boo"
@resource.stubs(:stat).returns "eh"
end
it "should use the file's :write method to write the content" do
@resource.expects(:write).with(:content)
@content.sync
end
it "should return :file_changed if the file already existed" do
@resource.expects(:stat).returns "something"
@resource.stubs(:write)
@content.sync.should == :file_changed
end
it "should return :file_created if the file did not exist" do
@resource.expects(:stat).returns nil
@resource.stubs(:write)
@content.sync.should == :file_created
end
end
describe "when writing" do
before do
@content = content.new(:resource => @resource)
@fh = stub_everything
end
it "should attempt to read from the filebucket if no actual content nor source exists" do
@content.should = "{md5}foo"
@content.resource.bucket.class.any_instance.stubs(:getfile).returns "foo"
@content.write(@fh)
end
describe "from actual content" do
before(:each) do
@content.stubs(:actual_content).returns("this is content")
end
it "should write to the given file handle" do
@fh.expects(:print).with("this is content")
@content.write(@fh)
end
it "should return the current checksum value" do
@resource.parameter(:checksum).expects(:sum_stream).returns "checksum"
@content.write(@fh).should == "checksum"
end
end
describe "from a file bucket" do
it "should fail if a file bucket cannot be retrieved" do
@content.should = "{md5}foo"
@content.resource.expects(:bucket).returns nil
lambda { @content.write(@fh) }.should raise_error(Puppet::Error)
end
it "should fail if the file bucket cannot find any content" do
@content.should = "{md5}foo"
bucket = stub 'bucket'
@content.resource.expects(:bucket).returns bucket
bucket.expects(:getfile).with("foo").raises "foobar"
lambda { @content.write(@fh) }.should raise_error(Puppet::Error)
end
it "should write the returned content to the file" do
@content.should = "{md5}foo"
bucket = stub 'bucket'
@content.resource.expects(:bucket).returns bucket
bucket.expects(:getfile).with("foo").returns "mycontent"
@fh.expects(:print).with("mycontent")
@content.write(@fh)
end
end
describe "from local source" do
before(:each) do
@content.stubs(:actual_content).returns(nil)
@source = stub_everything 'source', :local? => true, :full_path => "/path/to/source"
@resource.stubs(:parameter).with(:source).returns @source
@sum = stub_everything 'sum'
@resource.stubs(:parameter).with(:checksum).returns(@sum)
@digest = stub_everything 'digest'
@sum.stubs(:sum_stream).yields(@digest)
@file = stub_everything 'file'
File.stubs(:open).yields(@file)
@file.stubs(:read).with(8192).returns("chunk1").then.returns("chunk2").then.returns(nil)
end
it "should open the local file" do
File.expects(:open).with("/path/to/source", "r")
@content.write(@fh)
end
it "should read the local file by chunks" do
@file.expects(:read).with(8192).returns("chunk1").then.returns(nil)
@content.write(@fh)
end
it "should write each chunk to the file" do
@fh.expects(:print).with("chunk1").then.with("chunk2")
@content.write(@fh)
end
it "should pass each chunk to the current sum stream" do
@digest.expects(:<<).with("chunk1").then.with("chunk2")
@content.write(@fh)
end
it "should return the checksum computed" do
@sum.stubs(:sum_stream).yields(@digest).returns("checksum")
@content.write(@fh).should == "checksum"
end
end
describe "from remote source" do
before(:each) do
@response = stub_everything 'mock response', :code => "404"
@conn = stub_everything 'connection'
@conn.stubs(:request_get).yields(@response)
Puppet::Network::HttpPool.stubs(:http_instance).returns @conn
@content.stubs(:actual_content).returns(nil)
@source = stub_everything 'source', :local? => false, :full_path => "/path/to/source", :server => "server", :port => 1234
@resource.stubs(:parameter).with(:source).returns @source
@sum = stub_everything 'sum'
@resource.stubs(:parameter).with(:checksum).returns(@sum)
@digest = stub_everything 'digest'
@sum.stubs(:sum_stream).yields(@digest)
end
it "should open a network connection to source server and port" do
Puppet::Network::HttpPool.expects(:http_instance).with("server", 1234).returns @conn
@content.write(@fh)
end
it "should send the correct indirection uri" do
@conn.expects(:request_get).with { |uri,headers| uri == "/production/file_content/path/to/source" }.yields(@response)
@content.write(@fh)
end
it "should return nil if source is not found" do
@response.expects(:code).returns("404")
@content.write(@fh).should == nil
end
it "should not write anything if source is not found" do
@response.expects(:code).returns("404")
@fh.expects(:print).never
@content.write(@fh).should == nil
end
it "should raise an HTTP error in case of server error" do
@response.expects(:code).returns("500")
lambda { @content.write(@fh) }.should raise_error
end
it "should write content by chunks" do
@response.expects(:code).returns("200")
@response.expects(:read_body).multiple_yields("chunk1","chunk2")
@fh.expects(:print).with("chunk1").then.with("chunk2")
@content.write(@fh)
end
it "should pass each chunk to the current sum stream" do
@response.expects(:code).returns("200")
@response.expects(:read_body).multiple_yields("chunk1","chunk2")
@digest.expects(:<<).with("chunk1").then.with("chunk2")
@content.write(@fh)
end
it "should return the checksum computed" do
@response.expects(:code).returns("200")
@response.expects(:read_body).multiple_yields("chunk1","chunk2")
@sum.expects(:sum_stream).yields(@digest).returns("checksum")
@content.write(@fh).should == "checksum"
end
it "should get the current accept encoding header value" do
@content.expects(:add_accept_encoding)
@content.write(@fh)
end
it "should uncompress body on error" do
@response.expects(:code).returns("500")
@response.expects(:body).returns("compressed body")
@content.expects(:uncompress_body).with(@response).returns("uncompressed")
lambda { @content.write(@fh) }.should raise_error { |e| e.message =~ /uncompressed/ }
end
it "should uncompress chunk by chunk" do
uncompressor = stub_everything 'uncompressor'
@content.expects(:uncompress).with(@response).yields(uncompressor)
@response.expects(:code).returns("200")
@response.expects(:read_body).multiple_yields("chunk1","chunk2")
uncompressor.expects(:uncompress).with("chunk1").then.with("chunk2")
@content.write(@fh)
end
it "should write uncompressed chunks to the file" do
uncompressor = stub_everything 'uncompressor'
@content.expects(:uncompress).with(@response).yields(uncompressor)
@response.expects(:code).returns("200")
@response.expects(:read_body).multiple_yields("chunk1","chunk2")
uncompressor.expects(:uncompress).with("chunk1").returns("uncompressed1")
uncompressor.expects(:uncompress).with("chunk2").returns("uncompressed2")
@fh.expects(:print).with("uncompressed1")
@fh.expects(:print).with("uncompressed2")
@content.write(@fh)
end
it "should pass each uncompressed chunk to the current sum stream" do
uncompressor = stub_everything 'uncompressor'
@content.expects(:uncompress).with(@response).yields(uncompressor)
@response.expects(:code).returns("200")
@response.expects(:read_body).multiple_yields("chunk1","chunk2")
uncompressor.expects(:uncompress).with("chunk1").returns("uncompressed1")
uncompressor.expects(:uncompress).with("chunk2").returns("uncompressed2")
@digest.expects(:<<).with("uncompressed1").then.with("uncompressed2")
@content.write(@fh)
end
end
describe "from a filebucket" do
end
+
+ # These are testing the implementation rather than the desired behaviour; while that bites, there are a whole
+ # pile of other methods in the File type that depend on intimate details of this implementation and vice-versa.
+ # If these blow up, you are gonna have to review the callers to make sure they don't explode! --daniel 2011-02-01
+ describe "each_chunk_from should work" do
+ before do
+ @content = content.new(:resource => @resource)
+ end
+
+ it "when content is a string" do
+ @content.each_chunk_from('i_am_a_string') { |chunk| chunk.should == 'i_am_a_string' }
+ end
+
+ it "when no content, source, but ensure present" do
+ @resource[:ensure] = :present
+ @content.each_chunk_from(nil) { |chunk| chunk.should == '' }
+ end
+
+ it "when no content, source, but ensure file" do
+ @resource[:ensure] = :file
+ @content.each_chunk_from(nil) { |chunk| chunk.should == '' }
+ end
+
+ it "when no content or source" do
+ @content.expects(:read_file_from_filebucket).once.returns('im_a_filebucket')
+ @content.each_chunk_from(nil) { |chunk| chunk.should == 'im_a_filebucket' }
+ end
+
+ it "when running as puppet apply" do
+ @content.class.expects(:standalone?).returns true
+ source_or_content = stubs('source_or_content')
+ source_or_content.expects(:content).once.returns :whoo
+ @content.each_chunk_from(source_or_content) { |chunk| chunk.should == :whoo }
+ end
+
+ it "when running from source with a local file" do
+ source_or_content = stubs('source_or_content')
+ source_or_content.expects(:local?).returns true
+ @content.expects(:chunk_file_from_disk).with(source_or_content).once.yields 'woot'
+ @content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' }
+ end
+
+ it "when running from source with a remote file" do
+ source_or_content = stubs('source_or_content')
+ source_or_content.expects(:local?).returns false
+ @content.expects(:chunk_file_from_source).with(source_or_content).once.yields 'woot'
+ @content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' }
+ end
+ end
end
end
diff --git a/spec/unit/type/file/ensure_spec.rb b/spec/unit/type/file/ensure_spec.rb
index ec53ed85a..dbb3a1053 100755
--- a/spec/unit/type/file/ensure_spec.rb
+++ b/spec/unit/type/file/ensure_spec.rb
@@ -1,84 +1,85 @@
#!/usr/bin/env ruby
Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") }
property = Puppet::Type.type(:file).attrclass(:ensure)
describe property do
before do
# Wow that's a messy interface to the resource.
@resource = stub 'resource', :[] => nil, :[]= => nil, :property => nil, :newattr => nil, :parameter => nil, :replace? => true
@resource.stubs(:[]).returns "foo"
@resource.stubs(:[]).with(:path).returns "/my/file"
@ensure = property.new :resource => @resource
end
it "should be a subclass of Ensure" do
property.superclass.must == Puppet::Property::Ensure
end
describe "when retrieving the current state" do
it "should return :absent if the file does not exist" do
@ensure = property.new(:resource => @resource)
@resource.expects(:stat).returns nil
@ensure.retrieve.should == :absent
end
it "should return the current file type if the file exists" do
@ensure = property.new(:resource => @resource)
stat = mock 'stat', :ftype => "directory"
@resource.expects(:stat).returns stat
@ensure.retrieve.should == :directory
end
end
describe "when testing whether :ensure is in sync" do
before do
@ensure = property.new(:resource => @resource)
@stat = stub 'stat', :ftype => "file"
end
it "should always be in sync if replace is 'false' unless the file is missing" do
+ @ensure.should = :file
@resource.expects(:replace?).returns false
- @ensure.insync?(:link).should be_true
+ @ensure.safe_insync?(:link).should be_true
end
it "should be in sync if :ensure is set to :absent and the file does not exist" do
@ensure.should = :absent
- @ensure.must be_insync(:absent)
+ @ensure.must be_safe_insync(:absent)
end
it "should not be in sync if :ensure is set to :absent and the file exists" do
@ensure.should = :absent
- @ensure.should_not be_insync(:file)
+ @ensure.should_not be_safe_insync(:file)
end
it "should be in sync if a normal file exists and :ensure is set to :present" do
@ensure.should = :present
- @ensure.must be_insync(:file)
+ @ensure.must be_safe_insync(:file)
end
it "should be in sync if a directory exists and :ensure is set to :present" do
@ensure.should = :present
- @ensure.must be_insync(:directory)
+ @ensure.must be_safe_insync(:directory)
end
it "should be in sync if a symlink exists and :ensure is set to :present" do
@ensure.should = :present
- @ensure.must be_insync(:link)
+ @ensure.must be_safe_insync(:link)
end
it "should not be in sync if :ensure is set to :file and a directory exists" do
@ensure.should = :file
- @ensure.should_not be_insync(:directory)
+ @ensure.should_not be_safe_insync(:directory)
end
end
end
diff --git a/spec/unit/type/file/group_spec.rb b/spec/unit/type/file/group_spec.rb
index 2283b57fa..956cd57e7 100755
--- a/spec/unit/type/file/group_spec.rb
+++ b/spec/unit/type/file/group_spec.rb
@@ -1,123 +1,123 @@
#!/usr/bin/env ruby
Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") }
property = Puppet::Type.type(:file).attrclass(:group)
describe property do
before do
@resource = stub 'resource', :line => "foo", :file => "bar"
@resource.stubs(:[]).returns "foo"
@resource.stubs(:[]).with(:path).returns "/my/file"
@group = property.new :resource => @resource
end
it "should have a method for testing whether a group is valid" do
@group.must respond_to(:validgroup?)
end
it "should return the found gid if a group is valid" do
@group.expects(:gid).with("foo").returns 500
@group.validgroup?("foo").should == 500
end
it "should return false if a group is not valid" do
@group.expects(:gid).with("foo").returns nil
@group.validgroup?("foo").should be_false
end
describe "when retrieving the current value" do
it "should return :absent if the file cannot stat" do
@resource.expects(:stat).returns nil
@group.retrieve.should == :absent
end
it "should get the gid from the stat instance from the file" do
stat = stub 'stat', :ftype => "foo"
@resource.expects(:stat).returns stat
stat.expects(:gid).returns 500
@group.retrieve.should == 500
end
it "should warn and return :silly if the found value is higher than the maximum uid value" do
Puppet.settings.expects(:value).with(:maximum_uid).returns 500
stat = stub 'stat', :ftype => "foo"
@resource.expects(:stat).returns stat
stat.expects(:gid).returns 1000
@group.expects(:warning)
@group.retrieve.should == :silly
end
end
describe "when determining if the file is in sync" do
it "should directly compare the group values if the desired group is an integer" do
@group.should = [10]
- @group.must be_insync(10)
+ @group.must be_safe_insync(10)
end
it "should treat numeric strings as integers" do
@group.should = ["10"]
- @group.must be_insync(10)
+ @group.must be_safe_insync(10)
end
it "should convert the group name to an integer if the desired group is a string" do
@group.expects(:gid).with("foo").returns 10
@group.should = %w{foo}
- @group.must be_insync(10)
+ @group.must be_safe_insync(10)
end
it "should not validate that groups exist when a group is specified as an integer" do
@group.expects(:gid).never
@group.validgroup?(10)
end
it "should fail if it cannot convert a group name to an integer" do
@group.expects(:gid).with("foo").returns nil
@group.should = %w{foo}
- lambda { @group.insync?(10) }.should raise_error(Puppet::Error)
+ lambda { @group.safe_insync?(10) }.should raise_error(Puppet::Error)
end
it "should return false if the groups are not equal" do
@group.should = [10]
- @group.should_not be_insync(20)
+ @group.should_not be_safe_insync(20)
end
end
describe "when changing the group" do
before do
@group.should = %w{one}
@group.stubs(:gid).returns 500
end
it "should chown the file if :links is set to :follow" do
@resource.expects(:[]).with(:links).returns :follow
File.expects(:chown)
@group.sync
end
it "should lchown the file if :links is set to :manage" do
@resource.expects(:[]).with(:links).returns :manage
File.expects(:lchown)
@group.sync
end
it "should use the first valid group in its 'should' list" do
@group.should = %w{one two three}
@group.expects(:validgroup?).with("one").returns nil
@group.expects(:validgroup?).with("two").returns 500
@group.expects(:validgroup?).with("three").never
File.expects(:chown).with(nil, 500, "/my/file")
@group.sync
end
end
end
diff --git a/spec/unit/type/file/owner_spec.rb b/spec/unit/type/file/owner_spec.rb
index 8e136a187..bcb8e07d6 100755
--- a/spec/unit/type/file/owner_spec.rb
+++ b/spec/unit/type/file/owner_spec.rb
@@ -1,150 +1,150 @@
#!/usr/bin/env ruby
Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") }
property = Puppet::Type.type(:file).attrclass(:owner)
describe property do
before do
# FIXME: many of these tests exercise the provider rather than `owner`
# and should be moved into provider tests. ~JW
@provider = Puppet::Type.type(:file).provider(:posix).new
@provider.stubs(:uid).with("one").returns(1)
@resource = stub 'resource', :line => "foo", :file => "bar"
@resource.stubs(:[]).returns "foo"
@resource.stubs(:[]).with(:path).returns "/my/file"
@resource.stubs(:provider).returns @provider
@owner = property.new :resource => @resource
end
it "should have a method for testing whether an owner is valid" do
@provider.must respond_to(:validuser?)
end
it "should return the found uid if an owner is valid" do
@provider.expects(:uid).with("foo").returns 500
@provider.validuser?("foo").should == 500
end
it "should return false if an owner is not valid" do
@provider.expects(:uid).with("foo").returns nil
@provider.validuser?("foo").should be_false
end
describe "when retrieving the current value" do
it "should return :absent if the file cannot stat" do
@resource.expects(:stat).returns nil
@owner.retrieve.should == :absent
end
it "should get the uid from the stat instance from the file" do
stat = stub 'stat', :ftype => "foo"
@resource.expects(:stat).returns stat
stat.expects(:uid).returns 500
@owner.retrieve.should == 500
end
it "should warn and return :silly if the found value is higher than the maximum uid value" do
Puppet.settings.expects(:value).with(:maximum_uid).returns 500
stat = stub 'stat', :ftype => "foo"
@resource.expects(:stat).returns stat
stat.expects(:uid).returns 1000
@provider.expects(:warning)
@owner.retrieve.should == :silly
end
end
describe "when determining if the file is in sync" do
describe "and not running as root" do
it "should warn once and return true" do
Puppet.features.expects(:root?).returns false
@provider.expects(:warnonce)
@owner.should = [10]
- @owner.must be_insync(20)
+ @owner.must be_safe_insync(20)
end
end
before do
Puppet.features.stubs(:root?).returns true
end
it "should be in sync if 'should' is not provided" do
- @owner.must be_insync(10)
+ @owner.must be_safe_insync(10)
end
it "should directly compare the owner values if the desired owner is an integer" do
@owner.should = [10]
- @owner.must be_insync(10)
+ @owner.must be_safe_insync(10)
end
it "should treat numeric strings as integers" do
@owner.should = ["10"]
- @owner.must be_insync(10)
+ @owner.must be_safe_insync(10)
end
it "should convert the owner name to an integer if the desired owner is a string" do
@provider.expects(:uid).with("foo").returns 10
@owner.should = %w{foo}
- @owner.must be_insync(10)
+ @owner.must be_safe_insync(10)
end
it "should not validate that users exist when a user is specified as an integer" do
@provider.expects(:uid).never
@provider.validuser?(10)
end
it "should fail if it cannot convert an owner name to an integer" do
@provider.expects(:uid).with("foo").returns nil
@owner.should = %w{foo}
- lambda { @owner.insync?(10) }.should raise_error(Puppet::Error)
+ lambda { @owner.safe_insync?(10) }.should raise_error(Puppet::Error)
end
it "should return false if the owners are not equal" do
@owner.should = [10]
- @owner.should_not be_insync(20)
+ @owner.should_not be_safe_insync(20)
end
end
describe "when changing the owner" do
before do
@owner.should = %w{one}
@owner.stubs(:path).returns "path"
@owner.stubs(:uid).returns 500
end
it "should chown the file if :links is set to :follow" do
@resource.expects(:[]).with(:links).returns :follow
File.expects(:chown)
@owner.sync
end
it "should lchown the file if :links is set to :manage" do
@resource.expects(:[]).with(:links).returns :manage
File.expects(:lchown)
@owner.sync
end
it "should use the first valid owner in its 'should' list" do
@owner.should = %w{one two three}
@provider.expects(:validuser?).with("one").returns nil
@provider.expects(:validuser?).with("two").returns 500
@provider.expects(:validuser?).with("three").never
File.expects(:chown).with(500, nil, "/my/file")
@owner.sync
end
end
end
diff --git a/spec/unit/type/file/selinux_spec.rb b/spec/unit/type/file/selinux_spec.rb
index 1ca59e9e7..043471dec 100644
--- a/spec/unit/type/file/selinux_spec.rb
+++ b/spec/unit/type/file/selinux_spec.rb
@@ -1,83 +1,83 @@
#!/usr/bin/env ruby
Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") }
[:seluser, :selrole, :seltype, :selrange].each do |param|
property = Puppet::Type.type(:file).attrclass(param)
describe property do
before do
@resource = Puppet::Type.type(:file).new :path => "/my/file"
@sel = property.new :resource => @resource
end
it "retrieve on #{param} should return :absent if the file isn't statable" do
@resource.expects(:stat).returns nil
@sel.retrieve.should == :absent
end
it "should retrieve nil for #{param} if there is no SELinux support" do
stat = stub 'stat', :ftype => "foo"
@resource.expects(:stat).returns stat
@sel.expects(:get_selinux_current_context).with("/my/file").returns nil
@sel.retrieve.should be_nil
end
it "should retrieve #{param} if a SELinux context is found with a range" do
stat = stub 'stat', :ftype => "foo"
@resource.expects(:stat).returns stat
@sel.expects(:get_selinux_current_context).with("/my/file").returns "user_u:role_r:type_t:s0"
expectedresult = case param
when :seluser; "user_u"
when :selrole; "role_r"
when :seltype; "type_t"
when :selrange; "s0"
end
@sel.retrieve.should == expectedresult
end
it "should retrieve #{param} if a SELinux context is found without a range" do
stat = stub 'stat', :ftype => "foo"
@resource.expects(:stat).returns stat
@sel.expects(:get_selinux_current_context).with("/my/file").returns "user_u:role_r:type_t"
expectedresult = case param
when :seluser; "user_u"
when :selrole; "role_r"
when :seltype; "type_t"
when :selrange; nil
end
@sel.retrieve.should == expectedresult
end
it "should handle no default gracefully" do
@sel.expects(:get_selinux_default_context).with("/my/file").returns nil
@sel.default.must be_nil
end
it "should be able to detect matchpathcon defaults" do
@sel.stubs(:debug)
@sel.expects(:get_selinux_default_context).with("/my/file").returns "user_u:role_r:type_t:s0"
expectedresult = case param
when :seluser; "user_u"
when :selrole; "role_r"
when :seltype; "type_t"
when :selrange; "s0"
end
@sel.default.must == expectedresult
end
it "should be able to set a new context" do
stat = stub 'stat', :ftype => "foo"
@sel.should = %w{newone}
@sel.expects(:set_selinux_context).with("/my/file", ["newone"], param)
@sel.sync
end
- it "should do nothing for insync? if no SELinux support" do
+ it "should do nothing for safe_insync? if no SELinux support" do
@sel.should = %{newcontext}
@sel.expects(:selinux_support?).returns false
- @sel.insync?("oldcontext").should == true
+ @sel.safe_insync?("oldcontext").should == true
end
end
end
diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb
index 22921d85a..90f3daf09 100755
--- a/spec/unit/type/file_spec.rb
+++ b/spec/unit/type/file_spec.rb
@@ -1,1068 +1,1193 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
describe Puppet::Type.type(:file) do
before do
Puppet.settings.stubs(:use)
@real_posix = Puppet.features.posix?
Puppet.features.stubs("posix?").returns(true)
@path = Tempfile.new("puppetspec")
pathname = @path.path
@path.close!()
@path = pathname
@file = Puppet::Type::File.new(:name => @path)
@catalog = Puppet::Resource::Catalog.new
@file.catalog = @catalog
end
describe "when determining if recursion is enabled" do
it "should default to recursion being disabled" do
@file.should_not be_recurse
end
[true, "true", 10, "inf", "remote"].each do |value|
it "should consider #{value} to enable recursion" do
@file[:recurse] = value
@file.must be_recurse
end
end
[false, "false", 0].each do |value|
it "should consider #{value} to disable recursion" do
@file[:recurse] = value
@file.should_not be_recurse
end
end
end
describe "#write" do
it "should propagate failures encountered when renaming the temporary file" do
File.stubs(:open)
File.expects(:rename).raises ArgumentError
file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet")
file.stubs(:validate_checksum?).returns(false)
property = stub('content_property', :actual_content => "something", :length => "something".length)
file.stubs(:property).with(:content).returns(property)
lambda { file.write(:content) }.should raise_error(Puppet::Error)
end
it "should delegate writing to the content property" do
filehandle = stub_everything 'fh'
File.stubs(:open).yields(filehandle)
File.stubs(:rename)
property = stub('content_property', :actual_content => "something", :length => "something".length)
file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet")
file.stubs(:validate_checksum?).returns(false)
file.stubs(:property).with(:content).returns(property)
property.expects(:write).with(filehandle)
file.write(:content)
end
describe "when validating the checksum" do
before { @file.stubs(:validate_checksum?).returns(true) }
it "should fail if the checksum parameter and content checksums do not match" do
checksum = stub('checksum_parameter', :sum => 'checksum_b', :sum_file => 'checksum_b')
@file.stubs(:parameter).with(:checksum).returns(checksum)
property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a')
@file.stubs(:property).with(:content).returns(property)
lambda { @file.write :NOTUSED }.should raise_error(Puppet::Error)
end
end
describe "when not validating the checksum" do
before { @file.stubs(:validate_checksum?).returns(false) }
it "should not fail if the checksum property and content checksums do not match" do
checksum = stub('checksum_parameter', :sum => 'checksum_b')
@file.stubs(:parameter).with(:checksum).returns(checksum)
property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a')
@file.stubs(:property).with(:content).returns(property)
lambda { @file.write :NOTUSED }.should_not raise_error(Puppet::Error)
end
end
end
it "should have a method for determining if the file is present" do
@file.must respond_to(:exist?)
end
it "should be considered existent if it can be stat'ed" do
@file.expects(:stat).returns mock('stat')
@file.must be_exist
end
it "should be considered nonexistent if it can not be stat'ed" do
@file.expects(:stat).returns nil
@file.must_not be_exist
end
it "should have a method for determining if the file should be a normal file" do
@file.must respond_to(:should_be_file?)
end
it "should be a file if :ensure is set to :file" do
@file[:ensure] = :file
@file.must be_should_be_file
end
it "should be a file if :ensure is set to :present and the file exists as a normal file" do
@file.stubs(:stat).returns(mock('stat', :ftype => "file"))
@file[:ensure] = :present
@file.must be_should_be_file
end
it "should not be a file if :ensure is set to something other than :file" do
@file[:ensure] = :directory
@file.must_not be_should_be_file
end
it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do
@file.stubs(:stat).returns(mock('stat', :ftype => "directory"))
@file[:ensure] = :present
@file.must_not be_should_be_file
end
it "should be a file if :ensure is not set and :content is" do
@file[:content] = "foo"
@file.must be_should_be_file
end
it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do
@file.stubs(:stat).returns(mock("stat", :ftype => "file"))
@file.must be_should_be_file
end
it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do
@file.stubs(:stat).returns(mock("stat", :ftype => "directory"))
@file.must_not be_should_be_file
end
describe "when using POSIX filenames" do
describe "on POSIX systems" do
before do
Puppet.features.stubs(:posix?).returns(true)
Puppet.features.stubs(:microsoft_windows?).returns(false)
end
it "should autorequire its parent directory" do
file = Puppet::Type::File.new(:path => "/foo/bar")
dir = Puppet::Type::File.new(:path => "/foo")
@catalog.add_resource file
@catalog.add_resource dir
reqs = file.autorequire
reqs[0].source.must == dir
reqs[0].target.must == file
end
it "should not autorequire its parent dir if its parent dir is itself" do
file = Puppet::Type::File.new(:path => "/")
@catalog.add_resource file
file.autorequire.should be_empty
end
it "should remove trailing slashes" do
file = Puppet::Type::File.new(:path => "/foo/bar/baz/")
file[:path].should == "/foo/bar/baz"
end
it "should remove double slashes" do
file = Puppet::Type::File.new(:path => "/foo/bar//baz")
file[:path].should == "/foo/bar/baz"
end
it "should remove trailing double slashes" do
file = Puppet::Type::File.new(:path => "/foo/bar/baz//")
file[:path].should == "/foo/bar/baz"
end
it "should leave a single slash alone" do
file = Puppet::Type::File.new(:path => "/")
file[:path].should == "/"
end
+
+ it "should accept a double-slash at the start of the path" do
+ expect {
+ file = Puppet::Type::File.new(:path => "//tmp/xxx")
+ # REVISIT: This should be wrong, later. See the next test.
+ # --daniel 2011-01-31
+ file[:path].should == '/tmp/xxx'
+ }.should_not raise_error
+ end
+
+ # REVISIT: This is pending, because I don't want to try and audit the
+ # entire codebase to make sure we get this right. POSIX treats two (and
+ # exactly two) '/' characters at the start of the path specially.
+ #
+ # See sections 3.2 and 4.11, which allow DomainOS to be all special like
+ # and still have the POSIX branding and all. --daniel 2011-01-31
+ it "should preserve the double-slash at the start of the path"
end
describe "on Microsoft Windows systems" do
before do
Puppet.features.stubs(:posix?).returns(false)
Puppet.features.stubs(:microsoft_windows?).returns(true)
end
it "should refuse to work" do
lambda { Puppet::Type::File.new(:path => "/foo/bar") }.should raise_error(Puppet::Error)
end
end
end
describe "when using Microsoft Windows filenames", :if => Puppet.features.microsoft_windows? do
describe "on Microsoft Windows systems" do
before do
Puppet.features.stubs(:posix?).returns(false)
Puppet.features.stubs(:microsoft_windows?).returns(true)
end
it "should autorequire its parent directory" do
file = Puppet::Type::File.new(:path => "X:/foo/bar")
dir = Puppet::Type::File.new(:path => "X:/foo")
@catalog.add_resource file
@catalog.add_resource dir
reqs = file.autorequire
reqs[0].source.must == dir
reqs[0].target.must == file
end
it "should not autorequire its parent dir if its parent dir is itself" do
file = Puppet::Type::File.new(:path => "X:/")
@catalog.add_resource file
file.autorequire.should be_empty
end
it "should remove trailing slashes" do
file = Puppet::Type::File.new(:path => "X:/foo/bar/baz/")
file[:path].should == "X:/foo/bar/baz"
end
it "should remove double slashes" do
file = Puppet::Type::File.new(:path => "X:/foo/bar//baz")
file[:path].should == "X:/foo/bar/baz"
end
it "should remove trailing double slashes" do
file = Puppet::Type::File.new(:path => "X:/foo/bar/baz//")
file[:path].should == "X:/foo/bar/baz"
end
it "should leave a drive letter with a slash alone" do
file = Puppet::Type::File.new(:path => "X:/")
file[:path].should == "X:/"
end
it "should add a slash to a drive letter" do
file = Puppet::Type::File.new(:path => "X:")
file[:path].should == "X:/"
end
end
describe "on POSIX systems" do
before do
Puppet.features.stubs(:posix?).returns(true)
Puppet.features.stubs(:microsoft_windows?).returns(false)
end
it "should refuse to work" do
lambda { Puppet::Type::File.new(:path => "X:/foo/bar") }.should raise_error(Puppet::Error)
end
end
end
describe "when using UNC filenames" do
describe "on Microsoft Windows systems", :if => Puppet.features.microsoft_windows? do
before do
Puppet.features.stubs(:posix?).returns(false)
Puppet.features.stubs(:microsoft_windows?).returns(true)
end
it "should autorequire its parent directory" do
file = Puppet::Type::File.new(:path => "//server/foo/bar")
dir = Puppet::Type::File.new(:path => "//server/foo")
@catalog.add_resource file
@catalog.add_resource dir
reqs = file.autorequire
reqs[0].source.must == dir
reqs[0].target.must == file
end
it "should not autorequire its parent dir if its parent dir is itself" do
file = Puppet::Type::File.new(:path => "//server/foo")
@catalog.add_resource file
puts file.autorequire
file.autorequire.should be_empty
end
it "should remove trailing slashes" do
file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/")
file[:path].should == "//server/foo/bar/baz"
end
it "should remove double slashes" do
file = Puppet::Type::File.new(:path => "//server/foo/bar//baz")
file[:path].should == "//server/foo/bar/baz"
end
it "should remove trailing double slashes" do
file = Puppet::Type::File.new(:path => "//server/foo/bar/baz//")
file[:path].should == "//server/foo/bar/baz"
end
it "should remove a trailing slash from a sharename" do
file = Puppet::Type::File.new(:path => "//server/foo/")
file[:path].should == "//server/foo"
end
it "should not modify a sharename" do
file = Puppet::Type::File.new(:path => "//server/foo")
file[:path].should == "//server/foo"
end
end
describe "on POSIX systems" do
before do
Puppet.features.stubs(:posix?).returns(true)
Puppet.features.stubs(:microsoft_windows?).returns(false)
end
it "should refuse to work" do
lambda { Puppet::Type::File.new(:path => "X:/foo/bar") }.should raise_error(Puppet::Error)
end
end
end
describe "when initializing" do
it "should set a desired 'ensure' value if none is set and 'content' is set" do
file = Puppet::Type::File.new(:name => "/my/file", :content => "/foo/bar")
file[:ensure].should == :file
end
it "should set a desired 'ensure' value if none is set and 'target' is set" do
file = Puppet::Type::File.new(:name => "/my/file", :target => "/foo/bar")
file[:ensure].should == :symlink
end
end
describe "when validating attributes" do
%w{path checksum backup recurse recurselimit source replace force ignore links purge sourceselect}.each do |attr|
it "should have a '#{attr}' parameter" do
Puppet::Type.type(:file).attrtype(attr.intern).should == :param
end
end
%w{content target ensure owner group mode type}.each do |attr|
it "should have a '#{attr}' property" do
Puppet::Type.type(:file).attrtype(attr.intern).should == :property
end
end
it "should have its 'path' attribute set as its namevar" do
Puppet::Type.type(:file).key_attributes.should == [:path]
end
end
describe "when managing links" do
require 'puppettest/support/assertions'
include PuppetTest
require 'tempfile'
if @real_posix
describe "on POSIX systems" do
before do
@basedir = tempfile
Dir.mkdir(@basedir)
@file = File.join(@basedir, "file")
@link = File.join(@basedir, "link")
File.open(@file, "w", 0644) { |f| f.puts "yayness"; f.flush }
File.symlink(@file, @link)
@resource = Puppet::Type.type(:file).new(
:path => @link,
:mode => "755"
)
@catalog.add_resource @resource
end
after do
remove_tmp_files
end
it "should default to managing the link" do
@catalog.apply
# I convert them to strings so they display correctly if there's an error.
("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0644
end
it "should be able to follow links" do
@resource[:links] = :follow
@catalog.apply
("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0755
end
end
else # @real_posix
# should recode tests using expectations instead of using the filesystem
end
describe "on Microsoft Windows systems" do
before do
Puppet.features.stubs(:posix?).returns(false)
Puppet.features.stubs(:microsoft_windows?).returns(true)
end
it "should refuse to work with links"
end
end
it "should be able to retrieve a stat instance for the file it is managing" do
Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo").should respond_to(:stat)
end
describe "when stat'ing its file" do
before do
@resource = Puppet::Type.type(:file).new(:path => "/foo/bar")
@resource[:links] = :manage # so we always use :lstat
end
it "should use :stat if it is following links" do
@resource[:links] = :follow
File.expects(:stat)
@resource.stat
end
it "should use :lstat if is it not following links" do
@resource[:links] = :manage
File.expects(:lstat)
@resource.stat
end
it "should stat the path of the file" do
File.expects(:lstat).with("/foo/bar")
@resource.stat
end
# This only happens in testing.
it "should return nil if the stat does not exist" do
File.expects(:lstat).returns nil
@resource.stat.should be_nil
end
it "should return nil if the file does not exist" do
File.expects(:lstat).raises(Errno::ENOENT)
@resource.stat.should be_nil
end
it "should return nil if the file cannot be stat'ed" do
File.expects(:lstat).raises(Errno::EACCES)
@resource.stat.should be_nil
end
it "should return the stat instance" do
File.expects(:lstat).returns "mystat"
@resource.stat.should == "mystat"
end
it "should cache the stat instance if it has a catalog and is applying" do
stat = mock 'stat'
File.expects(:lstat).returns stat
catalog = Puppet::Resource::Catalog.new
@resource.catalog = catalog
catalog.stubs(:applying?).returns true
@resource.stat.should equal(@resource.stat)
end
end
describe "when flushing" do
it "should flush all properties that respond to :flush" do
@resource = Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo")
@resource.parameter(:source).expects(:flush)
@resource.flush
end
it "should reset its stat reference" do
@resource = Puppet::Type.type(:file).new(:path => "/foo/bar")
File.expects(:lstat).times(2).returns("stat1").then.returns("stat2")
@resource.stat.should == "stat1"
@resource.flush
@resource.stat.should == "stat2"
end
end
it "should have a method for performing recursion" do
@file.must respond_to(:perform_recursion)
end
describe "when executing a recursive search" do
it "should use Metadata to do its recursion" do
Puppet::FileServing::Metadata.expects(:search)
@file.perform_recursion(@file[:path])
end
it "should use the provided path as the key to the search" do
Puppet::FileServing::Metadata.expects(:search).with { |key, options| key == "/foo" }
@file.perform_recursion("/foo")
end
it "should return the results of the metadata search" do
Puppet::FileServing::Metadata.expects(:search).returns "foobar"
@file.perform_recursion(@file[:path]).should == "foobar"
end
it "should pass its recursion value to the search" do
@file[:recurse] = true
Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurse] == true }
@file.perform_recursion(@file[:path])
end
it "should pass true if recursion is remote" do
@file[:recurse] = :remote
Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurse] == true }
@file.perform_recursion(@file[:path])
end
it "should pass its recursion limit value to the search" do
@file[:recurselimit] = 10
Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurselimit] == 10 }
@file.perform_recursion(@file[:path])
end
it "should configure the search to ignore or manage links" do
@file[:links] = :manage
Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:links] == :manage }
@file.perform_recursion(@file[:path])
end
it "should pass its 'ignore' setting to the search if it has one" do
@file[:ignore] = %w{.svn CVS}
Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} }
@file.perform_recursion(@file[:path])
end
end
it "should have a method for performing local recursion" do
@file.must respond_to(:recurse_local)
end
describe "when doing local recursion" do
before do
@metadata = stub 'metadata', :relative_path => "my/file"
end
it "should pass its path to the :perform_recursion method" do
@file.expects(:perform_recursion).with(@file[:path]).returns [@metadata]
@file.stubs(:newchild)
@file.recurse_local
end
it "should return an empty hash if the recursion returns nothing" do
@file.expects(:perform_recursion).returns nil
@file.recurse_local.should == {}
end
it "should create a new child resource with each generated metadata instance's relative path" do
@file.expects(:perform_recursion).returns [@metadata]
@file.expects(:newchild).with(@metadata.relative_path).returns "fiebar"
@file.recurse_local
end
it "should not create a new child resource for the '.' directory" do
@metadata.stubs(:relative_path).returns "."
@file.expects(:perform_recursion).returns [@metadata]
@file.expects(:newchild).never
@file.recurse_local
end
it "should return a hash of the created resources with the relative paths as the hash keys" do
@file.expects(:perform_recursion).returns [@metadata]
@file.expects(:newchild).with("my/file").returns "fiebar"
@file.recurse_local.should == {"my/file" => "fiebar"}
end
it "should set checksum_type to none if this file checksum is none" do
@file[:checksum] = :none
Puppet::FileServing::Metadata.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata]
@file.expects(:newchild).with("my/file").returns "fiebar"
@file.recurse_local
end
end
it "should have a method for performing link recursion" do
@file.must respond_to(:recurse_link)
end
describe "when doing link recursion" do
before do
@first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory"
@second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file"
@resource = stub 'file', :[]= => nil
end
it "should pass its target to the :perform_recursion method" do
@file[:target] = "mylinks"
@file.expects(:perform_recursion).with("mylinks").returns [@first]
@file.stubs(:newchild).returns @resource
@file.recurse_link({})
end
it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do
@first.stubs(:relative_path).returns "."
@file[:target] = "mylinks"
@file.expects(:perform_recursion).with("mylinks").returns [@first]
@file.stubs(:newchild).never
@file.expects(:[]=).with(:ensure, :directory)
@file.recurse_link({})
end
it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do
@file.expects(:perform_recursion).returns [@first, @second]
@file.expects(:newchild).with(@first.relative_path).returns @resource
@file.recurse_link("second" => @resource)
end
it "should not create a new child resource for paths that already exist in the children hash" do
@file.expects(:perform_recursion).returns [@first]
@file.expects(:newchild).never
@file.recurse_link("first" => @resource)
end
it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory" do
file = stub 'file'
file.expects(:[]=).with(:target, "/my/second")
file.expects(:[]=).with(:ensure, :link)
@file.stubs(:perform_recursion).returns [@first, @second]
@file.recurse_link("first" => @resource, "second" => file)
end
it "should :ensure to :directory if the file is a directory" do
file = stub 'file'
file.expects(:[]=).with(:ensure, :directory)
@file.stubs(:perform_recursion).returns [@first, @second]
@file.recurse_link("first" => file, "second" => @resource)
end
it "should return a hash with both created and existing resources with the relative paths as the hash keys" do
file = stub 'file', :[]= => nil
@file.expects(:perform_recursion).returns [@first, @second]
@file.stubs(:newchild).returns file
@file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file}
end
end
it "should have a method for performing remote recursion" do
@file.must respond_to(:recurse_remote)
end
describe "when doing remote recursion" do
before do
@file[:source] = "puppet://foo/bar"
@first = Puppet::FileServing::Metadata.new("/my", :relative_path => "first")
@second = Puppet::FileServing::Metadata.new("/my", :relative_path => "second")
@first.stubs(:ftype).returns "directory"
@second.stubs(:ftype).returns "directory"
@parameter = stub 'property', :metadata= => nil
@resource = stub 'file', :[]= => nil, :parameter => @parameter
end
it "should pass its source to the :perform_recursion method" do
data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar")
@file.expects(:perform_recursion).with("puppet://foo/bar").returns [data]
@file.stubs(:newchild).returns @resource
@file.recurse_remote({})
end
it "should not recurse when the remote file is not a directory" do
data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => ".")
data.stubs(:ftype).returns "file"
@file.expects(:perform_recursion).with("puppet://foo/bar").returns [data]
@file.expects(:newchild).never
@file.recurse_remote({})
end
it "should set the source of each returned file to the searched-for URI plus the found relative path" do
@first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path)
@file.expects(:perform_recursion).returns [@first]
@file.stubs(:newchild).returns @resource
@file.recurse_remote({})
end
it "should create a new resource for any relative file paths that do not already have a resource" do
@file.stubs(:perform_recursion).returns [@first]
@file.expects(:newchild).with("first").returns @resource
@file.recurse_remote({}).should == {"first" => @resource}
end
it "should not create a new resource for any relative file paths that do already have a resource" do
@file.stubs(:perform_recursion).returns [@first]
@file.expects(:newchild).never
@file.recurse_remote("first" => @resource)
end
it "should set the source of each resource to the source of the metadata" do
@file.stubs(:perform_recursion).returns [@first]
@resource.stubs(:[]=)
@resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path))
@file.recurse_remote("first" => @resource)
end
# LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already
# filed, and when it's fixed, we'll just fix the whole flow.
it "should set the checksum type to :md5 if the remote file is a file" do
@first.stubs(:ftype).returns "file"
@file.stubs(:perform_recursion).returns [@first]
@resource.stubs(:[]=)
@resource.expects(:[]=).with(:checksum, :md5)
@file.recurse_remote("first" => @resource)
end
it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do
@file.stubs(:perform_recursion).returns [@first]
@resource.expects(:parameter).with(:source).returns @parameter
@parameter.expects(:metadata=).with(@first)
@file.recurse_remote("first" => @resource)
end
it "should not create a new resource for the '.' file" do
@first.stubs(:relative_path).returns "."
@file.stubs(:perform_recursion).returns [@first]
@file.expects(:newchild).never
@file.recurse_remote({})
end
it "should store the metadata in the main file's source property if the relative path is '.'" do
@first.stubs(:relative_path).returns "."
@file.stubs(:perform_recursion).returns [@first]
@file.parameter(:source).expects(:metadata=).with @first
@file.recurse_remote("first" => @resource)
end
describe "and multiple sources are provided" do
describe "and :sourceselect is set to :first" do
it "should create file instances for the results for the first source to return any values" do
data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar")
@file[:source] = %w{/one /two /three /four}
@file.expects(:perform_recursion).with("/one").returns nil
@file.expects(:perform_recursion).with("/two").returns []
@file.expects(:perform_recursion).with("/three").returns [data]
@file.expects(:perform_recursion).with("/four").never
@file.expects(:newchild).with("foobar").returns @resource
@file.recurse_remote({})
end
end
describe "and :sourceselect is set to :all" do
before do
@file[:sourceselect] = :all
end
it "should return every found file that is not in a previous source" do
klass = Puppet::FileServing::Metadata
@file[:source] = %w{/one /two /three /four}
@file.stubs(:newchild).returns @resource
one = [klass.new("/one", :relative_path => "a")]
@file.expects(:perform_recursion).with("/one").returns one
@file.expects(:newchild).with("a").returns @resource
two = [klass.new("/two", :relative_path => "a"), klass.new("/two", :relative_path => "b")]
@file.expects(:perform_recursion).with("/two").returns two
@file.expects(:newchild).with("b").returns @resource
three = [klass.new("/three", :relative_path => "a"), klass.new("/three", :relative_path => "c")]
@file.expects(:perform_recursion).with("/three").returns three
@file.expects(:newchild).with("c").returns @resource
@file.expects(:perform_recursion).with("/four").returns []
@file.recurse_remote({})
end
end
end
end
+ describe "when specifying both source, and content properties" do
+ before do
+ @file[:source] = '/one'
+ @file[:content] = 'file contents'
+ end
+
+ it "should raise an exception" do
+ lambda {@file.validate }.should raise_error(/You cannot specify more than one of/)
+ end
+ end
+
+ describe "when using source" do
+ before do
+ @file[:source] = '/one'
+ end
+ Puppet::Type::File::ParameterChecksum.value_collection.values.reject {|v| v == :none}.each do |checksum_type|
+ describe "with checksum '#{checksum_type}'" do
+ before do
+ @file[:checksum] = checksum_type
+ end
+
+ it 'should validate' do
+
+ lambda { @file.validate }.should_not raise_error
+ end
+ end
+ end
+
+ describe "with checksum 'none'" do
+ before do
+ @file[:checksum] = :none
+ end
+
+ it 'should raise an exception when validating' do
+ lambda { @file.validate }.should raise_error(/You cannot specify source when using checksum 'none'/)
+ end
+ end
+ end
+
+ describe "when using content" do
+ before do
+ @file[:content] = 'file contents'
+ end
+
+ (Puppet::Type::File::ParameterChecksum.value_collection.values - SOURCE_ONLY_CHECKSUMS).each do |checksum_type|
+ describe "with checksum '#{checksum_type}'" do
+ before do
+ @file[:checksum] = checksum_type
+ end
+
+ it 'should validate' do
+ lambda { @file.validate }.should_not raise_error
+ end
+ end
+ end
+
+ SOURCE_ONLY_CHECKSUMS.each do |checksum_type|
+ describe "with checksum '#{checksum_type}'" do
+ it 'should raise an exception when validating' do
+ @file[:checksum] = checksum_type
+
+ lambda { @file.validate }.should raise_error(/You cannot specify content when using checksum '#{checksum_type}'/)
+ end
+ end
+ end
+ end
+
describe "when returning resources with :eval_generate" do
before do
@graph = stub 'graph', :add_edge => nil
@catalog.stubs(:relationship_graph).returns @graph
@file.catalog = @catalog
@file[:recurse] = true
end
it "should recurse if recursion is enabled" do
resource = stub('resource', :[] => "resource")
@file.expects(:recurse?).returns true
@file.expects(:recurse).returns [resource]
@file.eval_generate.should == [resource]
end
it "should not recurse if recursion is disabled" do
@file.expects(:recurse?).returns false
@file.expects(:recurse).never
@file.eval_generate.should == []
end
it "should return each resource found through recursion" do
foo = stub 'foo', :[] => "/foo"
bar = stub 'bar', :[] => "/bar"
bar2 = stub 'bar2', :[] => "/bar"
@file.expects(:recurse).returns [foo, bar]
@file.eval_generate.should == [foo, bar]
end
end
describe "when recursing" do
before do
@file[:recurse] = true
@metadata = Puppet::FileServing::Metadata
end
describe "and a source is set" do
before { @file[:source] = "/my/source" }
it "should pass the already-discovered resources to recurse_remote" do
@file.stubs(:recurse_local).returns(:foo => "bar")
@file.expects(:recurse_remote).with(:foo => "bar").returns []
@file.recurse
end
end
describe "and a target is set" do
before { @file[:target] = "/link/target" }
it "should use recurse_link" do
@file.stubs(:recurse_local).returns(:foo => "bar")
@file.expects(:recurse_link).with(:foo => "bar").returns []
@file.recurse
end
end
it "should use recurse_local if recurse is not remote" do
@file.expects(:recurse_local).returns({})
@file.recurse
end
it "should not use recurse_local if recurse remote" do
@file[:recurse] = :remote
@file.expects(:recurse_local).never
@file.recurse
end
it "should return the generated resources as an array sorted by file path" do
one = stub 'one', :[] => "/one"
two = stub 'two', :[] => "/one/two"
three = stub 'three', :[] => "/three"
@file.expects(:recurse_local).returns(:one => one, :two => two, :three => three)
@file.recurse.should == [one, two, three]
end
describe "and purging is enabled" do
before do
@file[:purge] = true
end
it "should configure each file to be removed" do
local = stub 'local'
local.stubs(:[]).with(:source).returns nil # Thus, a local file
local.stubs(:[]).with(:path).returns "foo"
@file.expects(:recurse_local).returns("local" => local)
local.expects(:[]=).with(:ensure, :absent)
@file.recurse
end
it "should not remove files that exist in the remote repository" do
@file["source"] = "/my/file"
@file.expects(:recurse_local).returns({})
remote = stub 'remote'
remote.stubs(:[]).with(:source).returns "/whatever" # Thus, a remote file
remote.stubs(:[]).with(:path).returns "foo"
@file.expects(:recurse_remote).with { |hash| hash["remote"] = remote }
remote.expects(:[]=).with(:ensure, :absent).never
@file.recurse
end
end
describe "and making a new child resource" do
it "should not copy the parent resource's parent" do
Puppet::Type.type(:file).expects(:new).with { |options| ! options.include?(:parent) }
@file.newchild("my/path")
end
{:recurse => true, :target => "/foo/bar", :ensure => :present, :alias => "yay", :source => "/foo/bar"}.each do |param, value|
it "should not pass on #{param} to the sub resource" do
@file = Puppet::Type::File.new(:name => @path, param => value, :catalog => @catalog)
@file.class.expects(:new).with { |params| params[param].nil? }
@file.newchild("sub/file")
end
end
it "should copy all of the parent resource's 'should' values that were set at initialization" do
file = @file.class.new(:path => "/foo/bar", :owner => "root", :group => "wheel")
@catalog.add_resource(file)
file.class.expects(:new).with { |options| options[:owner] == "root" and options[:group] == "wheel" }
file.newchild("my/path")
end
it "should not copy default values to the new child" do
@file.class.expects(:new).with { |params| params[:backup].nil? }
@file.newchild("my/path")
end
it "should not copy values to the child which were set by the source" do
@file[:source] = "/foo/bar"
metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever"
@file.parameter(:source).stubs(:metadata).returns metadata
@file.parameter(:source).copy_source_values
@file.class.expects(:new).with { |params| params[:group].nil? }
@file.newchild("my/path")
end
end
end
describe "when setting the backup" do
it "should default to 'puppet'" do
Puppet::Type::File.new(:name => "/my/file")[:backup].should == "puppet"
end
it "should allow setting backup to 'false'" do
(!Puppet::Type::File.new(:name => "/my/file", :backup => false)[:backup]).should be_true
end
it "should set the backup to '.puppet-bak' if it is set to true" do
Puppet::Type::File.new(:name => "/my/file", :backup => true)[:backup].should == ".puppet-bak"
end
it "should support any other backup extension" do
Puppet::Type::File.new(:name => "/my/file", :backup => ".bak")[:backup].should == ".bak"
end
it "should set the filebucket when backup is set to a string matching the name of a filebucket in the catalog" do
catalog = Puppet::Resource::Catalog.new
bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket"
catalog.add_resource bucket_resource
file = Puppet::Type::File.new(:name => "/my/file")
catalog.add_resource file
file[:backup] = "foo"
file.bucket.should == bucket_resource.bucket
end
it "should find filebuckets added to the catalog after the file resource was created" do
catalog = Puppet::Resource::Catalog.new
file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo")
catalog.add_resource file
bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket"
catalog.add_resource bucket_resource
file.bucket.should == bucket_resource.bucket
end
it "should have a nil filebucket if backup is false" do
catalog = Puppet::Resource::Catalog.new
bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket"
catalog.add_resource bucket_resource
file = Puppet::Type::File.new(:name => "/my/file", :backup => false)
catalog.add_resource file
file.bucket.should be_nil
end
it "should have a nil filebucket if backup is set to a string starting with '.'" do
catalog = Puppet::Resource::Catalog.new
bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket"
catalog.add_resource bucket_resource
file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo")
catalog.add_resource file
file.bucket.should be_nil
end
it "should fail if there's no catalog and backup is not false" do
file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo")
lambda { file.bucket }.should raise_error(Puppet::Error)
end
it "should fail if a non-existent catalog is specified" do
file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo")
catalog = Puppet::Resource::Catalog.new
catalog.add_resource file
lambda { file.bucket }.should raise_error(Puppet::Error)
end
it "should be able to use the default filebucket without a catalog" do
file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet")
file.bucket.should be_instance_of(Puppet::FileBucket::Dipper)
end
it "should look up the filebucket during finish()" do
file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo")
file.expects(:bucket)
file.finish
end
end
describe "when retrieving the current file state" do
it "should copy the source values if the 'source' parameter is set" do
file = Puppet::Type::File.new(:name => "/my/file", :source => "/foo/bar")
file.parameter(:source).expects(:copy_source_values)
file.retrieve
end
end
describe ".title_patterns" do
before do
@type_class = Puppet::Type.type(:file)
end
it "should have a regexp that captures the entire string, except for a terminating slash" do
patterns = @type_class.title_patterns
string = "abc/\n\tdef/"
patterns[0][0] =~ string
$1.should == "abc/\n\tdef"
end
end
+ describe "when auditing" do
+ it "should not fail if creating a new file if group is not set" do
+ File.exists?(@path).should == false
+ file = Puppet::Type::File.new(:name => @path, :audit => "all", :content => "content")
+ catalog = Puppet::Resource::Catalog.new
+ catalog.add_resource(file)
+
+ Puppet::Util::Storage.stubs(:store) # to prevent the catalog from trying to write state.yaml
+ transaction = catalog.apply
+
+ transaction.report.resource_statuses["File[#{@path}]"].failed.should == false
+ File.exists?(@path).should == true
+ end
+
+ it "should not log errors if creating a new file with ensure present and no content" do
+ File.exists?(@path).should == false
+ file = Puppet::Type::File.new(:name => @path, :audit => "content", :ensure => "present")
+ catalog = Puppet::Resource::Catalog.new
+ catalog.add_resource(file)
+
+ Puppet::Util::Storage.stubs(:store) # to prevent the catalog from trying to write state.yaml
+
+ catalog.apply
+ @logs.reject {|l| l.level == :notice }.should be_empty
+ end
+ end
+
+ describe "when specifying both source and checksum" do
+ it 'should use the specified checksum when source is first' do
+ @file[:source] = '/foo'
+ @file[:checksum] = :md5lite
+
+ @file[:checksum].should be :md5lite
+ end
+ it 'should use the specified checksum when source is last' do
+ @file[:checksum] = :md5lite
+ @file[:source] = '/foo'
+
+ @file[:checksum].should be :md5lite
+ end
+ end
end
diff --git a/spec/unit/type/mount_spec.rb b/spec/unit/type/mount_spec.rb
index ce82cb516..0d74042e3 100755
--- a/spec/unit/type/mount_spec.rb
+++ b/spec/unit/type/mount_spec.rb
@@ -1,261 +1,261 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
describe Puppet::Type.type(:mount) do
it "should have a :refreshable feature that requires the :remount method" do
Puppet::Type.type(:mount).provider_feature(:refreshable).methods.should == [:remount]
end
it "should have no default value for :ensure" do
mount = Puppet::Type.type(:mount).new(:name => "yay")
mount.should(:ensure).should be_nil
end
end
describe Puppet::Type.type(:mount), "when validating attributes" do
[:name, :remounts].each do |param|
it "should have a #{param} parameter" do
Puppet::Type.type(:mount).attrtype(param).should == :param
end
end
[:ensure, :device, :blockdevice, :fstype, :options, :pass, :dump, :atboot, :target].each do |param|
it "should have a #{param} property" do
Puppet::Type.type(:mount).attrtype(param).should == :property
end
end
end
describe Puppet::Type.type(:mount)::Ensure, "when validating values" do
before do
@provider = stub 'provider', :class => Puppet::Type.type(:mount).defaultprovider, :clear => nil
Puppet::Type.type(:mount).defaultprovider.expects(:new).returns(@provider)
end
it "should alias :present to :defined as a value to :ensure" do
mount = Puppet::Type.type(:mount).new(:name => "yay", :ensure => :present)
mount.should(:ensure).should == :defined
end
it "should support :unmounted as a value to :ensure" do
mount = Puppet::Type.type(:mount).new(:name => "yay", :ensure => :unmounted)
mount.should(:ensure).should == :unmounted
end
it "should support :absent as a value to :ensure" do
Puppet::Type.type(:mount).new(:name => "yay", :ensure => :absent)
end
it "should support :mounted as a value to :ensure" do
Puppet::Type.type(:mount).new(:name => "yay", :ensure => :mounted)
end
end
describe Puppet::Type.type(:mount)::Ensure do
before :each do
@provider = stub 'provider', :class => Puppet::Type.type(:mount).defaultprovider, :clear => nil, :satisfies? => true, :name => :mock
Puppet::Type.type(:mount).defaultprovider.stubs(:new).returns(@provider)
@mount = Puppet::Type.type(:mount).new(:name => "yay", :check => :ensure)
@ensure = @mount.property(:ensure)
end
def mount_stub(params)
Puppet::Type.type(:mount).validproperties.each do |prop|
unless params[prop]
params[prop] = :absent
@mount[prop] = :absent
end
end
params.each do |param, value|
@provider.stubs(param).returns(value)
end
end
describe Puppet::Type.type(:mount)::Ensure, "when retrieving its current state" do
it "should return the provider's value if it is :absent" do
@provider.expects(:ensure).returns(:absent)
@ensure.retrieve.should == :absent
end
it "should return :mounted if the provider indicates it is mounted and the value is not :absent" do
@provider.expects(:ensure).returns(:present)
@provider.expects(:mounted?).returns(true)
@ensure.retrieve.should == :mounted
end
it "should return :unmounted if the provider indicates it is not mounted and the value is not :absent" do
@provider.expects(:ensure).returns(:present)
@provider.expects(:mounted?).returns(false)
@ensure.retrieve.should == :unmounted
end
end
describe Puppet::Type.type(:mount)::Ensure, "when changing the host" do
it "should destroy itself if it should be absent" do
@provider.stubs(:mounted?).returns(false)
@provider.expects(:destroy)
@ensure.should = :absent
@ensure.sync
end
it "should unmount itself before destroying if it is mounted and should be absent" do
@provider.expects(:mounted?).returns(true)
@provider.expects(:unmount)
@provider.expects(:destroy)
@ensure.should = :absent
@ensure.sync
end
it "should create itself if it is absent and should be defined" do
@provider.stubs(:ensure).returns(:absent)
@provider.stubs(:mounted?).returns(true)
@provider.stubs(:mounted?).returns(false)
@provider.expects(:create)
@ensure.should = :defined
@ensure.sync
end
it "should not unmount itself if it is mounted and should be defined" do
@provider.stubs(:ensure).returns(:mounted)
@provider.stubs(:mounted?).returns(true)
@provider.stubs(:create)
@provider.expects(:mount).never
@provider.expects(:unmount).never
@ensure.should = :defined
@ensure.sync
end
it "should not mount itself if it is unmounted and should be defined" do
@provider.stubs(:ensure).returns(:unmounted)
@provider.stubs(:mounted?).returns(false)
@ensure.stubs(:syncothers)
@provider.stubs(:create)
@provider.expects(:mount).never
@provider.expects(:unmount).never
@ensure.should = :present
@ensure.sync
end
it "should unmount itself if it is mounted and should be unmounted" do
@provider.stubs(:ensure).returns(:present)
@provider.stubs(:mounted?).returns(true)
@ensure.stubs(:syncothers)
@provider.expects(:unmount)
@ensure.should = :unmounted
@ensure.sync
end
it "should create and mount itself if it does not exist and should be mounted" do
@provider.stubs(:ensure).returns(:absent)
@provider.stubs(:mounted?).returns(false)
@provider.expects(:create)
@ensure.stubs(:syncothers)
@provider.expects(:mount)
@ensure.should = :mounted
@ensure.sync
end
it "should mount itself if it is present and should be mounted" do
@provider.stubs(:ensure).returns(:present)
@provider.stubs(:mounted?).returns(false)
@ensure.stubs(:syncothers)
@provider.expects(:mount)
@ensure.should = :mounted
@ensure.sync
end
it "should create but not mount itself if it is absent and mounted and should be mounted" do
@provider.stubs(:ensure).returns(:absent)
@provider.stubs(:mounted?).returns(true)
@ensure.stubs(:syncothers)
@provider.expects(:create)
@ensure.should = :mounted
@ensure.sync
end
it "should be insync if it is mounted and should be defined" do
@ensure.should = :defined
- @ensure.insync?(:mounted).should == true
+ @ensure.safe_insync?(:mounted).should == true
end
it "should be insync if it is unmounted and should be defined" do
@ensure.should = :defined
- @ensure.insync?(:unmounted).should == true
+ @ensure.safe_insync?(:unmounted).should == true
end
it "should be insync if it is mounted and should be present" do
@ensure.should = :present
- @ensure.insync?(:mounted).should == true
+ @ensure.safe_insync?(:mounted).should == true
end
it "should be insync if it is unmounted and should be present" do
@ensure.should = :present
- @ensure.insync?(:unmounted).should == true
+ @ensure.safe_insync?(:unmounted).should == true
end
end
describe Puppet::Type.type(:mount), "when responding to events" do
it "should remount if it is currently mounted" do
@provider.expects(:mounted?).returns(true)
@provider.expects(:remount)
@mount.refresh
end
it "should not remount if it is not currently mounted" do
@provider.expects(:mounted?).returns(false)
@provider.expects(:remount).never
@mount.refresh
end
it "should not remount swap filesystems" do
@mount[:fstype] = "swap"
@provider.expects(:remount).never
@mount.refresh
end
end
end
describe Puppet::Type.type(:mount), "when modifying an existing mount entry" do
before do
@provider = stub 'provider', :class => Puppet::Type.type(:mount).defaultprovider, :clear => nil, :satisfies? => true, :name => :mock, :remount => nil
Puppet::Type.type(:mount).defaultprovider.stubs(:new).returns(@provider)
@mount = Puppet::Type.type(:mount).new(:name => "yay", :ensure => :mounted)
{:device => "/foo/bar", :blockdevice => "/other/bar", :target => "/what/ever", :fstype => 'eh', :options => "", :pass => 0, :dump => 0, :atboot => 0,
:ensure => :mounted}.each do
|param, value|
@mount.provider.stubs(param).returns value
@mount[param] = value
end
@mount.provider.stubs(:mounted?).returns true
# stub this to not try to create state.yaml
Puppet::Util::Storage.stubs(:store)
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource @mount
end
it "should use the provider to change the dump value" do
@mount.provider.expects(:dump).returns 0
@mount.provider.expects(:dump=).with(1)
@mount[:dump] = 1
@catalog.apply
end
end
diff --git a/spec/unit/type/ssh_authorized_key_spec.rb b/spec/unit/type/ssh_authorized_key_spec.rb
index a0b435f80..666616c03 100755
--- a/spec/unit/type/ssh_authorized_key_spec.rb
+++ b/spec/unit/type/ssh_authorized_key_spec.rb
@@ -1,152 +1,152 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
ssh_authorized_key = Puppet::Type.type(:ssh_authorized_key)
describe ssh_authorized_key do
before do
@class = Puppet::Type.type(:ssh_authorized_key)
@provider_class = stub 'provider_class', :name => "fake", :suitable? => true, :supports_parameter? => true
@class.stubs(:defaultprovider).returns(@provider_class)
@class.stubs(:provider).returns(@provider_class)
@provider = stub 'provider', :class => @provider_class, :file_path => "/tmp/whatever", :clear => nil
@provider_class.stubs(:new).returns(@provider)
@catalog = Puppet::Resource::Catalog.new
end
it "should have a name parameter" do
@class.attrtype(:name).should == :param
end
it "should have :name be its namevar" do
@class.key_attributes.should == [:name]
end
it "should have a :provider parameter" do
@class.attrtype(:provider).should == :param
end
it "should have an ensure property" do
@class.attrtype(:ensure).should == :property
end
it "should support :present as a value for :ensure" do
proc { @class.new(:name => "whev", :ensure => :present, :user => "nobody") }.should_not raise_error
end
it "should support :absent as a value for :ensure" do
proc { @class.new(:name => "whev", :ensure => :absent, :user => "nobody") }.should_not raise_error
end
it "should have an type property" do
@class.attrtype(:type).should == :property
end
it "should support ssh-dss as an type value" do
proc { @class.new(:name => "whev", :type => "ssh-dss", :user => "nobody") }.should_not raise_error
end
it "should support ssh-rsa as an type value" do
proc { @class.new(:name => "whev", :type => "ssh-rsa", :user => "nobody") }.should_not raise_error
end
it "should support :dsa as an type value" do
proc { @class.new(:name => "whev", :type => :dsa, :user => "nobody") }.should_not raise_error
end
it "should support :rsa as an type value" do
proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody") }.should_not raise_error
end
it "should not support values other than ssh-dss, ssh-rsa, dsa, rsa in the ssh_authorized_key_type" do
proc { @class.new(:name => "whev", :type => :something) }.should raise_error(Puppet::Error)
end
it "should have an key property" do
@class.attrtype(:key).should == :property
end
it "should have an user property" do
@class.attrtype(:user).should == :property
end
it "should have an options property" do
@class.attrtype(:options).should == :property
end
it "'s options property should return well formed string of arrays from is_to_s" do
resource = @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"])
resource.property(:options).is_to_s(["a","b","c"]).should == "a,b,c"
end
it "'s options property should return well formed string of arrays from is_to_s" do
resource = @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"])
resource.property(:options).should_to_s(["a","b","c"]).should == "a,b,c"
end
it "should have a target property" do
@class.attrtype(:target).should == :property
end
describe "when neither user nor target is specified" do
it "should raise an error" do
proc do
@class.create(
:name => "Test",
:key => "AAA",
:type => "ssh-rsa",
:ensure => :present)
end.should raise_error(Puppet::Error)
end
end
describe "when both target and user are specified" do
it "should use target" do
resource = @class.create(
:name => "Test",
:user => "root",
:target => "/tmp/blah")
resource.should(:target).should == "/tmp/blah"
end
end
describe "when user is specified" do
it "should determine target" do
resource = @class.create(
:name => "Test",
:user => "root")
target = File.expand_path("~root/.ssh/authorized_keys")
resource.should(:target).should == target
end
# Bug #2124 - ssh_authorized_key always changes target if target is not defined
it "should not raise spurious change events" do
resource = @class.new(:name => "Test", :user => "root")
target = File.expand_path("~root/.ssh/authorized_keys")
- resource.property(:target).insync?(target).should == true
+ resource.property(:target).safe_insync?(target).should == true
end
end
describe "when calling validate" do
it "should not crash on a non-existant user" do
resource = @class.create(
:name => "Test",
:user => "ihopesuchuserdoesnotexist")
proc { resource.validate }.should_not raise_error
end
end
end
diff --git a/spec/unit/type/user_spec.rb b/spec/unit/type/user_spec.rb
index ccea9ee4c..297134446 100755
--- a/spec/unit/type/user_spec.rb
+++ b/spec/unit/type/user_spec.rb
@@ -1,304 +1,332 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
user = Puppet::Type.type(:user)
describe user do
before do
ENV["PATH"] += File::PATH_SEPARATOR + "/usr/sbin" unless ENV["PATH"].split(File::PATH_SEPARATOR).include?("/usr/sbin")
@provider = stub 'provider'
@resource = stub 'resource', :resource => nil, :provider => @provider, :line => nil, :file => nil
end
it "should have a default provider inheriting from Puppet::Provider" do
user.defaultprovider.ancestors.should be_include(Puppet::Provider)
end
it "should be able to create a instance" do
user.new(:name => "foo").should_not be_nil
end
it "should have an allows_duplicates feature" do
user.provider_feature(:allows_duplicates).should_not be_nil
end
it "should have an manages_homedir feature" do
user.provider_feature(:manages_homedir).should_not be_nil
end
it "should have an manages_passwords feature" do
user.provider_feature(:manages_passwords).should_not be_nil
end
it "should have a manages_solaris_rbac feature" do
user.provider_feature(:manages_solaris_rbac).should_not be_nil
end
it "should have a manages_expiry feature" do
user.provider_feature(:manages_expiry).should_not be_nil
end
it "should have a manages_password_age feature" do
user.provider_feature(:manages_password_age).should_not be_nil
end
describe "instances" do
it "should have a valid provider" do
user.new(:name => "foo").provider.class.ancestors.should be_include(Puppet::Provider)
end
it "should delegate existence questions to its provider" do
instance = user.new(:name => "foo")
instance.provider.expects(:exists?).returns "eh"
instance.exists?.should == "eh"
end
end
properties = [:ensure, :uid, :gid, :home, :comment, :shell, :password, :password_min_age, :password_max_age, :groups, :roles, :auths, :profiles, :project, :keys, :expiry]
properties.each do |property|
it "should have a #{property} property" do
user.attrclass(property).ancestors.should be_include(Puppet::Property)
end
it "should have documentation for its #{property} property" do
user.attrclass(property).doc.should be_instance_of(String)
end
end
list_properties = [:groups, :roles, :auths]
list_properties.each do |property|
it "should have a list '#{property}'" do
user.attrclass(property).ancestors.should be_include(Puppet::Property::List)
end
end
it "should have an ordered list 'profiles'" do
user.attrclass(:profiles).ancestors.should be_include(Puppet::Property::OrderedList)
end
it "should have key values 'keys'" do
user.attrclass(:keys).ancestors.should be_include(Puppet::Property::KeyValue)
end
describe "when retrieving all current values" do
before do
@user = user.new(:name => "foo", :uid => 10)
end
it "should return a hash containing values for all set properties" do
@user[:gid] = 10
@user.property(:ensure).expects(:retrieve).returns :present
@user.property(:uid).expects(:retrieve).returns 15
@user.property(:gid).expects(:retrieve).returns 15
values = @user.retrieve
[@user.property(:uid), @user.property(:gid)].each { |property| values.should be_include(property) }
end
it "should set all values to :absent if the user is absent" do
@user.property(:ensure).expects(:retrieve).returns :absent
@user.property(:uid).expects(:retrieve).never
@user.retrieve[@user.property(:uid)].should == :absent
end
it "should include the result of retrieving each property's current value if the user is present" do
@user.property(:ensure).expects(:retrieve).returns :present
@user.property(:uid).expects(:retrieve).returns 15
@user.retrieve[@user.property(:uid)].should == 15
end
end
describe "when managing the ensure property" do
before do
@ensure = user.attrclass(:ensure).new(:resource => @resource)
end
it "should support a :present value" do
lambda { @ensure.should = :present }.should_not raise_error
end
it "should support an :absent value" do
lambda { @ensure.should = :absent }.should_not raise_error
end
it "should call :create on the provider when asked to sync to the :present state" do
@provider.expects(:create)
@ensure.should = :present
@ensure.sync
end
it "should call :delete on the provider when asked to sync to the :absent state" do
@provider.expects(:delete)
@ensure.should = :absent
@ensure.sync
end
describe "and determining the current state" do
it "should return :present when the provider indicates the user exists" do
@provider.expects(:exists?).returns true
@ensure.retrieve.should == :present
end
it "should return :absent when the provider indicates the user does not exist" do
@provider.expects(:exists?).returns false
@ensure.retrieve.should == :absent
end
end
end
describe "when managing the uid property" do
it "should convert number-looking strings into actual numbers" do
uid = user.attrclass(:uid).new(:resource => @resource)
uid.should = "50"
uid.should.must == 50
end
it "should support UIDs as numbers" do
uid = user.attrclass(:uid).new(:resource => @resource)
uid.should = 50
uid.should.must == 50
end
it "should :absent as a value" do
uid = user.attrclass(:uid).new(:resource => @resource)
uid.should = :absent
uid.should.must == :absent
end
end
describe "when managing the gid" do
it "should :absent as a value" do
gid = user.attrclass(:gid).new(:resource => @resource)
gid.should = :absent
gid.should.must == :absent
end
it "should convert number-looking strings into actual numbers" do
gid = user.attrclass(:gid).new(:resource => @resource)
gid.should = "50"
gid.should.must == 50
end
it "should support GIDs specified as integers" do
gid = user.attrclass(:gid).new(:resource => @resource)
gid.should = 50
gid.should.must == 50
end
it "should support groups specified by name" do
gid = user.attrclass(:gid).new(:resource => @resource)
gid.should = "foo"
gid.should.must == "foo"
end
describe "when testing whether in sync" do
before do
@gid = user.attrclass(:gid).new(:resource => @resource, :should => %w{foo bar})
end
it "should return true if no 'should' values are set" do
@gid = user.attrclass(:gid).new(:resource => @resource)
- @gid.must be_insync(500)
+ @gid.must be_safe_insync(500)
end
it "should return true if any of the specified groups are equal to the current integer" do
Puppet::Util.expects(:gid).with("foo").returns 300
Puppet::Util.expects(:gid).with("bar").returns 500
- @gid.must be_insync(500)
+ @gid.must be_safe_insync(500)
end
it "should return false if none of the specified groups are equal to the current integer" do
Puppet::Util.expects(:gid).with("foo").returns 300
Puppet::Util.expects(:gid).with("bar").returns 500
- @gid.should_not be_insync(700)
+ @gid.should_not be_safe_insync(700)
end
end
describe "when syncing" do
before do
@gid = user.attrclass(:gid).new(:resource => @resource, :should => %w{foo bar})
end
it "should use the first found, specified group as the desired value and send it to the provider" do
Puppet::Util.expects(:gid).with("foo").returns nil
Puppet::Util.expects(:gid).with("bar").returns 500
@provider.expects(:gid=).with 500
@gid.sync
end
end
end
describe "when managing expiry" do
before do
@expiry = user.attrclass(:expiry).new(:resource => @resource)
end
it "should fail if given an invalid date" do
lambda { @expiry.should = "200-20-20" }.should raise_error(Puppet::Error)
end
end
+ describe "when managing minimum password age" do
+ before do
+ @age = user.attrclass(:password_min_age).new(:resource => @resource)
+ end
+
+ it "should accept a negative minimum age" do
+ expect { @age.should = -1 }.should_not raise_error
+ end
+
+ it "should fail with an empty minimum age" do
+ expect { @age.should = '' }.should raise_error(Puppet::Error)
+ end
+ end
+
+ describe "when managing maximum password age" do
+ before do
+ @age = user.attrclass(:password_max_age).new(:resource => @resource)
+ end
+
+ it "should accept a negative maximum age" do
+ expect { @age.should = -1 }.should_not raise_error
+ end
+
+ it "should fail with an empty maximum age" do
+ expect { @age.should = '' }.should raise_error(Puppet::Error)
+ end
+ end
+
describe "when managing passwords" do
before do
@password = user.attrclass(:password).new(:resource => @resource, :should => "mypass")
end
it "should not include the password in the change log when adding the password" do
@password.change_to_s(:absent, "mypass").should_not be_include("mypass")
end
it "should not include the password in the change log when changing the password" do
@password.change_to_s("other", "mypass").should_not be_include("mypass")
end
it "should fail if a ':' is included in the password" do
lambda { @password.should = "some:thing" }.should raise_error(Puppet::Error)
end
it "should allow the value to be set to :absent" do
lambda { @password.should = :absent }.should_not raise_error
end
end
describe "when manages_solaris_rbac is enabled" do
before do
@provider.stubs(:satisfies?).returns(false)
@provider.expects(:satisfies?).with([:manages_solaris_rbac]).returns(true)
end
it "should support a :role value for ensure" do
@ensure = user.attrclass(:ensure).new(:resource => @resource)
lambda { @ensure.should = :role }.should_not raise_error
end
end
describe "when user has roles" do
before do
# To test this feature, we have to support it.
user.new(:name => "foo").provider.class.stubs(:feature?).returns(true)
end
it "should autorequire roles" do
testuser = Puppet::Type.type(:user).new(:name => "testuser")
testuser[:roles] = "testrole"
testrole = Puppet::Type.type(:user).new(:name => "testrole")
config = Puppet::Resource::Catalog.new :testing do |conf|
[testuser, testrole].each { |resource| conf.add_resource resource }
end
Puppet::Type::User::ProviderDirectoryservice.stubs(:get_macosx_version_major).returns "10.5"
rel = testuser.autorequire[0]
rel.source.ref.should == testrole.ref
rel.target.ref.should == testuser.ref
end
end
end
diff --git a/spec/unit/type/zpool_spec.rb b/spec/unit/type/zpool_spec.rb
index db12459ab..be8cb12ba 100755
--- a/spec/unit/type/zpool_spec.rb
+++ b/spec/unit/type/zpool_spec.rb
@@ -1,110 +1,110 @@
#!/usr/bin/env ruby
Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") }
zpool = Puppet::Type.type(:zpool)
describe zpool do
before do
@provider = stub 'provider'
@resource = stub 'resource', :resource => nil, :provider => @provider, :line => nil, :file => nil
end
properties = [:ensure, :disk, :mirror, :raidz, :spare, :log]
properties.each do |property|
it "should have a #{property} property" do
zpool.attrclass(property).ancestors.should be_include(Puppet::Property)
end
end
parameters = [:pool, :raid_parity]
parameters.each do |parameter|
it "should have a #{parameter} parameter" do
zpool.attrclass(parameter).ancestors.should be_include(Puppet::Parameter)
end
end
end
vdev_property = Puppet::Property::VDev
describe vdev_property do
before do
vdev_property.initvars
@resource = stub 'resource', :[]= => nil, :property => nil
@property = vdev_property.new(:resource => @resource)
end
it "should be insync if the devices are the same" do
@property.should = ["dev1 dev2"]
- @property.insync?(["dev2 dev1"]).must be_true
+ @property.safe_insync?(["dev2 dev1"]).must be_true
end
it "should be out of sync if the devices are not the same" do
@property.should = ["dev1 dev3"]
- @property.insync?(["dev2 dev1"]).must be_false
+ @property.safe_insync?(["dev2 dev1"]).must be_false
end
it "should be insync if the devices are the same and the should values are comma seperated" do
@property.should = ["dev1", "dev2"]
- @property.insync?(["dev2 dev1"]).must be_true
+ @property.safe_insync?(["dev2 dev1"]).must be_true
end
it "should be out of sync if the device is absent and should has a value" do
@property.should = ["dev1", "dev2"]
- @property.insync?(:absent).must be_false
+ @property.safe_insync?(:absent).must be_false
end
it "should be insync if the device is absent and should is absent" do
@property.should = [:absent]
- @property.insync?(:absent).must be_true
+ @property.safe_insync?(:absent).must be_true
end
end
multi_vdev_property = Puppet::Property::MultiVDev
describe multi_vdev_property do
before do
multi_vdev_property.initvars
@resource = stub 'resource', :[]= => nil, :property => nil
@property = multi_vdev_property.new(:resource => @resource)
end
it "should be insync if the devices are the same" do
@property.should = ["dev1 dev2"]
- @property.insync?(["dev2 dev1"]).must be_true
+ @property.safe_insync?(["dev2 dev1"]).must be_true
end
it "should be out of sync if the devices are not the same" do
@property.should = ["dev1 dev3"]
- @property.insync?(["dev2 dev1"]).must be_false
+ @property.safe_insync?(["dev2 dev1"]).must be_false
end
it "should be out of sync if the device is absent and should has a value" do
@property.should = ["dev1", "dev2"]
- @property.insync?(:absent).must be_false
+ @property.safe_insync?(:absent).must be_false
end
it "should be insync if the device is absent and should is absent" do
@property.should = [:absent]
- @property.insync?(:absent).must be_true
+ @property.safe_insync?(:absent).must be_true
end
describe "when there are multiple lists of devices" do
it "should be in sync if each group has the same devices" do
@property.should = ["dev1 dev2", "dev3 dev4"]
- @property.insync?(["dev2 dev1", "dev3 dev4"]).must be_true
+ @property.safe_insync?(["dev2 dev1", "dev3 dev4"]).must be_true
end
it "should be out of sync if any group has the different devices" do
@property.should = ["dev1 devX", "dev3 dev4"]
- @property.insync?(["dev2 dev1", "dev3 dev4"]).must be_false
+ @property.safe_insync?(["dev2 dev1", "dev3 dev4"]).must be_false
end
it "should be out of sync if devices are in the wrong group" do
@property.should = ["dev1 dev2", "dev3 dev4"]
- @property.insync?(["dev2 dev3", "dev1 dev4"]).must be_false
+ @property.safe_insync?(["dev2 dev3", "dev1 dev4"]).must be_false
end
end
end
diff --git a/spec/unit/util/zaml_spec.rb b/spec/unit/util/zaml_spec.rb
index f2bcefe01..59590c571 100755
--- a/spec/unit/util/zaml_spec.rb
+++ b/spec/unit/util/zaml_spec.rb
@@ -1,39 +1,60 @@
#!/usr/bin/env ruby
Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") }
require 'puppet/util/monkey_patches'
describe "Pure ruby yaml implementation" do
{
7 => "--- 7",
3.14159 => "--- 3.14159",
'test' => "--- test",
[] => "--- []",
:symbol => "--- !ruby/sym symbol",
{:a => "A"} => "--- \n !ruby/sym a: A",
{:a => "x\ny"} => "--- \n !ruby/sym a: |-\n x\n y"
}.each { |o,y|
it "should convert the #{o.class} #{o.inspect} to yaml" do
o.to_yaml.should == y
end
it "should produce yaml for the #{o.class} #{o.inspect} that can be reconstituted" do
YAML.load(o.to_yaml).should == o
end
}
#
# Can't test for equality on raw objects
{
Object.new => "--- !ruby/object {}",
[Object.new] => "--- \n - !ruby/object {}",
{Object.new => Object.new} => "--- \n ? !ruby/object {}\n : !ruby/object {}"
}.each { |o,y|
it "should convert the #{o.class} #{o.inspect} to yaml" do
o.to_yaml.should == y
end
it "should produce yaml for the #{o.class} #{o.inspect} that can be reconstituted" do
lambda { YAML.load(o.to_yaml) }.should_not raise_error
end
}
+
+ it "should handle references to Array in Hash values correctly" do
+ list = [1]
+ data = { "one" => list, "two" => list }
+ data.to_yaml.should == "--- \n two: &id001 \n - 1\n one: *id001"
+ expect { YAML.load(data.to_yaml).should == data }.should_not raise_error
+ end
+
+ it "should handle references to Hash in Hash values correctly" do
+ hash = { 1 => 1 }
+ data = { "one" => hash, "two" => hash }
+ data.to_yaml.should == "--- \n two: &id001 \n 1: 1\n one: *id001"
+ expect { YAML.load(data.to_yaml).should == data }.should_not raise_error
+ end
+
+ it "should handle references to Scalar in Hash" do
+ str = "hello"
+ data = { "one" => str, "two" => str }
+ data.to_yaml.should == "--- \n two: &id001 hello\n one: *id001"
+ expect { YAML.load(data.to_yaml).should == data }.should_not raise_error
+ end
end