diff --git a/lib/puppet/feature/base.rb b/lib/puppet/feature/base.rb
index cecc1b9ad..8813197a8 100644
--- a/lib/puppet/feature/base.rb
+++ b/lib/puppet/feature/base.rb
@@ -1,65 +1,66 @@
require 'puppet/util/feature'
# Add the simple features, all in one file.
# Order is important as some features depend on others
# We have a syslog implementation
Puppet.features.add(:syslog, :libs => ["syslog"])
# We can use POSIX user functions
Puppet.features.add(:posix) do
require 'etc'
Etc.getpwuid(0) != nil && Puppet.features.syslog?
end
# We can use Microsoft Windows functions
Puppet.features.add(:microsoft_windows) do
begin
require 'sys/admin'
require 'win32/process'
require 'win32/dir'
require 'win32/service'
require 'win32ole'
require 'win32/api'
+ require 'win32/taskscheduler'
true
rescue LoadError => err
- warn "Cannot run on Microsoft Windows without the sys-admin, win32-process, win32-dir & win32-service gems: #{err}" unless Puppet.features.posix?
+ warn "Cannot run on Microsoft Windows without the sys-admin, win32-process, win32-dir, win32-service and win32-taskscheduler gems: #{err}" unless Puppet.features.posix?
end
end
raise Puppet::Error,"Cannot determine basic system flavour" unless Puppet.features.posix? or Puppet.features.microsoft_windows?
# We've got LDAP available.
Puppet.features.add(:ldap, :libs => ["ldap"])
# We have the Rdoc::Usage library.
Puppet.features.add(:usage, :libs => %w{rdoc/ri/ri_paths rdoc/usage})
# We have libshadow, useful for managing passwords.
Puppet.features.add(:libshadow, :libs => ["shadow"])
# We're running as root.
Puppet.features.add(:root) { require 'puppet/util/suidmanager'; Puppet::Util::SUIDManager.root? }
# We've got mongrel available
Puppet.features.add(:mongrel, :libs => %w{rubygems mongrel puppet/network/http_server/mongrel})
# We have lcs diff
Puppet.features.add :diff, :libs => %w{diff/lcs diff/lcs/hunk}
# We have augeas
Puppet.features.add(:augeas, :libs => ["augeas"])
# We have RRD available
Puppet.features.add(:rrd_legacy, :libs => ["RRDtool"])
Puppet.features.add(:rrd, :libs => ["RRD"])
# We have OpenSSL
Puppet.features.add(:openssl, :libs => ["openssl"])
# We have CouchDB
Puppet.features.add(:couchdb, :libs => ["couchrest"])
# We have sqlite
Puppet.features.add(:sqlite, :libs => ["sqlite3"])
diff --git a/lib/puppet/parameter.rb b/lib/puppet/parameter.rb
index e3e9e5164..80bd649ed 100644
--- a/lib/puppet/parameter.rb
+++ b/lib/puppet/parameter.rb
@@ -1,298 +1,316 @@
require 'puppet/util/methodhelper'
require 'puppet/util/log_paths'
require 'puppet/util/logging'
require 'puppet/util/docs'
class Puppet::Parameter
include Puppet::Util
include Puppet::Util::Errors
include Puppet::Util::LogPaths
include Puppet::Util::Logging
include Puppet::Util::MethodHelper
require 'puppet/parameter/value_collection'
class << self
include Puppet::Util
include Puppet::Util::Docs
attr_reader :validater, :munger, :name, :default, :required_features, :value_collection
attr_accessor :metaparam
# Define the default value for a given parameter or parameter. This
# means that 'nil' is an invalid default value. This defines
# the 'default' instance method.
def defaultto(value = nil, &block)
if block
define_method(:default, &block)
else
if value.nil?
raise Puppet::DevError,
"Either a default value or block must be provided"
end
define_method(:default) do value end
end
end
# Return a documentation string. If there are valid values,
# then tack them onto the string.
def doc
@doc ||= ""
unless defined?(@addeddocvals)
@doc += value_collection.doc
if f = self.required_features
@doc += " Requires features #{f.flatten.collect { |f| f.to_s }.join(" ")}."
end
@addeddocvals = true
end
@doc
end
def nodefault
undef_method :default if public_method_defined? :default
end
# Store documentation for this parameter.
def desc(str)
@doc = str
end
def initvars
@value_collection = ValueCollection.new
end
# This is how we munge the value. Basically, this is our
# opportunity to convert the value from one form into another.
def munge(&block)
# I need to wrap the unsafe version in begin/rescue parameterments,
# but if I directly call the block then it gets bound to the
# class's context, not the instance's, thus the two methods,
# instead of just one.
define_method(:unsafe_munge, &block)
end
# Does the parameter support reverse munging?
# This will be called when something wants to access the parameter
# in a canonical form different to what the storage form is.
def unmunge(&block)
define_method(:unmunge, &block)
end
# Mark whether we're the namevar.
def isnamevar
@isnamevar = true
@required = true
end
# Is this parameter the namevar? Defaults to false.
def isnamevar?
@isnamevar
end
# This parameter is required.
def isrequired
@required = true
end
# Specify features that are required for this parameter to work.
def required_features=(*args)
@required_features = args.flatten.collect { |a| a.to_s.downcase.intern }
end
# Is this parameter required? Defaults to false.
def required?
@required
end
# Verify that we got a good value
def validate(&block)
define_method(:unsafe_validate, &block)
end
# Define a new value for our parameter.
def newvalues(*names)
@value_collection.newvalues(*names)
end
def aliasvalue(name, other)
@value_collection.aliasvalue(name, other)
end
end
# Just a simple method to proxy instance methods to class methods
def self.proxymethods(*values)
values.each { |val|
define_method(val) do
self.class.send(val)
end
}
end
# And then define one of these proxies for each method in our
# ParamHandler class.
proxymethods("required?", "isnamevar?")
attr_accessor :resource
# LAK 2007-05-09: Keep the @parent around for backward compatibility.
attr_accessor :parent
[:line, :file, :version].each do |param|
define_method(param) do
resource.send(param)
end
end
def devfail(msg)
self.fail(Puppet::DevError, msg)
end
def fail(*args)
type = nil
if args[0].is_a?(Class)
type = args.shift
else
type = Puppet::Error
end
error = type.new(args.join(" "))
error.line = @resource.line if @resource and @resource.line
error.file = @resource.file if @resource and @resource.file
raise error
end
# Basic parameter initialization.
def initialize(options = {})
options = symbolize_options(options)
if resource = options[:resource]
self.resource = resource
options.delete(:resource)
else
raise Puppet::DevError, "No resource set for #{self.class.name}"
end
set_options(options)
end
def log(msg)
send_log(resource[:loglevel], msg)
end
# Is this parameter a metaparam?
def metaparam?
self.class.metaparam
end
# each parameter class must define the name method, and parameter
# instances do not change that name this implicitly means that a given
# object can only have one parameter instance of a given parameter
# class
def name
self.class.name
end
# for testing whether we should actually do anything
def noop
@noop ||= false
tmp = @noop || self.resource.noop || Puppet[:noop] || false
#debug "noop is #{tmp}"
tmp
end
# return the full path to us, for logging and rollback; not currently
# used
def pathbuilder
if @resource
return [@resource.pathbuilder, self.name]
else
return [self.name]
end
end
# If the specified value is allowed, then munge appropriately.
# If the developer uses a 'munge' hook, this method will get overridden.
def unsafe_munge(value)
self.class.value_collection.munge(value)
end
# no unmunge by default
def unmunge(value)
value
end
# A wrapper around our munging that makes sure we raise useful exceptions.
def munge(value)
begin
ret = unsafe_munge(value)
rescue Puppet::Error => detail
Puppet.debug "Reraising #{detail}"
raise
rescue => detail
raise Puppet::DevError, "Munging failed for value #{value.inspect} in class #{self.name}: #{detail}", detail.backtrace
end
ret
end
# Verify that the passed value is valid.
# If the developer uses a 'validate' hook, this method will get overridden.
def unsafe_validate(value)
self.class.value_collection.validate(value)
end
# A protected validation method that only ever raises useful exceptions.
def validate(value)
begin
unsafe_validate(value)
rescue ArgumentError => detail
fail detail.to_s
rescue Puppet::Error, TypeError
raise
rescue => detail
raise Puppet::DevError, "Validate method failed for class #{self.name}: #{detail}", detail.backtrace
end
end
def remove
@resource = nil
end
def value
unmunge(@value) unless @value.nil?
end
# Store the value provided. All of the checking should possibly be
# late-binding (e.g., users might not exist when the value is assigned
# but might when it is asked for).
def value=(value)
validate(value)
@value = munge(value)
end
# Retrieve the resource's provider. Some types don't have providers, in which
# case we return the resource object itself.
def provider
@resource.provider
end
# The properties need to return tags so that logs correctly collect them.
def tags
unless defined?(@tags)
@tags = []
# This might not be true in testing
@tags = @resource.tags if @resource.respond_to? :tags
@tags << self.name.to_s
end
@tags
end
def to_s
name.to_s
end
+
+ def self.format_value_for_display(value)
+ if value.is_a? Array
+ formatted_values = value.collect {|value| format_value_for_display(value)}.join(', ')
+ "[#{formatted_values}]"
+ elsif value.is_a? Hash
+ # Sorting the hash keys for display is largely for having stable
+ # output to test against, but also helps when scanning for hash
+ # keys, since they will be in ASCIIbetical order.
+ hash = value.keys.sort {|a,b| a.to_s <=> b.to_s}.collect do |k|
+ "'#{k}' => #{format_value_for_display(value[k])}"
+ end.join(', ')
+
+ "{#{hash}}"
+ else
+ "'#{value}'"
+ end
+ end
end
require 'puppet/parameter/path'
diff --git a/lib/puppet/property.rb b/lib/puppet/property.rb
index 12f496a6e..ee8f3b4c1 100644
--- a/lib/puppet/property.rb
+++ b/lib/puppet/property.rb
@@ -1,339 +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)}'"
+ return "defined '#{name}' as #{self.class.format_value_for_display should_to_s(newvalue)}"
elsif newvalue == :absent or newvalue == [:absent]
- return "undefined '#{name}' from '#{is_to_s(current_value)}'"
+ return "undefined '#{name}' from #{self.class.format_value_for_display is_to_s(current_value)}"
else
- return "#{name} changed '#{is_to_s(current_value)}' to '#{should_to_s(newvalue)}'"
+ return "#{name} changed #{self.class.format_value_for_display is_to_s(current_value)} to #{self.class.format_value_for_display 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.
#
# 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/provider/scheduled_task/win32_taskscheduler.rb b/lib/puppet/provider/scheduled_task/win32_taskscheduler.rb
new file mode 100644
index 000000000..a3d80842e
--- /dev/null
+++ b/lib/puppet/provider/scheduled_task/win32_taskscheduler.rb
@@ -0,0 +1,560 @@
+require 'puppet/parameter'
+
+if Puppet.features.microsoft_windows?
+ require 'win32/taskscheduler'
+ require 'puppet/util/adsi'
+end
+
+Puppet::Type.type(:scheduled_task).provide(:win32_taskscheduler) do
+ desc 'This uses the win32-taskscheduler gem to provide support for
+ managing scheduled tasks on Windows.'
+
+ defaultfor :operatingsystem => :windows
+ confine :operatingsystem => :windows
+
+ def self.instances
+ Win32::TaskScheduler.new.tasks.collect do |job_file|
+ job_title = File.basename(job_file, '.job')
+
+ new(
+ :provider => :win32_taskscheduler,
+ :name => job_title
+ )
+ end
+ end
+
+ def exists?
+ Win32::TaskScheduler.new.exists? resource[:name]
+ end
+
+ def task
+ return @task if @task
+
+ @task ||= Win32::TaskScheduler.new
+ @task.activate(resource[:name] + '.job') if exists?
+
+ @task
+ end
+
+ def clear_task
+ @task = nil
+ @triggers = nil
+ end
+
+ def enabled
+ task.flags & Win32::TaskScheduler::DISABLED == 0 ? :true : :false
+ end
+
+ def command
+ task.application_name
+ end
+
+ def arguments
+ task.parameters
+ end
+
+ def working_dir
+ task.working_directory
+ end
+
+ def user
+ account = task.account_information
+ return 'system' if account == ''
+ account
+ end
+
+ def trigger
+ return @triggers if @triggers
+
+ @triggers = []
+ task.trigger_count.times do |i|
+ trigger = begin
+ task.trigger(i)
+ rescue Win32::TaskScheduler::Error => e
+ # Win32::TaskScheduler can't handle all of the
+ # trigger types Windows uses, so we need to skip the
+ # unhandled types to prevent "puppet resource" from
+ # blowing up.
+ nil
+ end
+ next unless trigger and scheduler_trigger_types.include?(trigger['trigger_type'])
+
+ puppet_trigger = {}
+ case trigger['trigger_type']
+ when Win32::TaskScheduler::TASK_TIME_TRIGGER_DAILY
+ puppet_trigger['schedule'] = 'daily'
+ puppet_trigger['every'] = trigger['type']['days_interval'].to_s
+ when Win32::TaskScheduler::TASK_TIME_TRIGGER_WEEKLY
+ puppet_trigger['schedule'] = 'weekly'
+ puppet_trigger['every'] = trigger['type']['weeks_interval'].to_s
+ puppet_trigger['on'] = days_of_week_from_bitfield(trigger['type']['days_of_week'])
+ when Win32::TaskScheduler::TASK_TIME_TRIGGER_MONTHLYDATE
+ puppet_trigger['schedule'] = 'monthly'
+ puppet_trigger['months'] = months_from_bitfield(trigger['type']['months'])
+ puppet_trigger['on'] = days_from_bitfield(trigger['type']['days'])
+ when Win32::TaskScheduler::TASK_TIME_TRIGGER_MONTHLYDOW
+ puppet_trigger['schedule'] = 'monthly'
+ puppet_trigger['months'] = months_from_bitfield(trigger['type']['months'])
+ puppet_trigger['which_occurrence'] = occurrence_constant_to_name(trigger['type']['weeks'])
+ puppet_trigger['day_of_week'] = days_of_week_from_bitfield(trigger['type']['days_of_week'])
+ when Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE
+ puppet_trigger['schedule'] = 'once'
+ end
+ puppet_trigger['start_date'] = self.class.normalized_date("#{trigger['start_year']}-#{trigger['start_month']}-#{trigger['start_day']}")
+ puppet_trigger['start_time'] = self.class.normalized_time("#{trigger['start_hour']}:#{trigger['start_minute']}")
+ puppet_trigger['enabled'] = trigger['flags'] & Win32::TaskScheduler::TASK_TRIGGER_FLAG_DISABLED == 0
+ puppet_trigger['index'] = i
+
+ @triggers << puppet_trigger
+ end
+ @triggers = @triggers[0] if @triggers.length == 1
+
+ @triggers
+ end
+
+ def user_insync?(current, should)
+ return false unless current
+
+ # Win32::TaskScheduler can return the 'SYSTEM' account as the
+ # empty string.
+ current = 'system' if current == ''
+
+ # By comparing account SIDs we don't have to worry about case
+ # sensitivity, or canonicalization of the account name.
+ Puppet::Util::ADSI.sid_for_account(current) == Puppet::Util::ADSI.sid_for_account(should[0])
+ end
+
+ def trigger_insync?(current, should)
+ should = [should] unless should.is_a?(Array)
+ current = [current] unless current.is_a?(Array)
+ return false unless current.length == should.length
+
+ current_in_sync = current.all? do |c|
+ should.any? {|s| triggers_same?(c, s)}
+ end
+
+ should_in_sync = should.all? do |s|
+ current.any? {|c| triggers_same?(c,s)}
+ end
+
+ current_in_sync && should_in_sync
+ end
+
+ def command=(value)
+ task.application_name = value
+ end
+
+ def arguments=(value)
+ task.parameters = value
+ end
+
+ def working_dir=(value)
+ task.working_directory = value
+ end
+
+ def enabled=(value)
+ if value == :true
+ task.flags = task.flags & ~Win32::TaskScheduler::DISABLED
+ else
+ task.flags = task.flags | Win32::TaskScheduler::DISABLED
+ end
+ end
+
+ def trigger=(value)
+ desired_triggers = value.is_a?(Array) ? value : [value]
+ current_triggers = trigger.is_a?(Array) ? trigger : [trigger]
+
+ extra_triggers = []
+ desired_to_search = desired_triggers.dup
+ current_triggers.each do |current|
+ if found = desired_to_search.find {|desired| triggers_same?(current, desired)}
+ desired_to_search.delete(found)
+ else
+ extra_triggers << current['index']
+ end
+ end
+
+ needed_triggers = []
+ current_to_search = current_triggers.dup
+ desired_triggers.each do |desired|
+ if found = current_to_search.find {|current| triggers_same?(current, desired)}
+ current_to_search.delete(found)
+ else
+ needed_triggers << desired
+ end
+ end
+
+ extra_triggers.reverse_each do |index|
+ task.delete_trigger(index)
+ end
+
+ needed_triggers.each do |trigger_hash|
+ # Even though this is an assignment, the API for
+ # Win32::TaskScheduler ends up appending this trigger to the
+ # list of triggers for the task, while #add_trigger is only able
+ # to replace existing triggers. *shrug*
+ task.trigger = translate_hash_to_trigger(trigger_hash)
+ end
+ end
+
+ def user=(value)
+ self.fail("Invalid user: #{value}") unless Puppet::Util::ADSI.sid_for_account(value)
+
+ if value.to_s.downcase != 'system'
+ task.set_account_information(value, resource[:password])
+ else
+ # Win32::TaskScheduler treats a nil/empty username & password as
+ # requesting the SYSTEM account.
+ task.set_account_information(nil, nil)
+ end
+ end
+
+ def create
+ clear_task
+ @task = Win32::TaskScheduler.new(resource[:name], dummy_time_trigger)
+
+ self.command = resource[:command]
+
+ [:arguments, :working_dir, :enabled, :trigger, :user].each do |prop|
+ send("#{prop}=", resource[prop]) if resource[prop]
+ end
+ end
+
+ def destroy
+ Win32::TaskScheduler.new.delete(resource[:name] + '.job')
+ end
+
+ def flush
+ unless resource[:ensure] == :absent
+ self.fail('Parameter command is required.') unless resource[:command]
+ task.save
+ end
+ end
+
+ def triggers_same?(current_trigger, desired_trigger)
+ return false unless current_trigger['schedule'] == desired_trigger['schedule']
+ return false if current_trigger.has_key?('enabled') && !current_trigger['enabled']
+
+ desired = desired_trigger.dup
+
+ desired['every'] ||= current_trigger['every'] if current_trigger.has_key?('every')
+ desired['months'] ||= current_trigger['months'] if current_trigger.has_key?('months')
+ desired['on'] ||= current_trigger['on'] if current_trigger.has_key?('on')
+ desired['day_of_week'] ||= current_trigger['day_of_week'] if current_trigger.has_key?('day_of_week')
+
+ translate_hash_to_trigger(current_trigger) == translate_hash_to_trigger(desired)
+ end
+
+ def self.normalized_date(date_string)
+ date = Date.parse("#{date_string}")
+ "#{date.year}-#{date.month}-#{date.day}"
+ end
+
+ def self.normalized_time(time_string)
+ Time.parse("#{time_string}").strftime('%H:%M')
+ end
+
+ def dummy_time_trigger
+ now = Time.now
+
+ {
+ 'flags' => 0,
+ 'random_minutes_interval' => 0,
+ 'end_day' => 0,
+ "end_year" => 0,
+ "trigger_type" => 0,
+ "minutes_interval" => 0,
+ "end_month" => 0,
+ "minutes_duration" => 0,
+ 'start_year' => now.year,
+ 'start_month' => now.month,
+ 'start_day' => now.day,
+ 'start_hour' => now.hour,
+ 'start_minute' => now.min,
+ 'trigger_type' => Win32::TaskScheduler::ONCE,
+ }
+ end
+
+ def translate_hash_to_trigger(puppet_trigger, user_provided_input=false)
+ trigger = dummy_time_trigger
+
+ if user_provided_input
+ self.fail "'enabled' is read-only on triggers" if puppet_trigger.has_key?('enabled')
+ self.fail "'index' is read-only on triggers" if puppet_trigger.has_key?('index')
+ end
+ puppet_trigger.delete('index')
+
+ if puppet_trigger.delete('enabled') == false
+ trigger['flags'] |= Win32::TaskScheduler::TASK_TRIGGER_FLAG_DISABLED
+ else
+ trigger['flags'] &= ~Win32::TaskScheduler::TASK_TRIGGER_FLAG_DISABLED
+ end
+
+ extra_keys = puppet_trigger.keys.sort - ['schedule', 'start_date', 'start_time', 'every', 'months', 'on', 'which_occurrence', 'day_of_week']
+ self.fail "Unknown trigger option(s): #{Puppet::Parameter.format_value_for_display(extra_keys)}" unless extra_keys.empty?
+ self.fail "Must specify 'start_time' when defining a trigger" unless puppet_trigger['start_time']
+
+ case puppet_trigger['schedule']
+ when 'daily'
+ trigger['trigger_type'] = Win32::TaskScheduler::DAILY
+ trigger['type'] = {
+ 'days_interval' => Integer(puppet_trigger['every'] || 1)
+ }
+ when 'weekly'
+ trigger['trigger_type'] = Win32::TaskScheduler::WEEKLY
+ trigger['type'] = {
+ 'weeks_interval' => Integer(puppet_trigger['every'] || 1)
+ }
+
+ trigger['type']['days_of_week'] = if puppet_trigger['day_of_week']
+ bitfield_from_days_of_week(puppet_trigger['day_of_week'])
+ else
+ scheduler_days_of_week.inject(0) {|day_flags,day| day_flags |= day}
+ end
+ when 'monthly'
+ trigger['type'] = {
+ 'months' => bitfield_from_months(puppet_trigger['months'] || (1..12).to_a),
+ }
+
+ if puppet_trigger.keys.include?('on')
+ if puppet_trigger.has_key?('day_of_week') or puppet_trigger.has_key?('which_occurrence')
+ self.fail "Neither 'day_of_week' nor 'which_occurrence' can be specified when creating a monthly date-based trigger"
+ end
+
+ trigger['trigger_type'] = Win32::TaskScheduler::MONTHLYDATE
+ trigger['type']['days'] = bitfield_from_days(puppet_trigger['on'])
+ elsif puppet_trigger.keys.include?('which_occurrence') or puppet_trigger.keys.include?('day_of_week')
+ self.fail 'which_occurrence cannot be specified as an array' if puppet_trigger['which_occurrence'].is_a?(Array)
+ %w{day_of_week which_occurrence}.each do |field|
+ self.fail "#{field} must be specified when creating a monthly day-of-week based trigger" unless puppet_trigger.has_key?(field)
+ end
+
+ trigger['trigger_type'] = Win32::TaskScheduler::MONTHLYDOW
+ trigger['type']['weeks'] = occurrence_name_to_constant(puppet_trigger['which_occurrence'])
+ trigger['type']['days_of_week'] = bitfield_from_days_of_week(puppet_trigger['day_of_week'])
+ else
+ self.fail "Don't know how to create a 'monthly' schedule with the options: #{puppet_trigger.keys.sort.join(', ')}"
+ end
+ when 'once'
+ self.fail "Must specify 'start_date' when defining a one-time trigger" unless puppet_trigger['start_date']
+
+ trigger['trigger_type'] = Win32::TaskScheduler::ONCE
+ else
+ self.fail "Unknown schedule type: #{puppet_trigger["schedule"].inspect}"
+ end
+
+ if start_date = puppet_trigger['start_date']
+ start_date = Date.parse(start_date)
+ self.fail "start_date must be on or after 1753-01-01" unless start_date >= Date.new(1753, 1, 1)
+
+ trigger['start_year'] = start_date.year
+ trigger['start_month'] = start_date.month
+ trigger['start_day'] = start_date.day
+ end
+
+ start_time = Time.parse(puppet_trigger['start_time'])
+ trigger['start_hour'] = start_time.hour
+ trigger['start_minute'] = start_time.min
+
+ trigger
+ end
+
+ def validate_trigger(value)
+ value = [value] unless value.is_a?(Array)
+
+ # translate_hash_to_trigger handles the same validation that we
+ # would be doing here at the individual trigger level.
+ value.each {|t| translate_hash_to_trigger(t, true)}
+
+ true
+ end
+
+ private
+
+ def bitfield_from_months(months)
+ bitfield = 0
+
+ months = [months] unless months.is_a?(Array)
+ months.each do |month|
+ integer_month = Integer(month) rescue nil
+ self.fail 'Month must be specified as an integer in the range 1-12' unless integer_month == month.to_f and integer_month.between?(1,12)
+
+ bitfield |= scheduler_months[integer_month - 1]
+ end
+
+ bitfield
+ end
+
+ def bitfield_from_days(days)
+ bitfield = 0
+
+ days = [days] unless days.is_a?(Array)
+ days.each do |day|
+ # The special "day" of 'last' is represented by day "number"
+ # 32. 'last' has the special meaning of "the last day of the
+ # month", no matter how many days there are in the month.
+ day = 32 if day == 'last'
+
+ integer_day = Integer(day)
+ self.fail "Day must be specified as an integer in the range 1-31, or as 'last'" unless integer_day = day.to_f and integer_day.between?(1,32)
+
+ bitfield |= 1 << integer_day - 1
+ end
+
+ bitfield
+ end
+
+ def bitfield_from_days_of_week(days_of_week)
+ bitfield = 0
+
+ days_of_week = [days_of_week] unless days_of_week.is_a?(Array)
+ days_of_week.each do |day_of_week|
+ bitfield |= day_of_week_name_to_constant(day_of_week)
+ end
+
+ bitfield
+ end
+
+ def months_from_bitfield(bitfield)
+ months = []
+
+ scheduler_months.each do |month|
+ if bitfield & month != 0
+ months << month_constant_to_number(month)
+ end
+ end
+
+ months
+ end
+
+ def days_from_bitfield(bitfield)
+ days = []
+
+ i = 0
+ while bitfield > 0
+ if bitfield & 1 > 0
+ # Day 32 has the special meaning of "the last day of the
+ # month", no matter how many days there are in the month.
+ days << (i == 31 ? 'last' : i + 1)
+ end
+
+ bitfield = bitfield >> 1
+ i += 1
+ end
+
+ days
+ end
+
+ def days_of_week_from_bitfield(bitfield)
+ days_of_week = []
+
+ scheduler_days_of_week.each do |day_of_week|
+ if bitfield & day_of_week != 0
+ days_of_week << day_of_week_constant_to_name(day_of_week)
+ end
+ end
+
+ days_of_week
+ end
+
+ def scheduler_trigger_types
+ [
+ Win32::TaskScheduler::TASK_TIME_TRIGGER_DAILY,
+ Win32::TaskScheduler::TASK_TIME_TRIGGER_WEEKLY,
+ Win32::TaskScheduler::TASK_TIME_TRIGGER_MONTHLYDATE,
+ Win32::TaskScheduler::TASK_TIME_TRIGGER_MONTHLYDOW,
+ Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE
+ ]
+ end
+
+ def scheduler_days_of_week
+ [
+ Win32::TaskScheduler::SUNDAY,
+ Win32::TaskScheduler::MONDAY,
+ Win32::TaskScheduler::TUESDAY,
+ Win32::TaskScheduler::WEDNESDAY,
+ Win32::TaskScheduler::THURSDAY,
+ Win32::TaskScheduler::FRIDAY,
+ Win32::TaskScheduler::SATURDAY
+ ]
+ end
+
+ def scheduler_months
+ [
+ Win32::TaskScheduler::JANUARY,
+ Win32::TaskScheduler::FEBRUARY,
+ Win32::TaskScheduler::MARCH,
+ Win32::TaskScheduler::APRIL,
+ Win32::TaskScheduler::MAY,
+ Win32::TaskScheduler::JUNE,
+ Win32::TaskScheduler::JULY,
+ Win32::TaskScheduler::AUGUST,
+ Win32::TaskScheduler::SEPTEMBER,
+ Win32::TaskScheduler::OCTOBER,
+ Win32::TaskScheduler::NOVEMBER,
+ Win32::TaskScheduler::DECEMBER
+ ]
+ end
+
+ def scheduler_occurrences
+ [
+ Win32::TaskScheduler::FIRST_WEEK,
+ Win32::TaskScheduler::SECOND_WEEK,
+ Win32::TaskScheduler::THIRD_WEEK,
+ Win32::TaskScheduler::FOURTH_WEEK,
+ Win32::TaskScheduler::LAST_WEEK
+ ]
+ end
+
+ def day_of_week_constant_to_name(constant)
+ case constant
+ when Win32::TaskScheduler::SUNDAY; 'sun'
+ when Win32::TaskScheduler::MONDAY; 'mon'
+ when Win32::TaskScheduler::TUESDAY; 'tues'
+ when Win32::TaskScheduler::WEDNESDAY; 'wed'
+ when Win32::TaskScheduler::THURSDAY; 'thurs'
+ when Win32::TaskScheduler::FRIDAY; 'fri'
+ when Win32::TaskScheduler::SATURDAY; 'sat'
+ end
+ end
+
+ def day_of_week_name_to_constant(name)
+ case name
+ when 'sun'; Win32::TaskScheduler::SUNDAY
+ when 'mon'; Win32::TaskScheduler::MONDAY
+ when 'tues'; Win32::TaskScheduler::TUESDAY
+ when 'wed'; Win32::TaskScheduler::WEDNESDAY
+ when 'thurs'; Win32::TaskScheduler::THURSDAY
+ when 'fri'; Win32::TaskScheduler::FRIDAY
+ when 'sat'; Win32::TaskScheduler::SATURDAY
+ end
+ end
+
+ def month_constant_to_number(constant)
+ month_num = 1
+ while constant >> month_num - 1 > 1
+ month_num += 1
+ end
+ month_num
+ end
+
+ def occurrence_constant_to_name(constant)
+ case constant
+ when Win32::TaskScheduler::FIRST_WEEK; 'first'
+ when Win32::TaskScheduler::SECOND_WEEK; 'second'
+ when Win32::TaskScheduler::THIRD_WEEK; 'third'
+ when Win32::TaskScheduler::FOURTH_WEEK; 'fourth'
+ when Win32::TaskScheduler::LAST_WEEK; 'last'
+ end
+ end
+
+ def occurrence_name_to_constant(name)
+ case name
+ when 'first'; Win32::TaskScheduler::FIRST_WEEK
+ when 'second'; Win32::TaskScheduler::SECOND_WEEK
+ when 'third'; Win32::TaskScheduler::THIRD_WEEK
+ when 'fourth'; Win32::TaskScheduler::FOURTH_WEEK
+ when 'last'; Win32::TaskScheduler::LAST_WEEK
+ end
+ end
+end
diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb
index 17fcdb134..2330cc59c 100644
--- a/lib/puppet/resource.rb
+++ b/lib/puppet/resource.rb
@@ -1,455 +1,452 @@
require 'puppet'
require 'puppet/util/tagging'
require 'puppet/util/pson'
+require 'puppet/parameter'
# The simplest resource class. Eventually it will function as the
# base class for all resource-like behaviour.
class Puppet::Resource
# This stub class is only needed for serialization compatibility with 0.25.x.
# Specifically, it exists to provide a compatibility API when using YAML
# serialized objects loaded from StoreConfigs.
Reference = Puppet::Resource
include Puppet::Util::Tagging
require 'puppet/resource/type_collection_helper'
include Puppet::Resource::TypeCollectionHelper
extend Puppet::Util::Pson
include Enumerable
attr_accessor :file, :line, :catalog, :exported, :virtual, :validate_parameters, :strict
attr_reader :type, :title
require 'puppet/indirector'
extend Puppet::Indirector
indirects :resource, :terminus_class => :ral
ATTRIBUTES = [:file, :line, :exported]
def self.from_pson(pson)
raise ArgumentError, "No resource type provided in pson data" unless type = pson['type']
raise ArgumentError, "No resource title provided in pson data" unless title = pson['title']
resource = new(type, title)
if params = pson['parameters']
params.each { |param, value| resource[param] = value }
end
if tags = pson['tags']
tags.each { |tag| resource.tag(tag) }
end
ATTRIBUTES.each do |a|
if value = pson[a.to_s]
resource.send(a.to_s + "=", value)
end
end
resource.exported ||= false
resource
end
def inspect
"#{@type}[#{@title}]#{to_hash.inspect}"
end
def to_pson_data_hash
data = ([:type, :title, :tags] + ATTRIBUTES).inject({}) do |hash, param|
next hash unless value = self.send(param)
hash[param.to_s] = value
hash
end
data["exported"] ||= false
params = self.to_hash.inject({}) do |hash, ary|
param, value = ary
# Don't duplicate the title as the namevar
next hash if param == namevar and value == title
hash[param] = Puppet::Resource.value_to_pson_data(value)
hash
end
data["parameters"] = params unless params.empty?
data
end
def self.value_to_pson_data(value)
if value.is_a? Array
value.map{|v| value_to_pson_data(v) }
elsif value.is_a? Puppet::Resource
value.to_s
else
value
end
end
def yaml_property_munge(x)
case x
when Hash
x.inject({}) { |h,kv|
k,v = kv
h[k] = self.class.value_to_pson_data(v)
h
}
else self.class.value_to_pson_data(x)
end
end
def to_pson(*args)
to_pson_data_hash.to_pson(*args)
end
# Proxy these methods to the parameters hash. It's likely they'll
# be overridden at some point, but this works for now.
%w{has_key? keys length delete empty? <<}.each do |method|
define_method(method) do |*args|
parameters.send(method, *args)
end
end
# Set a given parameter. Converts all passed names
# to lower-case symbols.
def []=(param, value)
validate_parameter(param) if validate_parameters
parameters[parameter_name(param)] = value
end
# Return a given parameter's value. Converts all passed names
# to lower-case symbols.
def [](param)
parameters[parameter_name(param)]
end
def ==(other)
return false unless other.respond_to?(:title) and self.type == other.type and self.title == other.title
return false unless to_hash == other.to_hash
true
end
# Compatibility method.
def builtin?
builtin_type?
end
# Is this a builtin resource type?
def builtin_type?
resource_type.is_a?(Class)
end
# Iterate over each param/value pair, as required for Enumerable.
def each
parameters.each { |p,v| yield p, v }
end
def include?(parameter)
super || parameters.keys.include?( parameter_name(parameter) )
end
# These two methods are extracted into a Helper
# module, but file load order prevents me
# from including them in the class, and I had weird
# behaviour (i.e., sometimes it didn't work) when
# I directly extended each resource with the helper.
def environment
Puppet::Node::Environment.new(@environment)
end
def environment=(env)
if env.is_a?(String) or env.is_a?(Symbol)
@environment = env
else
@environment = env.name
end
end
%w{exported virtual strict}.each do |m|
define_method(m+"?") do
self.send(m)
end
end
# Create our resource.
def initialize(type, title = nil, attributes = {})
@parameters = {}
# Set things like strictness first.
attributes.each do |attr, value|
next if attr == :parameters
send(attr.to_s + "=", value)
end
@type, @title = extract_type_and_title(type, title)
@type = munge_type_name(@type)
if @type == "Class"
@title = :main if @title == ""
@title = munge_type_name(@title)
end
if params = attributes[:parameters]
extract_parameters(params)
end
tag(self.type)
tag(self.title) if valid_tag?(self.title)
@reference = self # for serialization compatibility with 0.25.x
if strict? and ! resource_type
if @type == 'Class'
raise ArgumentError, "Could not find declared class #{title}"
else
raise ArgumentError, "Invalid resource type #{type}"
end
end
end
def ref
to_s
end
# Find our resource.
def resolve
return(catalog ? catalog.resource(to_s) : nil)
end
def resource_type
case type
when "Class"; known_resource_types.hostclass(title == :main ? "" : title)
when "Node"; known_resource_types.node(title)
else
Puppet::Type.type(type.to_s.downcase.to_sym) || known_resource_types.definition(type)
end
end
# Produce a simple hash of our parameters.
def to_hash
parse_title.merge parameters
end
def to_s
"#{type}[#{title}]"
end
def uniqueness_key
# Temporary kludge to deal with inconsistant use patters
h = self.to_hash
h[namevar] ||= h[:name]
h[:name] ||= h[namevar]
h.values_at(*key_attributes.sort_by { |k| k.to_s })
end
def key_attributes
return(resource_type.respond_to? :key_attributes) ? resource_type.key_attributes : [:name]
end
# Convert our resource to Puppet code.
def to_manifest
# Collect list of attributes to align => and move ensure first
attr = parameters.keys
attr_max = attr.inject(0) { |max,k| k.to_s.length > max ? k.to_s.length : max }
attr.sort!
if attr.first != :ensure && attr.include?(:ensure)
attr.delete(:ensure)
attr.unshift(:ensure)
end
attributes = attr.collect { |k|
v = parameters[k]
- if v.is_a? Array
- " %-#{attr_max}s => %s,\n" % [ k, "[\'#{v.join("', '")}\']" ]
- else
- " %-#{attr_max}s => %s,\n" % [ k, "\'#{v}\'" ]
- end
+ " %-#{attr_max}s => %s,\n" % [k, Puppet::Parameter.format_value_for_display(v)]
}.join
"%s { '%s':\n%s}" % [self.type.to_s.downcase, self.title, attributes]
end
def to_ref
ref
end
# Convert our resource to a RAL resource instance. Creates component
# instances for resource types that don't exist.
def to_ral
if typeklass = Puppet::Type.type(self.type)
return typeklass.new(self)
else
return Puppet::Type::Component.new(self)
end
end
# Translate our object to a backward-compatible transportable object.
def to_trans
if builtin_type? and type.downcase.to_s != "stage"
result = to_transobject
else
result = to_transbucket
end
result.file = self.file
result.line = self.line
result
end
def to_trans_ref
[type.to_s, title.to_s]
end
# Create an old-style TransObject instance, for builtin resource types.
def to_transobject
# Now convert to a transobject
result = Puppet::TransObject.new(title, type)
to_hash.each do |p, v|
if v.is_a?(Puppet::Resource)
v = v.to_trans_ref
elsif v.is_a?(Array)
v = v.collect { |av|
av = av.to_trans_ref if av.is_a?(Puppet::Resource)
av
}
end
# If the value is an array with only one value, then
# convert it to a single value. This is largely so that
# the database interaction doesn't have to worry about
# whether it returns an array or a string.
result[p.to_s] = if v.is_a?(Array) and v.length == 1
v[0]
else
v
end
end
result.tags = self.tags
result
end
def name
# this is potential namespace conflict
# between the notion of an "indirector name"
# and a "resource name"
[ type, title ].join('/')
end
def to_resource
self
end
def valid_parameter?(name)
resource_type.valid_parameter?(name)
end
def validate_parameter(name)
raise ArgumentError, "Invalid parameter #{name}" unless valid_parameter?(name)
end
def prune_parameters(options = {})
properties = resource_type.properties.map(&:name)
dup.collect do |attribute, value|
if value.to_s.empty? or Array(value).empty?
delete(attribute)
elsif value.to_s == "absent" and attribute.to_s != "ensure"
delete(attribute)
end
parameters_to_include = options[:parameters_to_include] || []
delete(attribute) unless properties.include?(attribute) || parameters_to_include.include?(attribute)
end
self
end
private
# Produce a canonical method name.
def parameter_name(param)
param = param.to_s.downcase.to_sym
if param == :name and n = namevar
param = namevar
end
param
end
# The namevar for our resource type. If the type doesn't exist,
# always use :name.
def namevar
if builtin_type? and t = resource_type and t.key_attributes.length == 1
t.key_attributes.first
else
:name
end
end
# Create an old-style TransBucket instance, for non-builtin resource types.
def to_transbucket
bucket = Puppet::TransBucket.new([])
bucket.type = self.type
bucket.name = self.title
# TransBuckets don't support parameters, which is why they're being deprecated.
bucket
end
def extract_parameters(params)
params.each do |param, value|
validate_parameter(param) if strict?
self[param] = value
end
end
def extract_type_and_title(argtype, argtitle)
if (argtitle || argtype) =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ]
elsif argtitle then [ argtype, argtitle ]
elsif argtype.is_a?(Puppet::Type) then [ argtype.class.name, argtype.title ]
elsif argtype.is_a?(Hash) then
raise ArgumentError, "Puppet::Resource.new does not take a hash as the first argument. "+
"Did you mean (#{(argtype[:type] || argtype["type"]).inspect}, #{(argtype[:title] || argtype["title"]).inspect }) ?"
else raise ArgumentError, "No title provided and #{argtype.inspect} is not a valid resource reference"
end
end
def munge_type_name(value)
return :main if value == :main
return "Class" if value == "" or value.nil? or value.to_s.downcase == "component"
value.to_s.split("::").collect { |s| s.capitalize }.join("::")
end
def parse_title
h = {}
type = resource_type
if type.respond_to? :title_patterns
type.title_patterns.each { |regexp, symbols_and_lambdas|
if captures = regexp.match(title.to_s)
symbols_and_lambdas.zip(captures[1..-1]).each { |symbol_and_lambda,capture|
sym, lam = symbol_and_lambda
#self[sym] = lam.call(capture)
h[sym] = lam.call(capture)
}
return h
end
}
else
return { :name => title.to_s }
end
end
def parameters
# @parameters could have been loaded from YAML, causing it to be nil (by
# bypassing initialize).
@parameters ||= {}
end
end
diff --git a/lib/puppet/type/package.rb b/lib/puppet/type/package.rb
index 44f7d0ff5..a39aa696f 100644
--- a/lib/puppet/type/package.rb
+++ b/lib/puppet/type/package.rb
@@ -1,336 +1,337 @@
# 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
+ 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.
- **Autorequires:** If Puppet is managing the files specified as a package's
- `adminfile`, `responsefile`, or `source`, the package resource will autorequire
- those files."
+ **Autorequires:** If Puppet is managing the files specified as a
+ package's `adminfile`, `responsefile`, or `source`, the package
+ resource will autorequire those files."
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]
feature :install_options, "The provider accepts options to be
passed to the installer command."
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)
@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."
validate do |value|
provider.validate_source(value)
end
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
newparam(:install_options, :required_features => :install_options) do
desc "A hash of options to be handled by the provider when
installing a package."
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/scheduled_task.rb b/lib/puppet/type/scheduled_task.rb
new file mode 100644
index 000000000..d83adcb26
--- /dev/null
+++ b/lib/puppet/type/scheduled_task.rb
@@ -0,0 +1,222 @@
+require 'puppet/util'
+
+Puppet::Type.newtype(:scheduled_task) do
+ include Puppet::Util
+
+ @doc = "Installs and manages Windows Scheduled Tasks. All fields
+ except the name, command, and start_time are optional; specifying
+ no repetition parameters will result in a task that runs once on
+ the start date.
+
+ Examples:
+
+ # Create a task that will fire on August 31st, 2011 at 8am in
+ # the system's time-zone.
+ scheduled_task { 'One-shot task':
+ ensure => present,
+ enabled => true,
+ command => 'C:\path\to\command.exe',
+ arguments => '/flags /to /pass',
+ trigger => {
+ schedule => once,
+ start_date => '2011-08-31', # Defaults to 'today'
+ start_time => '08:00', # Must be specified
+ }
+ }
+
+ # Create a task that will fire every other day at 8am in the
+ # system's time-zone, starting August 31st, 2011.
+ scheduled_task { 'Daily task':
+ ensure => present,
+ enabled => true,
+ command => 'C:\path\to\command.exe',
+ arguments => '/flags /to /pass',
+ trigger => {
+ schedule => daily,
+ every => 2 # Defaults to 1
+ start_date => '2011-08-31', # Defaults to 'today'
+ start_time => '08:00', # Must be specified
+ }
+ }
+
+ # Create a task that will fire at 8am Monday every third week,
+ # starting after August 31st, 2011.
+ scheduled_task { 'Weekly task':
+ ensure => present,
+ enabled => true,
+ command => 'C:\path\to\command.exe',
+ arguments => '/flags /to /pass',
+ trigger => {
+ schedule => weekly,
+ every => 3, # Defaults to 1
+ start_date => '2011-08-31' # Defaults to 'today'
+ start_time => '08:00', # Must be specified
+ day_of_week => [mon], # Defaults to all
+ }
+ }
+
+ # Create a task that will fire at 8am on the 1st, 15th, and last
+ # day of the month in January, March, May, July, September, and
+ # November starting August 31st, 2011.
+ scheduled_task { 'Monthly date task':
+ ensure => present,
+ enabled => true,
+ command => 'C:\path\to\command.exe',
+ arguments => '/flags /to /pass',
+ trigger => {
+ schedule => monthly,
+ start_date => '2011-08-31', # Defaults to 'today'
+ start_time => '08:00', # Must be specified
+ months => [1,3,5,7,9,11], # Defaults to all
+ on => [1, 15, last], # Must be specified
+ }
+ }
+
+ # Create a task that will fire at 8am on the first Monday of the
+ # month for January, March, and May, after August 31st, 2011.
+ scheduled_task { 'Monthly day of week task':
+ enabled => true,
+ ensure => present,
+ command => 'C:\path\to\command.exe',
+ arguments => '/flags /to /pass',
+ trigger => {
+ schedule => monthly,
+ start_date => '2011-08-31', # Defaults to 'today'
+ start_time => '08:00', # Must be specified
+ months => [1,3,5], # Defaults to all
+ which_occurrence => first, # Must be specified
+ day_of_week => [mon], # Must be specified
+ }
+ }"
+
+ ensurable
+
+ newproperty(:enabled) do
+ desc "Whether the triggers for this task are enabled. This only
+ supports enabling or disabling all of the triggers for a task,
+ not enabling or disabling them on an individual basis."
+
+ newvalue(:true, :event => :task_enabled)
+ newvalue(:false, :event => :task_disabled)
+
+ defaultto(:true)
+ end
+
+ newparam(:name) do
+ desc "The name assigned to the scheduled task. This will uniquely
+ identify the task on the system."
+
+ isnamevar
+ end
+
+ newproperty(:command) do
+ desc "The full path to the application to be run, without any
+ arguments."
+
+ validate do |value|
+ raise Puppet::Error.new('Must be specified using an absolute path.') unless absolute_path?(value)
+ end
+ end
+
+ newproperty(:working_dir) do
+ desc "The full path of the directory in which to start the
+ command"
+
+ validate do |value|
+ raise Puppet::Error.new('Must be specified using an absolute path.') unless absolute_path?(value)
+ end
+ end
+
+ newproperty(:arguments, :array_matching => :all) do
+ desc "The optional arguments to pass to the command."
+ end
+
+ newproperty(:user) do
+ desc "The user to run the scheduled task as. Please note that not
+ all security configurations will allow running a scheduled task
+ as 'SYSTEM', and saving the scheduled task under these
+ conditions will fail with a reported error of 'The operation
+ completed successfully'. It is recommended that you either
+ choose another user to run the scheduled task, or alter the
+ security policy to allow v1 scheduled tasks to run as the
+ 'SYSTEM' account. Defaults to 'SYSTEM'."
+
+ defaultto :system
+
+ def insync?(current)
+ provider.user_insync?(current, @should)
+ end
+ end
+
+ newparam(:password) do
+ desc "The password for the user specified in the 'user' property.
+ This is only used if specifying a user other than 'SYSTEM'.
+ Since there is no way to retrieve the password used to set the
+ account information for a task, this parameter will not be used
+ to determine if a scheduled task is in sync or not."
+ end
+
+ newproperty(:trigger, :array_matching => :all) do
+ desc "This is a hash defining the properties of the trigger used
+ to fire the scheduled task. The one key that is always required
+ is 'schedule', which can be one of 'daily', 'weekly', or
+ 'monthly'. The other valid & required keys depend on the value
+ of schedule.
+
+ When schedule is 'daily', you can specify a value for 'every'
+ which specifies that the task will trigger every N days. If
+ 'every' is not specified, it defaults to 1 (running every day).
+
+ When schedule is 'weekly', you can specify values for 'every',
+ and 'day_of_week'. 'every' has similar behavior as when
+ specified for 'daily', though it repeats every N weeks, instead
+ of every N days. 'day_of_week' is used to specify on which days
+ of the week the task should be run. This can be specified as an
+ array where the possible values are 'mon', 'tues', 'wed',
+ 'thurs', 'fri', 'sat', and 'sun', or as the string 'all'. The
+ default is 'all'.
+
+ When schedule is 'monthly', the syntax depends on whether you
+ wish to specify the trigger using absolute, or relative dates.
+ In either case, you can specify which months this trigger
+ applies to using 'months', and specifying an array of integer
+ months. 'months' defaults to all months.
+
+ When specifying a monthly schedule with absolute dates, 'on'
+ must be provided as an array of days (1-31, or the special value
+ 'last' which will always be the last day of the month).
+
+ When specifying a monthly schedule with relative dates,
+ 'which_occurrence', and 'day_of_week' must be specified. The
+ possible values for 'which_occurrence' are 'first', 'second',
+ 'third', 'fourth', 'fifth', and 'last'. 'day_of_week' is an
+ array where the possible values are 'mon', 'tues', 'wed',
+ 'thurs', 'fri', 'sat', and 'sun'. These combine to be able to
+ specify things like: The task should run on the first Monday of
+ the specified month(s)."
+
+ validate do |value|
+ provider.validate_trigger(value)
+ end
+
+ def insync?(current)
+ provider.trigger_insync?(current, @should)
+ end
+
+ def should_to_s(new_value=@should)
+ self.class.format_value_for_display(new_value)
+ end
+
+ def is_to_s(current_value=@is)
+ self.class.format_value_for_display(current_value)
+ end
+ end
+
+ validate do
+ return true if self[:ensure] == :absent
+
+ if self[:arguments] and !(self[:arguments].is_a?(Array) and self[:arguments].length == 1)
+ self.fail('Parameter arguments failed: Must be specified as a single string')
+ end
+ end
+end
diff --git a/lib/puppet/type/ssh_authorized_key.rb b/lib/puppet/type/ssh_authorized_key.rb
index 974d9c899..4d768f1a2 100644
--- a/lib/puppet/type/ssh_authorized_key.rb
+++ b/lib/puppet/type/ssh_authorized_key.rb
@@ -1,116 +1,116 @@
module Puppet
newtype(:ssh_authorized_key) do
@doc = "Manages SSH authorized keys. Currently only type 2 keys are
supported.
-
- **Autorequires:** If Puppet is managing the user account in which this
+
+ **Autorequires:** If Puppet is managing the user account in which this
SSH key should be installed, the `ssh_authorized_key` resource will autorequire
that user."
ensurable
newparam(:name) do
desc "The SSH key comment. This attribute is currently used as a
system-wide primary key and therefore has to be unique."
isnamevar
validate do |value|
raise Puppet::Error, "Resourcename must not contain whitespace: #{value}" if value =~ /\s/
end
end
newproperty(:type) do
desc "The encryption type used: ssh-dss or ssh-rsa."
newvalue("ssh-dss")
newvalue("ssh-rsa")
aliasvalue(:dsa, "ssh-dss")
aliasvalue(:rsa, "ssh-rsa")
end
newproperty(:key) do
desc "The key itself; generally a long string of hex digits."
validate do |value|
raise Puppet::Error, "Key must not contain whitespace: #{value}" if value =~ /\s/
end
end
newproperty(:user) do
desc "The user account in which the SSH key should be installed.
The resource will automatically depend on this user."
end
newproperty(:target) do
desc "The absolute filename in which to store the SSH key. This
property is optional and should only be used in cases where keys
are stored in a non-standard location (i.e.` not in
`~user/.ssh/authorized_keys`)."
defaultto :absent
def should
return super if defined?(@should) and @should[0] != :absent
return nil unless user = resource[:user]
begin
return File.expand_path("~#{user}/.ssh/authorized_keys")
rescue
Puppet.debug "The required user is not yet present on the system"
return nil
end
end
def insync?(is)
is == should
end
end
newproperty(:options, :array_matching => :all) do
desc "Key options, see sshd(8) for possible values. Multiple values
should be specified as an array."
defaultto do :absent end
def is_to_s(value)
if value == :absent or value.include?(:absent)
super
else
value.join(",")
end
end
def should_to_s(value)
if value == :absent or value.include?(:absent)
super
else
value.join(",")
end
end
validate do |value|
unless value == :absent or value =~ /^[-a-z0-9A-Z_]+(?:=\".*?\")?$/
raise Puppet::Error, "Option #{value} is not valid. A single option must either be of the form 'option' or 'option=\"value\". Multiple options must be provided as an array"
end
end
end
autorequire(:user) do
should(:user) if should(:user)
end
validate do
# Go ahead if target attribute is defined
return if @parameters[:target].shouldorig[0] != :absent
# Go ahead if user attribute is defined
return if @parameters.include?(:user)
# If neither target nor user is defined, this is an error
raise Puppet::Error, "Attribute 'user' or 'target' is mandatory"
end
end
end
diff --git a/lib/puppet/util/adsi.rb b/lib/puppet/util/adsi.rb
index 5db69d93d..8d123c8f4 100644
--- a/lib/puppet/util/adsi.rb
+++ b/lib/puppet/util/adsi.rb
@@ -1,293 +1,294 @@
module Puppet::Util::ADSI
class << self
def connectable?(uri)
begin
!! connect(uri)
rescue
false
end
end
def connect(uri)
begin
WIN32OLE.connect(uri)
rescue Exception => e
raise Puppet::Error.new( "ADSI connection error: #{e}" )
end
end
def create(name, resource_type)
Puppet::Util::ADSI.connect(computer_uri).Create(resource_type, name)
end
def delete(name, resource_type)
Puppet::Util::ADSI.connect(computer_uri).Delete(resource_type, name)
end
def computer_name
unless @computer_name
buf = " " * 128
Win32API.new('kernel32', 'GetComputerName', ['P','P'], 'I').call(buf, buf.length.to_s)
@computer_name = buf.unpack("A*")
end
@computer_name
end
def computer_uri
"WinNT://#{computer_name}"
end
def wmi_resource_uri( host = '.' )
"winmgmts:{impersonationLevel=impersonate}!//#{host}/root/cimv2"
end
def uri(resource_name, resource_type)
"#{computer_uri}/#{resource_name},#{resource_type}"
end
def execquery(query)
connect(wmi_resource_uri).execquery(query)
end
def sid_for_account(name)
sid = nil
-
- execquery(
- "SELECT Sid from Win32_Account
- WHERE Name = '#{name}' AND LocalAccount = true"
- ).each {|u| sid ||= u.Sid}
-
+ if name =~ /\\/
+ domain, name = name.split('\\', 2)
+ query = "SELECT Sid from Win32_Account WHERE Name = '#{name}' AND Domain = '#{domain}' AND LocalAccount = true"
+ else
+ query = "SELECT Sid from Win32_Account WHERE Name = '#{name}' AND LocalAccount = true"
+ end
+ execquery(query).each { |u| sid ||= u.Sid }
sid
end
end
class User
extend Enumerable
attr_accessor :native_user
attr_reader :name
def initialize(name, native_user = nil)
@name = name
@native_user = native_user
end
def native_user
@native_user ||= Puppet::Util::ADSI.connect(uri)
end
def self.uri(name)
Puppet::Util::ADSI.uri(name, 'user')
end
def uri
self.class.uri(name)
end
def self.logon(name, password)
fLOGON32_LOGON_NETWORK = 3
fLOGON32_PROVIDER_DEFAULT = 0
logon_user = Win32API.new("advapi32", "LogonUser", ['P', 'P', 'P', 'L', 'L', 'P'], 'L')
close_handle = Win32API.new("kernel32", "CloseHandle", ['P'], 'V')
token = ' ' * 4
if logon_user.call(name, "", password, fLOGON32_LOGON_NETWORK, fLOGON32_PROVIDER_DEFAULT, token) != 0
close_handle.call(token.unpack('L')[0])
true
else
false
end
end
def [](attribute)
native_user.Get(attribute)
end
def []=(attribute, value)
native_user.Put(attribute, value)
end
def commit
begin
native_user.SetInfo unless native_user.nil?
rescue Exception => e
raise Puppet::Error.new( "User update failed: #{e}" )
end
self
end
def password_is?(password)
self.class.logon(name, password)
end
def add_flag(flag_name, value)
flag = native_user.Get(flag_name) rescue 0
native_user.Put(flag_name, flag | value)
commit
end
def password=(password)
native_user.SetPassword(password)
commit
fADS_UF_DONT_EXPIRE_PASSWD = 0x10000
add_flag("UserFlags", fADS_UF_DONT_EXPIRE_PASSWD)
end
def groups
# WIN32OLE objects aren't enumerable, so no map
groups = []
native_user.Groups.each {|g| groups << g.Name} rescue nil
groups
end
def add_to_groups(*group_names)
group_names.each do |group_name|
Puppet::Util::ADSI::Group.new(group_name).add_member(@name)
end
end
alias add_to_group add_to_groups
def remove_from_groups(*group_names)
group_names.each do |group_name|
Puppet::Util::ADSI::Group.new(group_name).remove_member(@name)
end
end
alias remove_from_group remove_from_groups
def set_groups(desired_groups, minimum = true)
return if desired_groups.nil? or desired_groups.empty?
desired_groups = desired_groups.split(',').map(&:strip)
current_groups = self.groups
# First we add the user to all the groups it should be in but isn't
groups_to_add = desired_groups - current_groups
add_to_groups(*groups_to_add)
# Then we remove the user from all groups it is in but shouldn't be, if
# that's been requested
groups_to_remove = current_groups - desired_groups
remove_from_groups(*groups_to_remove) unless minimum
end
def self.create(name)
# Windows error 1379: The specified local group already exists.
raise Puppet::Error.new( "Cannot create user if group '#{name}' exists." ) if Puppet::Util::ADSI::Group.exists? name
new(name, Puppet::Util::ADSI.create(name, 'user'))
end
def self.exists?(name)
Puppet::Util::ADSI::connectable?(User.uri(name))
end
def self.delete(name)
Puppet::Util::ADSI.delete(name, 'user')
end
def self.each(&block)
wql = Puppet::Util::ADSI.execquery("select * from win32_useraccount")
users = []
wql.each do |u|
users << new(u.name, u)
end
users.each(&block)
end
end
class Group
extend Enumerable
attr_accessor :native_group
attr_reader :name
def initialize(name, native_group = nil)
@name = name
@native_group = native_group
end
def uri
self.class.uri(name)
end
def self.uri(name)
Puppet::Util::ADSI.uri(name, 'group')
end
def native_group
@native_group ||= Puppet::Util::ADSI.connect(uri)
end
def commit
begin
native_group.SetInfo unless native_group.nil?
rescue Exception => e
raise Puppet::Error.new( "Group update failed: #{e}" )
end
self
end
def add_members(*names)
names.each do |name|
native_group.Add(Puppet::Util::ADSI::User.uri(name))
end
end
alias add_member add_members
def remove_members(*names)
names.each do |name|
native_group.Remove(Puppet::Util::ADSI::User.uri(name))
end
end
alias remove_member remove_members
def members
# WIN32OLE objects aren't enumerable, so no map
members = []
native_group.Members.each {|m| members << m.Name}
members
end
def set_members(desired_members)
return if desired_members.nil? or desired_members.empty?
current_members = self.members
# First we add all missing members
members_to_add = desired_members - current_members
add_members(*members_to_add)
# Then we remove all extra members
members_to_remove = current_members - desired_members
remove_members(*members_to_remove)
end
def self.create(name)
# Windows error 2224: The account already exists.
raise Puppet::Error.new( "Cannot create group if user '#{name}' exists." ) if Puppet::Util::ADSI::User.exists? name
new(name, Puppet::Util::ADSI.create(name, 'group'))
end
def self.exists?(name)
Puppet::Util::ADSI.connectable?(Group.uri(name))
end
def self.delete(name)
Puppet::Util::ADSI.delete(name, 'group')
end
def self.each(&block)
wql = Puppet::Util::ADSI.execquery( "select * from win32_group" )
groups = []
wql.each do |g|
groups << new(g.name, g)
end
groups.each(&block)
end
end
end
diff --git a/spec/unit/parameter_spec.rb b/spec/unit/parameter_spec.rb
index 1ed211957..d76a32bd7 100755
--- a/spec/unit/parameter_spec.rb
+++ b/spec/unit/parameter_spec.rb
@@ -1,160 +1,196 @@
#!/usr/bin/env rspec
require 'spec_helper'
require 'puppet/parameter'
describe Puppet::Parameter do
before do
@class = Class.new(Puppet::Parameter) do
@name = :foo
end
@class.initvars
@resource = mock 'resource'
@resource.stub_everything
@parameter = @class.new :resource => @resource
end
it "should create a value collection" do
@class = Class.new(Puppet::Parameter)
@class.value_collection.should be_nil
@class.initvars
@class.value_collection.should be_instance_of(Puppet::Parameter::ValueCollection)
end
it "should return its name as a string when converted to a string" do
@parameter.to_s.should == @parameter.name.to_s
end
[:line, :file, :version].each do |data|
it "should return its resource's #{data} as its #{data}" do
@resource.expects(data).returns "foo"
@parameter.send(data).should == "foo"
end
end
it "should return the resource's tags plus its name as its tags" do
@resource.expects(:tags).returns %w{one two}
@parameter.tags.should == %w{one two foo}
end
it "should provide source_descriptors" do
@resource.expects(:line).returns 10
@resource.expects(:file).returns "file"
@resource.expects(:tags).returns %w{one two}
@parameter.source_descriptors.should == {:tags=>["one", "two", "foo"], :path=>"//foo", :file => "file", :line => 10}
end
describe "when returning the value" do
it "should return nil if no value is set" do
@parameter.value.should be_nil
end
it "should validate the value" do
@parameter.expects(:validate).with("foo")
@parameter.value = "foo"
end
it "should munge the value and use any result as the actual value" do
@parameter.expects(:munge).with("foo").returns "bar"
@parameter.value = "foo"
@parameter.value.should == "bar"
end
it "should unmunge the value when accessing the actual value" do
@parameter.class.unmunge do |value| value.to_sym end
@parameter.value = "foo"
@parameter.value.should == :foo
end
it "should return the actual value by default when unmunging" do
@parameter.unmunge("bar").should == "bar"
end
it "should return any set value" do
@parameter.value = "foo"
@parameter.value.should == "foo"
end
end
describe "when validating values" do
it "should do nothing if no values or regexes have been defined" do
@parameter.validate("foo")
end
it "should catch abnormal failures thrown during validation" do
@class.validate { |v| raise "This is broken" }
lambda { @parameter.validate("eh") }.should raise_error(Puppet::DevError)
end
it "should fail if the value is not a defined value or alias and does not match a regex" do
@class.newvalues :foo
lambda { @parameter.validate("bar") }.should raise_error(Puppet::Error)
end
it "should succeed if the value is one of the defined values" do
@class.newvalues :foo
lambda { @parameter.validate(:foo) }.should_not raise_error(ArgumentError)
end
it "should succeed if the value is one of the defined values even if the definition uses a symbol and the validation uses a string" do
@class.newvalues :foo
lambda { @parameter.validate("foo") }.should_not raise_error(ArgumentError)
end
it "should succeed if the value is one of the defined values even if the definition uses a string and the validation uses a symbol" do
@class.newvalues "foo"
lambda { @parameter.validate(:foo) }.should_not raise_error(ArgumentError)
end
it "should succeed if the value is one of the defined aliases" do
@class.newvalues :foo
@class.aliasvalue :bar, :foo
lambda { @parameter.validate("bar") }.should_not raise_error(ArgumentError)
end
it "should succeed if the value matches one of the regexes" do
@class.newvalues %r{\d}
lambda { @parameter.validate("10") }.should_not raise_error(ArgumentError)
end
end
describe "when munging values" do
it "should do nothing if no values or regexes have been defined" do
@parameter.munge("foo").should == "foo"
end
it "should catch abnormal failures thrown during munging" do
@class.munge { |v| raise "This is broken" }
lambda { @parameter.munge("eh") }.should raise_error(Puppet::DevError)
end
it "should return return any matching defined values" do
@class.newvalues :foo, :bar
@parameter.munge("foo").should == :foo
end
it "should return any matching aliases" do
@class.newvalues :foo
@class.aliasvalue :bar, :foo
@parameter.munge("bar").should == :foo
end
it "should return the value if it matches a regex" do
@class.newvalues %r{\w}
@parameter.munge("bar").should == "bar"
end
it "should return the value if no other option is matched" do
@class.newvalues :foo
@parameter.munge("bar").should == "bar"
end
end
describe "when logging" do
it "should use its resource's log level and the provided message" do
@resource.expects(:[]).with(:loglevel).returns :notice
@parameter.expects(:send_log).with(:notice, "mymessage")
@parameter.log "mymessage"
end
end
+
+ describe ".format_value_for_display" do
+ it 'should format strings appropriately' do
+ described_class.format_value_for_display('foo').should == "'foo'"
+ end
+
+ it 'should format numbers appropriately' do
+ described_class.format_value_for_display(1).should == "'1'"
+ end
+
+ it 'should format symbols appropriately' do
+ described_class.format_value_for_display(:bar).should == "'bar'"
+ end
+
+ it 'should format arrays appropriately' do
+ described_class.format_value_for_display([1, 'foo', :bar]).should == "['1', 'foo', 'bar']"
+ end
+
+ it 'should format hashes appropriately' do
+ described_class.format_value_for_display(
+ {1 => 'foo', :bar => 2, 'baz' => :qux}
+ ).should == "{'1' => 'foo', 'bar' => '2', 'baz' => 'qux'}"
+ end
+
+ it 'should format arrays with nested data appropriately' do
+ described_class.format_value_for_display(
+ [1, 'foo', :bar, [1, 2, 3], {1 => 2, 3 => 4}]
+ ).should == "['1', 'foo', 'bar', ['1', '2', '3'], {'1' => '2', '3' => '4'}]"
+ end
+
+ it 'should format hashes with nested data appropriately' do
+ described_class.format_value_for_display(
+ {1 => 'foo', :bar => [2, 3, 4], 'baz' => {:qux => 1, :quux => 'two'}}
+ ).should == "{'1' => 'foo', 'bar' => ['2', '3', '4'], 'baz' => {'quux' => 'two', 'qux' => '1'}}"
+ end
+ end
end
diff --git a/spec/unit/provider/scheduled_task/win32_taskscheduler_spec.rb b/spec/unit/provider/scheduled_task/win32_taskscheduler_spec.rb
new file mode 100644
index 000000000..1bf8b7eb3
--- /dev/null
+++ b/spec/unit/provider/scheduled_task/win32_taskscheduler_spec.rb
@@ -0,0 +1,1571 @@
+#!/usr/bin/env rspec
+require 'spec_helper'
+
+require 'win32/taskscheduler' if Puppet.features.microsoft_windows?
+
+shared_examples_for "a trigger that handles start_date and start_time" do
+ let(:trigger) do
+ described_class.new(
+ :name => 'Shared Test Task',
+ :command => 'C:\Windows\System32\notepad.exe'
+ ).translate_hash_to_trigger(trigger_hash)
+ end
+
+ before :each do
+ Win32::TaskScheduler.any_instance.stubs(:save)
+ end
+
+ describe 'the given start_date' do
+ before :each do
+ trigger_hash['start_time'] = '00:00'
+ end
+
+ def date_component
+ {
+ 'start_year' => trigger['start_year'],
+ 'start_month' => trigger['start_month'],
+ 'start_day' => trigger['start_day']
+ }
+ end
+
+ it 'should be able to be specified in ISO 8601 calendar date format' do
+ trigger_hash['start_date'] = '2011-12-31'
+
+ date_component.should == {
+ 'start_year' => 2011,
+ 'start_month' => 12,
+ 'start_day' => 31
+ }
+ end
+
+ it 'should fail if before 1753-01-01' do
+ trigger_hash['start_date'] = '1752-12-31'
+
+ expect { date_component }.to raise_error(
+ Puppet::Error,
+ 'start_date must be on or after 1753-01-01'
+ )
+ end
+
+ it 'should succeed if on 1753-01-01' do
+ trigger_hash['start_date'] = '1753-01-01'
+
+ date_component.should == {
+ 'start_year' => 1753,
+ 'start_month' => 1,
+ 'start_day' => 1
+ }
+ end
+
+ it 'should succeed if after 1753-01-01' do
+ trigger_hash['start_date'] = '1753-01-02'
+
+ date_component.should == {
+ 'start_year' => 1753,
+ 'start_month' => 1,
+ 'start_day' => 2
+ }
+ end
+ end
+
+ describe 'the given start_time' do
+ before :each do
+ trigger_hash['start_date'] = '2011-12-31'
+ end
+
+ def time_component
+ {
+ 'start_hour' => trigger['start_hour'],
+ 'start_minute' => trigger['start_minute']
+ }
+ end
+
+ it 'should be able to be specified as a 24-hour "hh:mm"' do
+ trigger_hash['start_time'] = '17:13'
+
+ time_component.should == {
+ 'start_hour' => 17,
+ 'start_minute' => 13
+ }
+ end
+
+ it 'should be able to be specified as a 12-hour "hh:mm am"' do
+ trigger_hash['start_time'] = '3:13 am'
+
+ time_component.should == {
+ 'start_hour' => 3,
+ 'start_minute' => 13
+ }
+ end
+
+ it 'should be able to be specified as a 12-hour "hh:mm pm"' do
+ trigger_hash['start_time'] = '3:13 pm'
+
+ time_component.should == {
+ 'start_hour' => 15,
+ 'start_minute' => 13
+ }
+ end
+ end
+end
+
+describe Puppet::Type.type(:scheduled_task).provider(:win32_taskscheduler), :if => Puppet.features.microsoft_windows? do
+ before :each do
+ Puppet::Type.type(:scheduled_task).stubs(:defaultprovider).returns(described_class)
+ end
+
+ describe 'when retrieving' do
+ before :each do
+ @mock_task = mock
+ @mock_task.responds_like(Win32::TaskScheduler.new)
+ described_class.any_instance.stubs(:task).returns(@mock_task)
+
+ Win32::TaskScheduler.stubs(:new).returns(@mock_task)
+ end
+ let(:resource) { Puppet::Type.type(:scheduled_task).new(:name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe') }
+
+ describe 'the triggers for a task' do
+ describe 'with only one trigger' do
+ before :each do
+ @mock_task.expects(:trigger_count).returns(1)
+ end
+
+ it 'should handle a single daily trigger' do
+ @mock_task.expects(:trigger).with(0).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_DAILY,
+ 'start_year' => 2011,
+ 'start_month' => 9,
+ 'start_day' => 12,
+ 'start_hour' => 13,
+ 'start_minute' => 20,
+ 'flags' => 0,
+ 'type' => { 'days_interval' => 2 },
+ })
+
+ resource.provider.trigger.should == {
+ 'start_date' => '2011-9-12',
+ 'start_time' => '13:20',
+ 'schedule' => 'daily',
+ 'every' => '2',
+ 'enabled' => true,
+ 'index' => 0,
+ }
+ end
+
+ it 'should handle a single weekly trigger' do
+ scheduled_days_of_week = Win32::TaskScheduler::MONDAY |
+ Win32::TaskScheduler::WEDNESDAY |
+ Win32::TaskScheduler::FRIDAY |
+ Win32::TaskScheduler::SUNDAY
+ @mock_task.expects(:trigger).with(0).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_WEEKLY,
+ 'start_year' => 2011,
+ 'start_month' => 9,
+ 'start_day' => 12,
+ 'start_hour' => 13,
+ 'start_minute' => 20,
+ 'flags' => 0,
+ 'type' => {
+ 'weeks_interval' => 2,
+ 'days_of_week' => scheduled_days_of_week
+ }
+ })
+
+ resource.provider.trigger.should == {
+ 'start_date' => '2011-9-12',
+ 'start_time' => '13:20',
+ 'schedule' => 'weekly',
+ 'every' => '2',
+ 'on' => ['sun', 'mon', 'wed', 'fri'],
+ 'enabled' => true,
+ 'index' => 0,
+ }
+ end
+
+ it 'should handle a single monthly date-based trigger' do
+ scheduled_months = Win32::TaskScheduler::JANUARY |
+ Win32::TaskScheduler::FEBRUARY |
+ Win32::TaskScheduler::AUGUST |
+ Win32::TaskScheduler::SEPTEMBER |
+ Win32::TaskScheduler::DECEMBER
+ # 1 3 5 15 'last'
+ scheduled_days = 1 | 1 << 2 | 1 << 4 | 1 << 14 | 1 << 31
+ @mock_task.expects(:trigger).with(0).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_MONTHLYDATE,
+ 'start_year' => 2011,
+ 'start_month' => 9,
+ 'start_day' => 12,
+ 'start_hour' => 13,
+ 'start_minute' => 20,
+ 'flags' => 0,
+ 'type' => {
+ 'months' => scheduled_months,
+ 'days' => scheduled_days
+ }
+ })
+
+ resource.provider.trigger.should == {
+ 'start_date' => '2011-9-12',
+ 'start_time' => '13:20',
+ 'schedule' => 'monthly',
+ 'months' => [1, 2, 8, 9, 12],
+ 'on' => [1, 3, 5, 15, 'last'],
+ 'enabled' => true,
+ 'index' => 0,
+ }
+ end
+
+ it 'should handle a single monthly day-of-week-based trigger' do
+ scheduled_months = Win32::TaskScheduler::JANUARY |
+ Win32::TaskScheduler::FEBRUARY |
+ Win32::TaskScheduler::AUGUST |
+ Win32::TaskScheduler::SEPTEMBER |
+ Win32::TaskScheduler::DECEMBER
+ scheduled_days_of_week = Win32::TaskScheduler::MONDAY |
+ Win32::TaskScheduler::WEDNESDAY |
+ Win32::TaskScheduler::FRIDAY |
+ Win32::TaskScheduler::SUNDAY
+ @mock_task.expects(:trigger).with(0).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_MONTHLYDOW,
+ 'start_year' => 2011,
+ 'start_month' => 9,
+ 'start_day' => 12,
+ 'start_hour' => 13,
+ 'start_minute' => 20,
+ 'flags' => 0,
+ 'type' => {
+ 'months' => scheduled_months,
+ 'weeks' => Win32::TaskScheduler::FIRST_WEEK,
+ 'days_of_week' => scheduled_days_of_week
+ }
+ })
+
+ resource.provider.trigger.should == {
+ 'start_date' => '2011-9-12',
+ 'start_time' => '13:20',
+ 'schedule' => 'monthly',
+ 'months' => [1, 2, 8, 9, 12],
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['sun', 'mon', 'wed', 'fri'],
+ 'enabled' => true,
+ 'index' => 0,
+ }
+ end
+
+ it 'should handle a single one-time trigger' do
+ @mock_task.expects(:trigger).with(0).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE,
+ 'start_year' => 2011,
+ 'start_month' => 9,
+ 'start_day' => 12,
+ 'start_hour' => 13,
+ 'start_minute' => 20,
+ 'flags' => 0,
+ })
+
+ resource.provider.trigger.should == {
+ 'start_date' => '2011-9-12',
+ 'start_time' => '13:20',
+ 'schedule' => 'once',
+ 'enabled' => true,
+ 'index' => 0,
+ }
+ end
+ end
+
+ it 'should handle multiple triggers' do
+ @mock_task.expects(:trigger_count).returns(3)
+ @mock_task.expects(:trigger).with(0).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE,
+ 'start_year' => 2011,
+ 'start_month' => 10,
+ 'start_day' => 13,
+ 'start_hour' => 14,
+ 'start_minute' => 21,
+ 'flags' => 0,
+ })
+ @mock_task.expects(:trigger).with(1).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE,
+ 'start_year' => 2012,
+ 'start_month' => 11,
+ 'start_day' => 14,
+ 'start_hour' => 15,
+ 'start_minute' => 22,
+ 'flags' => 0,
+ })
+ @mock_task.expects(:trigger).with(2).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE,
+ 'start_year' => 2013,
+ 'start_month' => 12,
+ 'start_day' => 15,
+ 'start_hour' => 16,
+ 'start_minute' => 23,
+ 'flags' => 0,
+ })
+
+ resource.provider.trigger.should =~ [
+ {
+ 'start_date' => '2011-10-13',
+ 'start_time' => '14:21',
+ 'schedule' => 'once',
+ 'enabled' => true,
+ 'index' => 0,
+ },
+ {
+ 'start_date' => '2012-11-14',
+ 'start_time' => '15:22',
+ 'schedule' => 'once',
+ 'enabled' => true,
+ 'index' => 1,
+ },
+ {
+ 'start_date' => '2013-12-15',
+ 'start_time' => '16:23',
+ 'schedule' => 'once',
+ 'enabled' => true,
+ 'index' => 2,
+ }
+ ]
+ end
+
+ it 'should skip triggers Win32::TaskScheduler cannot handle' do
+ @mock_task.expects(:trigger_count).returns(3)
+ @mock_task.expects(:trigger).with(0).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE,
+ 'start_year' => 2011,
+ 'start_month' => 10,
+ 'start_day' => 13,
+ 'start_hour' => 14,
+ 'start_minute' => 21,
+ 'flags' => 0,
+ })
+ @mock_task.expects(:trigger).with(1).raises(
+ Win32::TaskScheduler::Error.new('Unhandled trigger type!')
+ )
+ @mock_task.expects(:trigger).with(2).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE,
+ 'start_year' => 2013,
+ 'start_month' => 12,
+ 'start_day' => 15,
+ 'start_hour' => 16,
+ 'start_minute' => 23,
+ 'flags' => 0,
+ })
+
+ resource.provider.trigger.should =~ [
+ {
+ 'start_date' => '2011-10-13',
+ 'start_time' => '14:21',
+ 'schedule' => 'once',
+ 'enabled' => true,
+ 'index' => 0,
+ },
+ {
+ 'start_date' => '2013-12-15',
+ 'start_time' => '16:23',
+ 'schedule' => 'once',
+ 'enabled' => true,
+ 'index' => 2,
+ }
+ ]
+ end
+
+ it 'should skip trigger types Puppet does not handle' do
+ @mock_task.expects(:trigger_count).returns(3)
+ @mock_task.expects(:trigger).with(0).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE,
+ 'start_year' => 2011,
+ 'start_month' => 10,
+ 'start_day' => 13,
+ 'start_hour' => 14,
+ 'start_minute' => 21,
+ 'flags' => 0,
+ })
+ @mock_task.expects(:trigger).with(1).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_EVENT_TRIGGER_AT_LOGON,
+ })
+ @mock_task.expects(:trigger).with(2).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE,
+ 'start_year' => 2013,
+ 'start_month' => 12,
+ 'start_day' => 15,
+ 'start_hour' => 16,
+ 'start_minute' => 23,
+ 'flags' => 0,
+ })
+
+ resource.provider.trigger.should =~ [
+ {
+ 'start_date' => '2011-10-13',
+ 'start_time' => '14:21',
+ 'schedule' => 'once',
+ 'enabled' => true,
+ 'index' => 0,
+ },
+ {
+ 'start_date' => '2013-12-15',
+ 'start_time' => '16:23',
+ 'schedule' => 'once',
+ 'enabled' => true,
+ 'index' => 2,
+ }
+ ]
+ end
+ end
+
+ it 'should get the working directory from the working_directory on the task' do
+ @mock_task.expects(:working_directory).returns('C:\Windows\System32')
+
+ resource.provider.working_dir.should == 'C:\Windows\System32'
+ end
+
+ it 'should get the command from the application_name on the task' do
+ @mock_task.expects(:application_name).returns('C:\Windows\System32\notepad.exe')
+
+ resource.provider.command.should == 'C:\Windows\System32\notepad.exe'
+ end
+
+ it 'should get the command arguments from the parameters on the task' do
+ @mock_task.expects(:parameters).returns('these are my arguments')
+
+ resource.provider.arguments.should == 'these are my arguments'
+ end
+
+ it 'should get the user from the account_information on the task' do
+ @mock_task.expects(:account_information).returns('this is my user')
+
+ resource.provider.user.should == 'this is my user'
+ end
+
+ describe 'whether the task is enabled' do
+ it 'should report tasks with the disabled bit set as disabled' do
+ @mock_task.stubs(:flags).returns(Win32::TaskScheduler::DISABLED)
+
+ resource.provider.enabled.should == :false
+ end
+
+ it 'should report tasks without the disabled bit set as enabled' do
+ @mock_task.stubs(:flags).returns(~Win32::TaskScheduler::DISABLED)
+
+ resource.provider.enabled.should == :true
+ end
+
+ it 'should not consider triggers for determining if the task is enabled' do
+ @mock_task.stubs(:flags).returns(~Win32::TaskScheduler::DISABLED)
+ @mock_task.stubs(:trigger_count).returns(1)
+ @mock_task.stubs(:trigger).with(0).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE,
+ 'start_year' => 2011,
+ 'start_month' => 10,
+ 'start_day' => 13,
+ 'start_hour' => 14,
+ 'start_minute' => 21,
+ 'flags' => Win32::TaskScheduler::TASK_TRIGGER_FLAG_DISABLED,
+ })
+
+ resource.provider.enabled.should == :true
+ end
+ end
+ end
+
+ describe '#exists?' do
+ before :each do
+ @mock_task = mock
+ @mock_task.responds_like(Win32::TaskScheduler.new)
+ described_class.any_instance.stubs(:task).returns(@mock_task)
+
+ Win32::TaskScheduler.stubs(:new).returns(@mock_task)
+ end
+ let(:resource) { Puppet::Type.type(:scheduled_task).new(:name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe') }
+
+ it "should delegate to Win32::TaskScheduler using the resource's name" do
+ @mock_task.expects(:exists?).with('Test Task').returns(true)
+
+ resource.provider.exists?.should == true
+ end
+ end
+
+ describe '#clear_task' do
+ before :each do
+ @mock_task = mock
+ @new_mock_task = mock
+ @mock_task.responds_like(Win32::TaskScheduler.new)
+ @new_mock_task.responds_like(Win32::TaskScheduler.new)
+ Win32::TaskScheduler.stubs(:new).returns(@mock_task, @new_mock_task)
+
+ described_class.any_instance.stubs(:exists?).returns(false)
+ end
+ let(:resource) { Puppet::Type.type(:scheduled_task).new(:name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe') }
+
+ it 'should clear the cached task object' do
+ resource.provider.task.should == @mock_task
+ resource.provider.task.should == @mock_task
+
+ resource.provider.clear_task
+
+ resource.provider.task.should == @new_mock_task
+ end
+
+ it 'should clear the cached list of triggers for the task' do
+ @mock_task.stubs(:trigger_count).returns(1)
+ @mock_task.stubs(:trigger).with(0).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE,
+ 'start_year' => 2011,
+ 'start_month' => 10,
+ 'start_day' => 13,
+ 'start_hour' => 14,
+ 'start_minute' => 21,
+ 'flags' => 0,
+ })
+ @new_mock_task.stubs(:trigger_count).returns(1)
+ @new_mock_task.stubs(:trigger).with(0).returns({
+ 'trigger_type' => Win32::TaskScheduler::TASK_TIME_TRIGGER_ONCE,
+ 'start_year' => 2012,
+ 'start_month' => 11,
+ 'start_day' => 14,
+ 'start_hour' => 15,
+ 'start_minute' => 22,
+ 'flags' => 0,
+ })
+
+ mock_task_trigger = {
+ 'start_date' => '2011-10-13',
+ 'start_time' => '14:21',
+ 'schedule' => 'once',
+ 'enabled' => true,
+ 'index' => 0,
+ }
+
+ resource.provider.trigger.should == mock_task_trigger
+ resource.provider.trigger.should == mock_task_trigger
+
+ resource.provider.clear_task
+
+ resource.provider.trigger.should == {
+ 'start_date' => '2012-11-14',
+ 'start_time' => '15:22',
+ 'schedule' => 'once',
+ 'enabled' => true,
+ 'index' => 0,
+ }
+ end
+ end
+
+ describe '.instances' do
+ it 'should use the list of .job files to construct the list of scheduled_tasks' do
+ job_files = ['foo.job', 'bar.job', 'baz.job']
+ Win32::TaskScheduler.any_instance.stubs(:tasks).returns(job_files)
+ job_files.each do |job|
+ job = File.basename(job, '.job')
+
+ described_class.expects(:new).with(:provider => :win32_taskscheduler, :name => job)
+ end
+
+ described_class.instances
+ end
+ end
+
+ describe '#user_insync?' do
+ let(:resource) { described_class.new(:name => 'foobar', :command => 'C:\Windows\System32\notepad.exe') }
+
+ before :each do
+ Puppet::Util::ADSI.stubs(:sid_for_account).with('system').returns('SYSTEM SID')
+ Puppet::Util::ADSI.stubs(:sid_for_account).with('joe').returns('SID A')
+ Puppet::Util::ADSI.stubs(:sid_for_account).with('MACHINE\joe').returns('SID A')
+ Puppet::Util::ADSI.stubs(:sid_for_account).with('bob').returns('SID B')
+ end
+
+ it 'should consider the user as in sync if the name matches' do
+ resource.should be_user_insync('joe', ['joe'])
+ end
+
+ it 'should consider the user as in sync if the current user is fully qualified' do
+ resource.should be_user_insync('MACHINE\joe', ['joe'])
+ end
+
+ it 'should consider a current user of the empty string to be the same as the system user' do
+ resource.should be_user_insync('', ['system'])
+ end
+
+ it 'should consider different users as being different' do
+ resource.should_not be_user_insync('joe', ['bob'])
+ end
+ end
+
+ describe '#trigger_insync?' do
+ let(:resource) { described_class.new(:name => 'foobar', :command => 'C:\Windows\System32\notepad.exe') }
+
+ it 'should not consider any extra current triggers as in sync' do
+ current = [
+ {'start_date' => '2011-09-12', 'start_time' => '15:15', 'schedule' => 'once'},
+ {'start_date' => '2012-10-13', 'start_time' => '16:16', 'schedule' => 'once'}
+ ]
+ desired = {'start_date' => '2011-09-12', 'start_time' => '15:15', 'schedule' => 'once'}
+
+ resource.should_not be_trigger_insync(current, desired)
+ end
+
+ it 'should not consider any extra desired triggers as in sync' do
+ current = {'start_date' => '2011-09-12', 'start_time' => '15:15', 'schedule' => 'once'}
+ desired = [
+ {'start_date' => '2011-09-12', 'start_time' => '15:15', 'schedule' => 'once'},
+ {'start_date' => '2012-10-13', 'start_time' => '16:16', 'schedule' => 'once'}
+ ]
+
+ resource.should_not be_trigger_insync(current, desired)
+ end
+
+ it 'should consider triggers to be in sync if the sets of current and desired triggers are equal' do
+ current = [
+ {'start_date' => '2011-09-12', 'start_time' => '15:15', 'schedule' => 'once'},
+ {'start_date' => '2012-10-13', 'start_time' => '16:16', 'schedule' => 'once'}
+ ]
+ desired = [
+ {'start_date' => '2011-09-12', 'start_time' => '15:15', 'schedule' => 'once'},
+ {'start_date' => '2012-10-13', 'start_time' => '16:16', 'schedule' => 'once'}
+ ]
+
+ resource.should be_trigger_insync(current, desired)
+ end
+ end
+
+ describe '#triggers_same?' do
+ let(:provider) { described_class.new(:name => 'foobar', :command => 'C:\Windows\System32\notepad.exe') }
+
+ it "should not consider a disabled 'current' trigger to be the same" do
+ current = {'schedule' => 'once', 'enabled' => false}
+ desired = {'schedule' => 'once'}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it 'should not consider triggers with different schedules to be the same' do
+ current = {'schedule' => 'once'}
+ desired = {'schedule' => 'weekly'}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ describe 'comparing daily triggers' do
+ it "should consider 'desired' triggers not specifying 'every' to have the same value as the 'current' trigger" do
+ current = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3}
+ desired = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30'}
+
+ provider.should be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'start_dates' as different triggers" do
+ current = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3}
+ desired = {'schedule' => 'daily', 'start_date' => '2012-09-12', 'start_time' => '15:30', 'every' => 3}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'start_times' as different triggers" do
+ current = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3}
+ desired = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:31', 'every' => 3}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it 'should not consider differences in date formatting to be different triggers' do
+ current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3}
+ desired = {'schedule' => 'weekly', 'start_date' => '2011-9-12', 'start_time' => '15:30', 'every' => 3}
+
+ provider.should be_triggers_same(current, desired)
+ end
+
+ it 'should not consider differences in time formatting to be different triggers' do
+ current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '5:30', 'every' => 3}
+ desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '05:30', 'every' => 3}
+
+ provider.should be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'every' as different triggers" do
+ current = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3}
+ desired = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 1}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it 'should consider triggers that are the same as being the same' do
+ trigger = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '01:30', 'every' => 1}
+
+ provider.should be_triggers_same(trigger, trigger)
+ end
+ end
+
+ describe 'comparing one-time triggers' do
+ it "should consider different 'start_dates' as different triggers" do
+ current = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30'}
+ desired = {'schedule' => 'daily', 'start_date' => '2012-09-12', 'start_time' => '15:30'}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'start_times' as different triggers" do
+ current = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:30'}
+ desired = {'schedule' => 'daily', 'start_date' => '2011-09-12', 'start_time' => '15:31'}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it 'should not consider differences in date formatting to be different triggers' do
+ current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30'}
+ desired = {'schedule' => 'weekly', 'start_date' => '2011-9-12', 'start_time' => '15:30'}
+
+ provider.should be_triggers_same(current, desired)
+ end
+
+ it 'should not consider differences in time formatting to be different triggers' do
+ current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '1:30'}
+ desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '01:30'}
+
+ provider.should be_triggers_same(current, desired)
+ end
+
+ it 'should consider triggers that are the same as being the same' do
+ trigger = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '01:30'}
+
+ provider.should be_triggers_same(trigger, trigger)
+ end
+ end
+
+ describe 'comparing monthly date-based triggers' do
+ it "should consider 'desired' triggers not specifying 'months' to have the same value as the 'current' trigger" do
+ current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [3], 'on' => [1,'last']}
+ desired = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'on' => [1, 'last']}
+
+ provider.should be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'start_dates' as different triggers" do
+ current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]}
+ desired = {'schedule' => 'monthly', 'start_date' => '2011-10-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'start_times' as different triggers" do
+ current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]}
+ desired = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '22:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it 'should not consider differences in date formatting to be different triggers' do
+ current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]}
+ desired = {'schedule' => 'monthly', 'start_date' => '2011-9-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]}
+
+ provider.should be_triggers_same(current, desired)
+ end
+
+ it 'should not consider differences in time formatting to be different triggers' do
+ current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '5:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]}
+ desired = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '05:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]}
+
+ provider.should be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'months' as different triggers" do
+ current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]}
+ desired = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1], 'on' => [1, 3, 5, 7]}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'on' as different triggers" do
+ current = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]}
+ desired = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 5, 7]}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it 'should consider triggers that are the same as being the same' do
+ trigger = {'schedule' => 'monthly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'months' => [1, 2], 'on' => [1, 3, 5, 7]}
+
+ provider.should be_triggers_same(trigger, trigger)
+ end
+ end
+
+ describe 'comparing monthly day-of-week-based triggers' do
+ it "should consider 'desired' triggers not specifying 'months' to have the same value as the 'current' trigger" do
+ current = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-09-12',
+ 'start_time' => '15:30',
+ 'months' => [3],
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['mon', 'tues', 'sat']
+ }
+ desired = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-09-12',
+ 'start_time' => '15:30',
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['mon', 'tues', 'sat']
+ }
+
+ provider.should be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'start_dates' as different triggers" do
+ current = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-09-12',
+ 'start_time' => '15:30',
+ 'months' => [3],
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['mon', 'tues', 'sat']
+ }
+ desired = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-10-12',
+ 'start_time' => '15:30',
+ 'months' => [3],
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['mon', 'tues', 'sat']
+ }
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'start_times' as different triggers" do
+ current = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-09-12',
+ 'start_time' => '15:30',
+ 'months' => [3],
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['mon', 'tues', 'sat']
+ }
+ desired = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-09-12',
+ 'start_time' => '22:30',
+ 'months' => [3],
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['mon', 'tues', 'sat']
+ }
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'months' as different triggers" do
+ current = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-09-12',
+ 'start_time' => '15:30',
+ 'months' => [3],
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['mon', 'tues', 'sat']
+ }
+ desired = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-09-12',
+ 'start_time' => '15:30',
+ 'months' => [3, 5, 7, 9],
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['mon', 'tues', 'sat']
+ }
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'which_occurrence' as different triggers" do
+ current = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-09-12',
+ 'start_time' => '15:30',
+ 'months' => [3],
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['mon', 'tues', 'sat']
+ }
+ desired = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-09-12',
+ 'start_time' => '15:30',
+ 'months' => [3],
+ 'which_occurrence' => 'last',
+ 'day_of_week' => ['mon', 'tues', 'sat']
+ }
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'day_of_week' as different triggers" do
+ current = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-09-12',
+ 'start_time' => '15:30',
+ 'months' => [3],
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['mon', 'tues', 'sat']
+ }
+ desired = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-09-12',
+ 'start_time' => '15:30',
+ 'months' => [3],
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['fri']
+ }
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it 'should consider triggers that are the same as being the same' do
+ trigger = {
+ 'schedule' => 'monthly',
+ 'start_date' => '2011-09-12',
+ 'start_time' => '15:30',
+ 'months' => [3],
+ 'which_occurrence' => 'first',
+ 'day_of_week' => ['mon', 'tues', 'sat']
+ }
+
+ provider.should be_triggers_same(trigger, trigger)
+ end
+ end
+
+ describe 'comparing weekly triggers' do
+ it "should consider 'desired' triggers not specifying 'day_of_week' to have the same value as the 'current' trigger" do
+ current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']}
+ desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3}
+
+ provider.should be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'start_dates' as different triggers" do
+ current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']}
+ desired = {'schedule' => 'weekly', 'start_date' => '2011-10-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'start_times' as different triggers" do
+ current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']}
+ desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '22:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it 'should not consider differences in date formatting to be different triggers' do
+ current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']}
+ desired = {'schedule' => 'weekly', 'start_date' => '2011-9-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']}
+
+ provider.should be_triggers_same(current, desired)
+ end
+
+ it 'should not consider differences in time formatting to be different triggers' do
+ current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '1:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']}
+ desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '01:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']}
+
+ provider.should be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'every' as different triggers" do
+ current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 1, 'day_of_week' => ['mon', 'wed', 'fri']}
+ desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it "should consider different 'day_of_week' as different triggers" do
+ current = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']}
+ desired = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['fri']}
+
+ provider.should_not be_triggers_same(current, desired)
+ end
+
+ it 'should consider triggers that are the same as being the same' do
+ trigger = {'schedule' => 'weekly', 'start_date' => '2011-09-12', 'start_time' => '15:30', 'every' => 3, 'day_of_week' => ['mon', 'wed', 'fri']}
+
+ provider.should be_triggers_same(trigger, trigger)
+ end
+ end
+ end
+
+ describe '#normalized_date' do
+ it 'should format the date without leading zeros' do
+ described_class.normalized_date('2011-01-01').should == '2011-1-1'
+ end
+ end
+
+ describe '#normalized_time' do
+ it 'should format the time as {24h}:{minutes}' do
+ described_class.normalized_time('8:37 PM').should == '20:37'
+ end
+ end
+
+ describe '#translate_hash_to_trigger' do
+ before :each do
+ @puppet_trigger = {
+ 'start_date' => '2011-1-1',
+ 'start_time' => '01:10'
+ }
+ end
+ let(:provider) { described_class.new(:name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe') }
+ let(:trigger) { provider.translate_hash_to_trigger(@puppet_trigger) }
+
+ describe 'when given a one-time trigger' do
+ before :each do
+ @puppet_trigger['schedule'] = 'once'
+ end
+
+ it 'should set the trigger_type to Win32::TaskScheduler::ONCE' do
+ trigger['trigger_type'].should == Win32::TaskScheduler::ONCE
+ end
+
+ it 'should not set a type' do
+ trigger.should_not be_has_key('type')
+ end
+
+ it "should require 'start_date'" do
+ @puppet_trigger.delete('start_date')
+
+ expect { trigger }.to raise_error(
+ Puppet::Error,
+ /Must specify 'start_date' when defining a one-time trigger/
+ )
+ end
+
+ it "should require 'start_time'" do
+ @puppet_trigger.delete('start_time')
+
+ expect { trigger }.to raise_error(
+ Puppet::Error,
+ /Must specify 'start_time' when defining a trigger/
+ )
+ end
+
+ it_behaves_like "a trigger that handles start_date and start_time" do
+ let(:trigger_hash) {{'schedule' => 'once' }}
+ end
+ end
+
+ describe 'when given a daily trigger' do
+ before :each do
+ @puppet_trigger['schedule'] = 'daily'
+ end
+
+ it "should default 'every' to 1" do
+ trigger['type']['days_interval'].should == 1
+ end
+
+ it "should use the specified value for 'every'" do
+ @puppet_trigger['every'] = 5
+
+ trigger['type']['days_interval'].should == 5
+ end
+
+ it "should default 'start_date' to 'today'" do
+ @puppet_trigger.delete('start_date')
+ today = Time.now
+
+ trigger['start_year'].should == today.year
+ trigger['start_month'].should == today.month
+ trigger['start_day'].should == today.day
+ end
+
+ it_behaves_like "a trigger that handles start_date and start_time" do
+ let(:trigger_hash) {{'schedule' => 'daily', 'every' => 1}}
+ end
+ end
+
+ describe 'when given a weekly trigger' do
+ before :each do
+ @puppet_trigger['schedule'] = 'weekly'
+ end
+
+ it "should default 'every' to 1" do
+ trigger['type']['weeks_interval'].should == 1
+ end
+
+ it "should use the specified value for 'every'" do
+ @puppet_trigger['every'] = 4
+
+ trigger['type']['weeks_interval'].should == 4
+ end
+
+ it "should default 'day_of_week' to be every day of the week" do
+ trigger['type']['days_of_week'].should == Win32::TaskScheduler::MONDAY |
+ Win32::TaskScheduler::TUESDAY |
+ Win32::TaskScheduler::WEDNESDAY |
+ Win32::TaskScheduler::THURSDAY |
+ Win32::TaskScheduler::FRIDAY |
+ Win32::TaskScheduler::SATURDAY |
+ Win32::TaskScheduler::SUNDAY
+ end
+
+ it "should use the specified value for 'day_of_week'" do
+ @puppet_trigger['day_of_week'] = ['mon', 'wed', 'fri']
+
+ trigger['type']['days_of_week'].should == Win32::TaskScheduler::MONDAY |
+ Win32::TaskScheduler::WEDNESDAY |
+ Win32::TaskScheduler::FRIDAY
+ end
+
+ it "should default 'start_date' to 'today'" do
+ @puppet_trigger.delete('start_date')
+ today = Time.now
+
+ trigger['start_year'].should == today.year
+ trigger['start_month'].should == today.month
+ trigger['start_day'].should == today.day
+ end
+
+ it_behaves_like "a trigger that handles start_date and start_time" do
+ let(:trigger_hash) {{'schedule' => 'weekly', 'every' => 1, 'day_of_week' => 'mon'}}
+ end
+ end
+
+ shared_examples_for 'a monthly schedule' do
+ it "should default 'months' to be every month" do
+ trigger['type']['months'].should == Win32::TaskScheduler::JANUARY |
+ Win32::TaskScheduler::FEBRUARY |
+ Win32::TaskScheduler::MARCH |
+ Win32::TaskScheduler::APRIL |
+ Win32::TaskScheduler::MAY |
+ Win32::TaskScheduler::JUNE |
+ Win32::TaskScheduler::JULY |
+ Win32::TaskScheduler::AUGUST |
+ Win32::TaskScheduler::SEPTEMBER |
+ Win32::TaskScheduler::OCTOBER |
+ Win32::TaskScheduler::NOVEMBER |
+ Win32::TaskScheduler::DECEMBER
+ end
+
+ it "should use the specified value for 'months'" do
+ @puppet_trigger['months'] = [2, 8]
+
+ trigger['type']['months'].should == Win32::TaskScheduler::FEBRUARY |
+ Win32::TaskScheduler::AUGUST
+ end
+ end
+
+ describe 'when given a monthly date-based trigger' do
+ before :each do
+ @puppet_trigger['schedule'] = 'monthly'
+ @puppet_trigger['on'] = [7, 14]
+ end
+
+ it_behaves_like 'a monthly schedule'
+
+ it "should not allow 'which_occurrence' to be specified" do
+ @puppet_trigger['which_occurrence'] = 'first'
+
+ expect {trigger}.to raise_error(
+ Puppet::Error,
+ /Neither 'day_of_week' nor 'which_occurrence' can be specified when creating a monthly date-based trigger/
+ )
+ end
+
+ it "should not allow 'day_of_week' to be specified" do
+ @puppet_trigger['day_of_week'] = 'mon'
+
+ expect {trigger}.to raise_error(
+ Puppet::Error,
+ /Neither 'day_of_week' nor 'which_occurrence' can be specified when creating a monthly date-based trigger/
+ )
+ end
+
+ it "should require 'on'" do
+ @puppet_trigger.delete('on')
+
+ expect {trigger}.to raise_error(
+ Puppet::Error,
+ /Don't know how to create a 'monthly' schedule with the options: schedule, start_date, start_time/
+ )
+ end
+
+ it "should default 'start_date' to 'today'" do
+ @puppet_trigger.delete('start_date')
+ today = Time.now
+
+ trigger['start_year'].should == today.year
+ trigger['start_month'].should == today.month
+ trigger['start_day'].should == today.day
+ end
+
+ it_behaves_like "a trigger that handles start_date and start_time" do
+ let(:trigger_hash) {{'schedule' => 'monthly', 'months' => 1, 'on' => 1}}
+ end
+ end
+
+ describe 'when given a monthly day-of-week-based trigger' do
+ before :each do
+ @puppet_trigger['schedule'] = 'monthly'
+ @puppet_trigger['which_occurrence'] = 'first'
+ @puppet_trigger['day_of_week'] = 'mon'
+ end
+
+ it_behaves_like 'a monthly schedule'
+
+ it "should not allow 'on' to be specified" do
+ @puppet_trigger['on'] = 15
+
+ expect {trigger}.to raise_error(
+ Puppet::Error,
+ /Neither 'day_of_week' nor 'which_occurrence' can be specified when creating a monthly date-based trigger/
+ )
+ end
+
+ it "should require 'which_occurrence'" do
+ @puppet_trigger.delete('which_occurrence')
+
+ expect {trigger}.to raise_error(
+ Puppet::Error,
+ /which_occurrence must be specified when creating a monthly day-of-week based trigger/
+ )
+ end
+
+ it "should require 'day_of_week'" do
+ @puppet_trigger.delete('day_of_week')
+
+ expect {trigger}.to raise_error(
+ Puppet::Error,
+ /day_of_week must be specified when creating a monthly day-of-week based trigger/
+ )
+ end
+
+ it "should default 'start_date' to 'today'" do
+ @puppet_trigger.delete('start_date')
+ today = Time.now
+
+ trigger['start_year'].should == today.year
+ trigger['start_month'].should == today.month
+ trigger['start_day'].should == today.day
+ end
+
+ it_behaves_like "a trigger that handles start_date and start_time" do
+ let(:trigger_hash) {{'schedule' => 'monthly', 'months' => 1, 'which_occurrence' => 'first', 'day_of_week' => 'mon'}}
+ end
+ end
+ end
+
+ describe '#validate_trigger' do
+ let(:provider) { described_class.new(:name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe') }
+
+ it 'should succeed if all passed triggers translate from hashes to triggers' do
+ triggers_to_validate = [
+ {'schedule' => 'once', 'start_date' => '2011-09-13', 'start_time' => '13:50'},
+ {'schedule' => 'weekly', 'start_date' => '2011-09-13', 'start_time' => '13:50', 'day_of_week' => 'mon'}
+ ]
+
+ provider.validate_trigger(triggers_to_validate).should == true
+ end
+
+ it 'should use the exception from translate_hash_to_trigger when it fails' do
+ triggers_to_validate = [
+ {'schedule' => 'once', 'start_date' => '2011-09-13', 'start_time' => '13:50'},
+ {'schedule' => 'monthly', 'this is invalid' => true}
+ ]
+
+ expect {provider.validate_trigger(triggers_to_validate)}.to raise_error(
+ Puppet::Error,
+ /#{Regexp.escape("Unknown trigger option(s): ['this is invalid']")}/
+ )
+ end
+ end
+
+ describe '#flush' do
+ let(:resource) do
+ Puppet::Type.type(:scheduled_task).new(
+ :name => 'Test Task',
+ :command => 'C:\Windows\System32\notepad.exe',
+ :ensure => @ensure
+ )
+ end
+
+ before :each do
+ @mock_task = mock
+ @mock_task.responds_like(Win32::TaskScheduler.new)
+ @mock_task.stubs(:exists?).returns(true)
+ @mock_task.stubs(:activate)
+ Win32::TaskScheduler.stubs(:new).returns(@mock_task)
+
+ @command = 'C:\Windows\System32\notepad.exe'
+ end
+
+ describe 'when :ensure is :present' do
+ before :each do
+ @ensure = :present
+ end
+
+ it 'should save the task' do
+ @mock_task.expects(:save)
+
+ resource.provider.flush
+ end
+
+ it 'should fail if the command is not specified' do
+ resource = Puppet::Type.type(:scheduled_task).new(
+ :name => 'Test Task',
+ :ensure => @ensure
+ )
+
+ expect { resource.provider.flush }.to raise_error(
+ Puppet::Error,
+ 'Parameter command is required.'
+ )
+ end
+ end
+
+ describe 'when :ensure is :absent' do
+ before :each do
+ @ensure = :absent
+ @mock_task.stubs(:activate)
+ end
+
+ it 'should not save the task if :ensure is :absent' do
+ @mock_task.expects(:save).never
+
+ resource.provider.flush
+ end
+
+ it 'should not fail if the command is not specified' do
+ @mock_task.stubs(:save)
+
+ resource = Puppet::Type.type(:scheduled_task).new(
+ :name => 'Test Task',
+ :ensure => @ensure
+ )
+
+ resource.provider.flush
+ end
+ end
+ end
+
+ describe 'property setter methods' do
+ let(:resource) do
+ Puppet::Type.type(:scheduled_task).new(
+ :name => 'Test Task',
+ :command => 'C:\dummy_task.exe'
+ )
+ end
+
+ before :each do
+ @mock_task = mock
+ @mock_task.responds_like(Win32::TaskScheduler.new)
+ @mock_task.stubs(:exists?).returns(true)
+ @mock_task.stubs(:activate)
+ Win32::TaskScheduler.stubs(:new).returns(@mock_task)
+ end
+
+ describe '#command=' do
+ it 'should set the application_name on the task' do
+ @mock_task.expects(:application_name=).with('C:\Windows\System32\notepad.exe')
+
+ resource.provider.command = 'C:\Windows\System32\notepad.exe'
+ end
+ end
+
+ describe '#arguments=' do
+ it 'should set the parameters on the task' do
+ @mock_task.expects(:parameters=).with(['/some /arguments /here'])
+
+ resource.provider.arguments = ['/some /arguments /here']
+ end
+ end
+
+ describe '#working_dir=' do
+ it 'should set the working_directory on the task' do
+ @mock_task.expects(:working_directory=).with('C:\Windows\System32')
+
+ resource.provider.working_dir = 'C:\Windows\System32'
+ end
+ end
+
+ describe '#enabled=' do
+ it 'should set the disabled flag if the task should be disabled' do
+ @mock_task.stubs(:flags).returns(0)
+ @mock_task.expects(:flags=).with(Win32::TaskScheduler::DISABLED)
+
+ resource.provider.enabled = :false
+ end
+
+ it 'should clear the disabled flag if the task should be enabled' do
+ @mock_task.stubs(:flags).returns(Win32::TaskScheduler::DISABLED)
+ @mock_task.expects(:flags=).with(0)
+
+ resource.provider.enabled = :true
+ end
+ end
+
+ describe '#trigger=' do
+ let(:resource) do
+ Puppet::Type.type(:scheduled_task).new(
+ :name => 'Test Task',
+ :command => 'C:\Windows\System32\notepad.exe',
+ :trigger => @trigger
+ )
+ end
+
+ before :each do
+ @mock_task = mock
+ @mock_task.responds_like(Win32::TaskScheduler.new)
+ @mock_task.stubs(:exists?).returns(true)
+ @mock_task.stubs(:activate)
+ Win32::TaskScheduler.stubs(:new).returns(@mock_task)
+ end
+
+ it 'should not consider all duplicate current triggers in sync with a single desired trigger' do
+ @trigger = {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10'}
+ current_triggers = [
+ {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10', 'index' => 0},
+ {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10', 'index' => 1},
+ {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10', 'index' => 2},
+ ]
+ resource.provider.stubs(:trigger).returns(current_triggers)
+ @mock_task.expects(:delete_trigger).with(1)
+ @mock_task.expects(:delete_trigger).with(2)
+
+ resource.provider.trigger = @trigger
+ end
+
+ it 'should remove triggers not defined in the resource' do
+ @trigger = {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10'}
+ current_triggers = [
+ {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10', 'index' => 0},
+ {'schedule' => 'once', 'start_date' => '2012-09-15', 'start_time' => '15:10', 'index' => 1},
+ {'schedule' => 'once', 'start_date' => '2013-09-15', 'start_time' => '15:10', 'index' => 2},
+ ]
+ resource.provider.stubs(:trigger).returns(current_triggers)
+ @mock_task.expects(:delete_trigger).with(1)
+ @mock_task.expects(:delete_trigger).with(2)
+
+ resource.provider.trigger = @trigger
+ end
+
+ it 'should add triggers defined in the resource, but not found on the system' do
+ @trigger = [
+ {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10'},
+ {'schedule' => 'once', 'start_date' => '2012-09-15', 'start_time' => '15:10'},
+ {'schedule' => 'once', 'start_date' => '2013-09-15', 'start_time' => '15:10'},
+ ]
+ current_triggers = [
+ {'schedule' => 'once', 'start_date' => '2011-09-15', 'start_time' => '15:10', 'index' => 0},
+ ]
+ resource.provider.stubs(:trigger).returns(current_triggers)
+ @mock_task.expects(:trigger=).with(resource.provider.translate_hash_to_trigger(@trigger[1]))
+ @mock_task.expects(:trigger=).with(resource.provider.translate_hash_to_trigger(@trigger[2]))
+
+ resource.provider.trigger = @trigger
+ end
+ end
+
+ describe '#user=' do
+ before :each do
+ @mock_task = mock
+ @mock_task.responds_like(Win32::TaskScheduler.new)
+ @mock_task.stubs(:exists?).returns(true)
+ @mock_task.stubs(:activate)
+ Win32::TaskScheduler.stubs(:new).returns(@mock_task)
+ end
+
+ it 'should use nil for user and password when setting the user to the SYSTEM account' do
+ Puppet::Util::ADSI.stubs(:sid_for_account).with('system').returns('SYSTEM SID')
+
+ resource = Puppet::Type.type(:scheduled_task).new(
+ :name => 'Test Task',
+ :command => 'C:\dummy_task.exe',
+ :user => 'system'
+ )
+
+ @mock_task.expects(:set_account_information).with(nil, nil)
+
+ resource.provider.user = 'system'
+ end
+
+ it 'should use the specified user and password when setting the user to anything other than SYSTEM' do
+ Puppet::Util::ADSI.stubs(:sid_for_account).with('my_user_name').returns('SID A')
+
+ resource = Puppet::Type.type(:scheduled_task).new(
+ :name => 'Test Task',
+ :command => 'C:\dummy_task.exe',
+ :user => 'my_user_name',
+ :password => 'my password'
+ )
+
+ @mock_task.expects(:set_account_information).with('my_user_name', 'my password')
+
+ resource.provider.user = 'my_user_name'
+ end
+ end
+ end
+
+ describe '#create' do
+ let(:resource) do
+ Puppet::Type.type(:scheduled_task).new(
+ :name => 'Test Task',
+ :enabled => @enabled,
+ :command => @command,
+ :arguments => @arguments,
+ :working_dir => @working_dir,
+ :trigger => { 'schedule' => 'once', 'start_date' => '2011-09-27', 'start_time' => '17:00' }
+ )
+ end
+
+ before :each do
+ @enabled = :true
+ @command = 'C:\Windows\System32\notepad.exe'
+ @arguments = '/a /list /of /arguments'
+ @working_dir = 'C:\Windows\Some\Directory'
+
+ @mock_task = mock
+ @mock_task.responds_like(Win32::TaskScheduler.new)
+ @mock_task.stubs(:exists?).returns(true)
+ @mock_task.stubs(:activate)
+ @mock_task.stubs(:application_name=)
+ @mock_task.stubs(:parameters=)
+ @mock_task.stubs(:working_directory=)
+ @mock_task.stubs(:set_account_information)
+ @mock_task.stubs(:flags)
+ @mock_task.stubs(:flags=)
+ @mock_task.stubs(:trigger_count).returns(0)
+ @mock_task.stubs(:trigger=)
+ @mock_task.stubs(:save)
+ Win32::TaskScheduler.stubs(:new).returns(@mock_task)
+
+ described_class.any_instance.stubs(:sync_triggers)
+ end
+
+ it 'should set the command' do
+ resource.provider.expects(:command=).with(@command)
+
+ resource.provider.create
+ end
+
+ it 'should set the arguments' do
+ resource.provider.expects(:arguments=).with([@arguments])
+
+ resource.provider.create
+ end
+
+ it 'should set the working_dir' do
+ resource.provider.expects(:working_dir=).with(@working_dir)
+
+ resource.provider.create
+ end
+
+ it "should set the user" do
+ resource.provider.expects(:user=).with(:system)
+
+ resource.provider.create
+ end
+
+ it 'should set the enabled property' do
+ resource.provider.expects(:enabled=)
+
+ resource.provider.create
+ end
+
+ it 'should sync triggers' do
+ resource.provider.expects(:trigger=)
+
+ resource.provider.create
+ end
+ end
+end
diff --git a/spec/unit/type/scheduled_task_spec.rb b/spec/unit/type/scheduled_task_spec.rb
new file mode 100644
index 000000000..17d84900e
--- /dev/null
+++ b/spec/unit/type/scheduled_task_spec.rb
@@ -0,0 +1,102 @@
+#!/usr/bin/env rspec
+require 'spec_helper'
+
+describe Puppet::Type.type(:scheduled_task), :if => Puppet.features.microsoft_windows? do
+
+ it 'should use name as the namevar' do
+ described_class.new(
+ :title => 'Foo',
+ :command => 'C:\Windows\System32\notepad.exe'
+ ).name.must == 'Foo'
+ end
+
+ describe 'when setting the command' do
+ it 'should accept an absolute path to the command' do
+ described_class.new(:name => 'Test Task', :command => 'C:\Windows\System32\notepad.exe')[:command].should == 'C:\Windows\System32\notepad.exe'
+ end
+
+ it 'should fail if the path to the command is not absolute' do
+ expect {
+ described_class.new(:name => 'Test Task', :command => 'notepad.exe')
+ }.to raise_error(
+ Puppet::Error,
+ /Parameter command failed: Must be specified using an absolute path\./
+ )
+ end
+ end
+
+ describe 'when setting the command arguments' do
+ it 'should fail if provided an array' do
+ expect {
+ described_class.new(
+ :name => 'Test Task',
+ :command => 'C:\Windows\System32\notepad.exe',
+ :arguments => ['/a', '/b', '/c']
+ )
+ }.to raise_error(
+ Puppet::Error,
+ /Parameter arguments failed: Must be specified as a single string/
+ )
+ end
+
+ it 'should accept a string' do
+ described_class.new(
+ :name => 'Test Task',
+ :command => 'C:\Windows\System32\notepad.exe',
+ :arguments => '/a /b /c'
+ )[:arguments].should == ['/a /b /c']
+ end
+
+ it 'should allow not specifying any command arguments' do
+ described_class.new(
+ :name => 'Test Task',
+ :command => 'C:\Windows\System32\notepad.exe'
+ )[:arguments].should_not be
+ end
+ end
+
+ describe 'when setting whether the task is enabled or not' do
+ end
+
+ describe 'when setting the working directory' do
+ it 'should accept an absolute path to the working directory' do
+ described_class.new(
+ :name => 'Test Task',
+ :command => 'C:\Windows\System32\notepad.exe',
+ :working_dir => 'C:\Windows\System32'
+ )[:working_dir].should == 'C:\Windows\System32'
+ end
+
+ it 'should fail if the path to the working directory is not absolute' do
+ expect {
+ described_class.new(
+ :name => 'Test Task',
+ :command => 'C:\Windows\System32\notepad.exe',
+ :working_dir => 'Windows\System32'
+ )
+ }.to raise_error(
+ Puppet::Error,
+ /Parameter working_dir failed: Must be specified using an absolute path/
+ )
+ end
+
+ it 'should allow not specifying any working directory' do
+ described_class.new(
+ :name => 'Test Task',
+ :command => 'C:\Windows\System32\notepad.exe'
+ )[:working_dir].should_not be
+ end
+ end
+
+ describe 'when setting the trigger' do
+ it 'should delegate to the provider to validate the trigger' do
+ described_class.defaultprovider.any_instance.expects(:validate_trigger).returns(true)
+
+ described_class.new(
+ :name => 'Test Task',
+ :command => 'C:\Windows\System32\notepad.exe',
+ :trigger => {'schedule' => 'once', 'start_date' => '2011-09-16', 'start_time' => '13:20'}
+ )
+ end
+ end
+end
diff --git a/spec/unit/util/adsi_spec.rb b/spec/unit/util/adsi_spec.rb
index 6f0428b6f..7e5672b2d 100755
--- a/spec/unit/util/adsi_spec.rb
+++ b/spec/unit/util/adsi_spec.rb
@@ -1,223 +1,232 @@
#!/usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/adsi'
describe Puppet::Util::ADSI do
let(:connection) { stub 'connection' }
before(:each) do
Puppet::Util::ADSI.instance_variable_set(:@computer_name, 'testcomputername')
Puppet::Util::ADSI.stubs(:connect).returns connection
end
after(:each) do
Puppet::Util::ADSI.instance_variable_set(:@computer_name, nil)
end
it "should generate the correct URI for a resource" do
Puppet::Util::ADSI.uri('test', 'user').should == "WinNT://testcomputername/test,user"
end
it "should be able to get the name of the computer" do
Puppet::Util::ADSI.computer_name.should == 'testcomputername'
end
it "should be able to provide the correct WinNT base URI for the computer" do
Puppet::Util::ADSI.computer_uri.should == "WinNT://testcomputername"
end
describe ".sid_for_account" do
- it "should return the SID" do
- result = [stub('account', :Sid => 'S-1-1-50')]
- connection.expects(:execquery).returns(result)
+ it "should return nil if the account does not exist" do
+ connection.expects(:execquery).returns([])
- Puppet::Util::ADSI.sid_for_account('joe').should == 'S-1-1-50'
+ Puppet::Util::ADSI.sid_for_account('foobar').should be_nil
end
- it "should return nil if the account does not exist" do
- connection.expects(:execquery).returns([])
+ it "should return a SID for a passed user or group name" do
+ Puppet::Util::ADSI.expects(:execquery).with(
+ "SELECT Sid from Win32_Account WHERE Name = 'testers' AND LocalAccount = true"
+ ).returns([stub('acct_id', :Sid => 'S-1-5-32-547')])
- Puppet::Util::ADSI.sid_for_account('foobar').should be_nil
+ Puppet::Util::ADSI.sid_for_account('testers').should == 'S-1-5-32-547'
+ end
+
+ it "should return a SID for a passed fully-qualified user or group name" do
+ Puppet::Util::ADSI.expects(:execquery).with(
+ "SELECT Sid from Win32_Account WHERE Name = 'testers' AND Domain = 'MACHINE' AND LocalAccount = true"
+ ).returns([stub('acct_id', :Sid => 'S-1-5-32-547')])
+
+ Puppet::Util::ADSI.sid_for_account('MACHINE\testers').should == 'S-1-5-32-547'
end
end
describe Puppet::Util::ADSI::User do
let(:username) { 'testuser' }
it "should generate the correct URI" do
Puppet::Util::ADSI::User.uri(username).should == "WinNT://testcomputername/#{username},user"
end
it "should be able to create a user" do
adsi_user = stub('adsi')
connection.expects(:Create).with('user', username).returns(adsi_user)
Puppet::Util::ADSI::Group.expects(:exists?).with(username).returns(false)
user = Puppet::Util::ADSI::User.create(username)
user.should be_a(Puppet::Util::ADSI::User)
user.native_user.should == adsi_user
end
it "should be able to check the existence of a user" do
Puppet::Util::ADSI.expects(:connect).with("WinNT://testcomputername/#{username},user").returns connection
Puppet::Util::ADSI::User.exists?(username).should be_true
end
it "should be able to delete a user" do
connection.expects(:Delete).with('user', username)
Puppet::Util::ADSI::User.delete(username)
end
describe "an instance" do
let(:adsi_user) { stub 'user' }
let(:user) { Puppet::Util::ADSI::User.new(username, adsi_user) }
it "should provide its groups as a list of names" do
names = ["group1", "group2"]
groups = names.map { |name| mock('group', :Name => name) }
adsi_user.expects(:Groups).returns(groups)
user.groups.should =~ names
end
it "should be able to test whether a given password is correct" do
Puppet::Util::ADSI::User.expects(:logon).with(username, 'pwdwrong').returns(false)
Puppet::Util::ADSI::User.expects(:logon).with(username, 'pwdright').returns(true)
user.password_is?('pwdwrong').should be_false
user.password_is?('pwdright').should be_true
end
it "should be able to set a password" do
adsi_user.expects(:SetPassword).with('pwd')
adsi_user.expects(:SetInfo).at_least_once
flagname = "UserFlags"
fADS_UF_DONT_EXPIRE_PASSWD = 0x10000
adsi_user.expects(:Get).with(flagname).returns(0)
adsi_user.expects(:Put).with(flagname, fADS_UF_DONT_EXPIRE_PASSWD)
user.password = 'pwd'
end
it "should generate the correct URI" do
user.uri.should == "WinNT://testcomputername/#{username},user"
end
describe "when given a set of groups to which to add the user" do
let(:groups_to_set) { 'group1,group2' }
before(:each) do
user.expects(:groups).returns ['group2', 'group3']
end
describe "if membership is specified as inclusive" do
it "should add the user to those groups, and remove it from groups not in the list" do
group1 = stub 'group1'
group1.expects(:Add).with("WinNT://testcomputername/#{username},user")
group3 = stub 'group1'
group3.expects(:Remove).with("WinNT://testcomputername/#{username},user")
Puppet::Util::ADSI.expects(:connect).with('WinNT://testcomputername/group1,group').returns group1
Puppet::Util::ADSI.expects(:connect).with('WinNT://testcomputername/group3,group').returns group3
user.set_groups(groups_to_set, false)
end
end
describe "if membership is specified as minimum" do
it "should add the user to the specified groups without affecting its other memberships" do
group1 = stub 'group1'
group1.expects(:Add).with("WinNT://testcomputername/#{username},user")
Puppet::Util::ADSI.expects(:connect).with('WinNT://testcomputername/group1,group').returns group1
user.set_groups(groups_to_set, true)
end
end
end
end
end
describe Puppet::Util::ADSI::Group do
let(:groupname) { 'testgroup' }
describe "an instance" do
let(:adsi_group) { stub 'group' }
let(:group) { Puppet::Util::ADSI::Group.new(groupname, adsi_group) }
it "should be able to add a member" do
adsi_group.expects(:Add).with("WinNT://testcomputername/someone,user")
group.add_member('someone')
end
it "should be able to remove a member" do
adsi_group.expects(:Remove).with("WinNT://testcomputername/someone,user")
group.remove_member('someone')
end
it "should provide its groups as a list of names" do
names = ['user1', 'user2']
users = names.map { |name| mock('user', :Name => name) }
adsi_group.expects(:Members).returns(users)
group.members.should =~ names
end
it "should be able to add a list of users to a group" do
names = ['user1', 'user2']
adsi_group.expects(:Members).returns names.map{|n| stub(:Name => n)}
adsi_group.expects(:Remove).with('WinNT://testcomputername/user1,user')
adsi_group.expects(:Add).with('WinNT://testcomputername/user3,user')
group.set_members(['user2', 'user3'])
end
it "should generate the correct URI" do
group.uri.should == "WinNT://testcomputername/#{groupname},group"
end
end
it "should generate the correct URI" do
Puppet::Util::ADSI::Group.uri("people").should == "WinNT://testcomputername/people,group"
end
it "should be able to create a group" do
adsi_group = stub("adsi")
connection.expects(:Create).with('group', groupname).returns(adsi_group)
Puppet::Util::ADSI::User.expects(:exists?).with(groupname).returns(false)
group = Puppet::Util::ADSI::Group.create(groupname)
group.should be_a(Puppet::Util::ADSI::Group)
group.native_group.should == adsi_group
end
it "should be able to confirm the existence of a group" do
Puppet::Util::ADSI.expects(:connect).with("WinNT://testcomputername/#{groupname},group").returns connection
Puppet::Util::ADSI::Group.exists?(groupname).should be_true
end
it "should be able to delete a group" do
connection.expects(:Delete).with('group', groupname)
Puppet::Util::ADSI::Group.delete(groupname)
end
end
end