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